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.