Skip to main content

Evalium Frontend Conventions

SvelteKit 2 + Svelte 5 (Runes) — “Reactive Chassis for a Ferrari Engine”

This document defines the frontend conventions for Evalium so the UI stays:

  • simple for SMBs (calm, guided, progressive disclosure)
  • defensible for enterprise (auditability, provenance, immutable history)
  • consistent at scale (no component spaghetti, no ad-hoc navigation logic)

Backend remains authoritative for permissions, RLS scope, and ledger integrity.


0) Capability Baseline (Validated 2026-02-25)

Use these conventions against the current backend capability shape:

  • authoring inventory is live for Questions, Evaluations, and Passages.
  • Saved inventory views are live for Questions/Evaluations (personal, shared).
  • Taxonomy assignment/filtering is live on Questions/Evaluations (including descendants semantics).
  • Usage graphs are live on Questions/Evaluations with dependency details.
  • Bulk authoring actions are live (archive, restore, taxonomyReplace) with dryRun.
  • Import preview/commit is live for Questions, Passages, Evaluations.
  • Evaluation version policy fields are live (randomizationType, fixedSeed, requiredVerificationLevel, reviewPolicy, autoApprove) and feedback tag config is live.

Still not fully represented as first-class frontend assets:

  • Compliance case entity/timeline (jobs + ledger are live; case orchestration is pending).
  • Skills/evidence-fact hubs (skills inference remains parked).
  • First-class proctor command endpoints (pause / resume / terminate) as explicit APIs.
  • Granular per-user/group sharing ACLs for inventory views (beyond personal / shared).

1) Core UX Doctrine (Non-Negotiable)

1.1 SMB Interface, Enterprise Engine

Never expose backend complexity (versioning, silos, recalculation jobs) directly. Use simple actions like Save, Publish, Refresh, Explain.

If the user can view an entity reference, it is clickable in-place.

  • No “dead text” that looks like a reference (chips, badges, names, IDs).
  • If a user cannot view a target, render as non-link with tooltip: “No permission to view.”
  • On drawer-enabled surfaces, normal click opens a read-only right drawer peek for supported entity references.
  • Drawer peek is for browse/review context only; edit and deeper workflow actions must route to the canonical full page.
  • Cmd/Ctrl-click, shift-click, and middle-click must keep native browser behavior and open canonical full pages.
  • If a drawer read model is not available for that entity on that surface, normal click must navigate to the canonical full page.
  • Rollout status (2026-03-06): production drawer-peek behavior is fully wired on /questions; other surfaces should remain canonical full-page until explicitly wired.

1.4 Ledger Truth (MUST)

High-stakes data is append-only. UI must:

  • display history not “overwrite”
  • show correction badges and audit trails (e.g., “Corrected” → remediation reason)

1.5 Status Label Consistency (MUST)

Use canonical user-facing labels from /docs/ux/language-and-tone-guidelines.md and /docs/ux/UX-SHARED-COMPONENTS-CONTRACT.md.

At minimum:

  • readiness: Ready, Needs review, Blocked
  • exception triage: Open, Acknowledged, Resolved
  • assignment monitor: Not started, In progress, Completed, Expired

Implementation rule:

  • keep enum mapping in one shared frontend utility so chips, filters, and table exports cannot drift.

1.6 i18n Foundation Contract (MUST)

Frontend i18n foundation is Inlang Paraglide-JS.

Rules:

  • no hardcoded user-facing strings in Svelte components or controllers.
  • all user-facing text must resolve from Paraglide message keys/functions.
  • untranslated or missing values must still render via fallback chain, never as raw keys.

Override precedence (authoritative):

  • org override (most specific)
  • fallback to tenant override
  • fallback to product default (Paraglide base messages)

Implementation rule:

  • centralize lookup in one resolver utility so all surfaces (drawers, tables, toasts, forms) use identical precedence logic.

1.7 Device and Task-Tier Contract (MUST)

Evalium frontend uses task-tiering rather than one-size-fits-all device behavior.

Rules:

  • Tier A (quick tasks) must be mobile-safe.
  • Tier B (guided tasks) must be responsive and iPad-usable.
  • Tier C (full complexity) may be desktop-optimized.
  • complex actions on constrained devices must show explicit handoff actions (Continue on desktop, Send me a link, Save and exit) with no silent dead-ends.

Source policy:

  • /docs/ux/UX-MOBILE-AND-TASK-TIER-POLICY.md
  • /docs/ux/UX-ACCESSIBILITY-STANDARDS-AND-GUARDRAILS.md

2) Svelte 5 Runes: Rules of Use

2.1 $state for mutable UI + builder state

Use $state for reactive, mutable state (draft edits, selection, filters, preview seed/result).

2.2 $derived for pure “UI intent”

Use $derived for side-effect-free computed state. Examples:

  • canPublish
  • shouldShowPublishWarning
  • trafficLightCounts
  • hasBlockingErrors

Hard rule: no mutation inside derived expressions (Svelte disallows it).

2.3 $effect only for DOM-bound side effects

Use $effect for subscriptions, focus handling, timers, observers, and other mounted effects. It runs after DOM updates and does not run during SSR. Do not rely on $effect for initial render correctness.


3) Data Loading Contract (SvelteKit)

3.1 Server load must be serialisable

Anything returned from a server load must be serialisable via devalue. Never return controller instances/classes from server load.

3.2 Controller instances are created in the component

Pattern:

  • +page.server.ts (or +layout.server.ts) returns plain read models
  • +page.svelte instantiates controllers using that data

This keeps SSR/network boundaries clean while still enabling rich client behaviour.


4) Composition Convention: Snippets + Standard Shells

4.1 Use snippets as the default “layout injection”

Prefer {#snippet ...} + \{@render ...} for component composition. Legacy <slot> is supported in legacy mode but should not be used for new design-system shells.

4.2 Standard shells (design system primitives)

Build reusable shells so every domain surface shares the same interaction contracts:

  • StandardHub (Entity Hub shell)
  • StandardTable (filter/sort, row actions, empty/error states)
  • RightDrawerShell (focus trap, ESC close, return focus)
  • ExplainDrawer (Evidence Basis / Provenance)
  • ImpactSummaryModal (traffic-light propagation)

Controllers are not “bags of state”; they are state machines that expose UI intent.

5.1 Controller responsibilities

A controller owns:

  • mutable UI state ($state)
  • draft form state ($state)
  • computed intent ($derived)
  • commands (methods that trigger actions / navigation / refresh)

Components stay presentational: they render what the controller says to render.

5.2 Controller invariants

Controllers are responsible for enforcing consistency:

  • “Save quiet, Publish loud”
  • Working Draft fork rules
  • traffic-light grouping of impact
  • validation sidekick model shape
  • “stale / freshness” flags

6) Navigation: URL-as-State + DrawerCoordinator

6.1 Deep-linkable drawers are a first-class navigation channel

Drawers must:

  • be openable via URL (query params)
  • support browser Back/Forward naturally
  • not trigger full page reload

6.2 DrawerCoordinator (root layout contract)

Implement a single coordinator at the root layout that:

  • derives drawer state from $page.state.drawer (primary reactive source for shallow routing)
  • falls back to URL parsing for deep-link entry (?drawer=...)
  • opens/closes the Right Drawer reactively
  • ensures “Back closes drawer” without disrupting the underlying page

(Implementation detail: pushState/replaceState update $page.state but do not update $page.url reactively. Build next URLs from location.href, keep URL params for shareability, and treat parsed URL state as fallback.)

6.3 Drawer URL schema (convention)

Use a single param:

  • ?drawer=<type>:<id> Examples:
  • ?drawer=user:usr_123
  • ?drawer=skill:sk_456
  • ?drawer=fact:ef_789

Optional:

  • &drawerTab=activity
  • &returnTo=<encoded> for rare cases (prefer native history)

All entity references must render through a single EntityLink abstraction.

7.1 Behaviour

  • Normal click:
    • if drawer-enabled on the current surface, open a read-only context drawer (update URL param)
    • otherwise, navigate canonical full page (with breadcrumbs)
  • Permission blocked:
    • render non-link + tooltip: “No permission to view”
  • “Power user” behaviour:
    • Ctrl/Cmd-click opens a new tab to the full hub view (even if normal click would open a drawer)

Rationale: enterprise users share links; they also expect standard browser tab behaviour.

7.2 Canonical full-page URLs

Every entity that can appear in a drawer must have a canonical hub URL:

  • /questions/<id>
  • /passages/<id>
  • /evaluations/<id>
  • /assignments/<id>
  • /submissions/<id>
  • /users/<id>
  • /groups/<id>
  • /skills/<id> (feature-flagged until skills profile APIs are active) …even when normal click opens drawer peek.

8) Forms & Mutations (SvelteKit Actions First)

8.1 Use form actions for authoritative writes

Use SvelteKit actions (+page.server.ts) as the default write path. This supports progressive enhancement and keeps validation server-side.

8.2 Validation: avoid Go/Zod drift with a “Translator” contract

Because the backend is Go, treat UI schema validation as best-effort UX, not truth.

Contract: Go APIs return structured validation errors; SvelteKit actions translate them into the form state.

Recommended error shape (conceptual):

  • fieldErrors: { [fieldPath: string]: string[] }
  • formErrors: string[]
  • errorId / correlation id (optional)

Superforms can still be used for:

  • form wiring
  • client convenience validation
  • mapping server errors into the form UI
    Superforms supports schema adapters and client validation configuration.

8.3 Multiple actions per page

Prefer named actions for hub pages with multiple CTAs (Save, Publish, Archive, etc.).

8.4 Idempotency on mutating actions (MUST)

Where backend mutating routes are idempotency-protected, frontend actions must attach an Idempotency-Key.

Rules:

  • Generate one key per user intent (button click/submit), not per retry attempt.
  • Reuse the same key only for retried delivery of the same payload.
  • If payload changes, mint a new key.

8.5 Conflict/error translation contract (MUST)

Frontend must map stable backend codes to deterministic UX states.

Examples:

  • idempotency_conflict -> request changed after key issue
  • idempotency_in_progress -> operation is already running
  • question_in_use / evaluation_in_use -> switch delete flow into archive-first impact UX
  • generic conflict (for optimistic concurrency paths) -> stale-write recovery UX

8.6 Text Override Mode (Capability-Gated) (SHOULD)

Provide an operator/admin mode for runtime copy overrides without modifying product defaults.

Scope:

  • edit translated values by locale for existing message keys.
  • capability-gated (localization.manage or equivalent).
  • supported scopes: org and tenant.

Behavior:

  • draft edits with live preview.
  • publish creates a versioned override set.
  • rollback to prior override version is supported.
  • all edits are audit logged (actor, scope, locale, key, old value, new value).

Guardrails:

  • placeholders/variables must be preserved (no token breakage).
  • empty required values are rejected.
  • no executable/unsafe markup in override values.

9) Key Domain Patterns (How the Doctrine Shows Up)

9.1 authoring: Save Quiet, Publish Loud

  • Save:
    • never prompts for version numbers
    • saves drafts or working drafts quietly
  • Working Draft Fork:
    • editing a Published/Live asset forks to a Working Draft (visual mode change)
  • Publish:
    • shows Impact Summary (traffic lights)
    • explicit propagation choices
    • creates ledger trail

Publish redirect rule: redirect to canonical asset route with explicit published version context.

  • Question publish uses activeVersionId from response.
  • Evaluation publish uses the requested versionId (route param) because publish returns 204.

9.1.1 authoring bulk actions (MUST where supported)

For Question/Evaluation libraries, bulk mutations must follow:

  • Step 1: preview (dryRun)
  • Step 2: apply
  • Step 3: show per-item outcomes and aggregate summary (total/succeeded/failed/changed)

Never collapse partial success into a single generic toast.

9.1.2 Import pipeline UX (MUST where supported)

For Question/Passage/Evaluation import:

  • preview first with row diagnostics
  • commit only after explicit user confirmation
  • post-commit summary links back to filtered inventory views

9.1.3 Passage authoring is first-class (MUST)

authoring IA must include passage workflows (create/edit/link question versions), not hide passages behind question-only controls.

9.2 Validation Sidekick (Builders)

Validation must be:

  • persistent
  • actionable (“Go to item”, “Fix” where safe)
  • clearly classified:
    • blocking errors vs warnings

Include “score integrity / evidence coverage” warnings when authoring changes reduce skill evidence.

9.3 Operations: Manual Refresh First + Freshness

  • On-demand refresh always available
  • Auto-refresh optional, OFF by default
  • “Updated X ago” + “Stale” badge beyond threshold
  • proctor/override actions require mandatory reason on high-stakes paths
  • Until explicit proctor command endpoints are available, Command Centre should treat proctor controls as feature-flagged/read-only telemetry views.

Every derived metric has:

  • Explain link → evidence basis / mapping provenance / version snapshot
  • Scope chips: version, cohort/run label, timeframe
  • Processing states for delayed projections

10) Performance Rules

10.1 Patch updates, don’t replace whole lists

For tables like Command Centre:

  • keyed rows always
  • update only the affected row/cell (patch)
  • avoid full-array reassignment loops

10.2 DOM cost is still real

Even with fine-grained updates, very large tables may require virtualisation. Treat this as a “when needed” optimisation, not a default.


11) Accessibility Rules (Hard Requirements)

  • Drawer:
    • focus trap
    • ESC closes
    • focus returns to the opener
  • Keyboard-first authoring:
    • builder canvas + validation sidekick fully operable by keyboard
  • Visible focus and labelled controls for chips/badges/actions

12) Implementation “Definition of Done” Hooks

A feature is not done unless:

  • all visible entity references are clickable per EntityLink rules
  • drawers are deep-linkable and Back/Forward works via DrawerCoordinator
  • server loads return serialisable read models only
  • derived metrics have Explain links (where applicable)
  • high-stakes actions produce ledger links/receipts (where applicable)
  • loading/empty/error/processing/stale states are present and consistent
  • bulk-capable tables use dryRun preview before apply (where backend supports it)
  • usage tabs show dependency details (not counts-only) when usage graph endpoints exist
  • mutation actions use idempotency keys where required by backend contract

Appendix: Optional Tools (When They Fit)

Remote functions

Use remote functions selectively for command-like calls where forms are awkward; arguments and return values are serialised via devalue.

Legacy slots

Allowed only for legacy interop; new design-system shells use snippets/render tags.