Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

06 — Screen Specifications (Settings Module)

Production-grade specs for each Settings screen: chrome, states, components, micro-interactions, a11y, responsive, dark/light, keyboard, motion. Tokens: Sp = 00-shared/02 §3, Ty = typography §2, El = elevation §5, R = radius §4, Mo = motion §6. All components reference 00-shared/03.


S1 — Settings List /settings

Chrome: standard shell AppBar (Icons.settings, headlineMedium "Settings"); horizontal padding 16 (phone) / 24 (tablet+); content in ListView.builder (never Column of cards — keys can number in the hundreds).

Layout (phone):

AppBar: "Settings"                    headlineMedium
AppSearchBar (client-side)            h 56, debounce 300 ms
Group chips (horizontal scroll)       FilterChip per enum value + "All"
Group section header (first group)    titleMedium "Attendance (12)"
SettingRow (key / value / type chip)  AppCard flat, 56–72 h
SettingRow ...
[FAB extended: "New setting"]         Icons.add

Layout (desktop ≥840 dp): master-detail — left pane list (max width 480), right pane = detail S2 for the selected row; selection persists in SettingsCubit.

States:

  • loading → AppSkeleton(list) (≤200 ms target).
  • success+empty → AppEmptyState Icons.tune "No settings yet — add your first key."
  • group-empty → inline bodySmall "Nothing in {group} yet."
  • error → AppErrorState(code) (401 → refresh flow; 5xx → generic + requestId + Retry).
  • offline → AppOfflineBanner + last-good rows; editors disabled with tooltip.
  • permission → route guard to /settings/403 (no settings.read).

Group chips: server-filtered (GET /settings?group=) — tapping a chip triggers a server refetch (not client filter), because the contract filters server-side (settings.controller.ts:26-28). The search box filters the current fetch client-side (no q param exists — 00-shared/07 §5 applies only where controllers support it).

Row (SettingRow):

ElementSpec
Leadinggroup icon map (school/event_available/grading/notifications/palette/settings)
Titlekey in mono titleMedium (FontFeature.tabularFigures() for numeric keys)
Subtitlevalue preview, truncated 1 line: strings quoted, numbers plain, booleans as true/false, objects {…} + count, arrays [… n]
Trailingtype chip (AppBadge: STR / NUM / BOOL / JSON), AppMenu (Edit, Duplicate key, Delete)
Tap→ detail pane/page
Dirty markerwhen edited in place: left primary indicator dot + row tinted primaryContainer

Batch mode: long-press enters selection; AppBottomSheet fixed bottom bar "Save all (N)" — only dirty keys serialized (03 §3). Per-row saved state clears the dot (Mo.m-fast fade).

Motion: list entrance m-entrance stagger 40 ms; chip indicator slide m-base; FAB hide on scroll-down, show on scroll-up (00-shared/03 D).

A11y: search Semantics(liveRegion) for result count ("12 of 48"); chips announce selected state; rows are single Semantics(button) with key label; dirty state announced via Semantics(label: "unsaved").

Dark/light: token-only; dirty tint primaryContainer on dark = elevated contrast.


S2 — Setting Detail / Typed Editors (module core)

Chrome: detail pane (desktop) or pushed page (phone). Header: key (mono titleLarge), group dropdown, isEncrypted badge ((planned)), meta footer (bodySmall: "created {date} · v{version}" — base.schema.ts:30-34; createdBy/updatedBy when present).

Editor by value type (client inference, 05 §3)

TypeEditorSave behaviorValidation
stringAppTextField, single line, mono for likely IDs/URLsexplicit Save button (or Enter)non-empty unless empty allowed by key; no server rule (update-setting.dto.ts:5-10)
numberAppTextField keyboard numberWithOptions(decimal: true), formatter ^\d*\.?\d*$explicit Saveparseable float/int; saved as JSON number (no string coercion)
booleanAppSwitch rowinstant save on toggle (safe mutation — 00-shared/06 §3.5); server-confirm then reflect; rollback + snackbar on errorn/a
object / array / nullAppJsonEditor (mono multi-line, min 6 rows, grow)explicit Save (disabled while invalid)live JSON parse; save blocked on parse error with inline message
unknown typefall back to JSON editorsame as JSON

Save semantics (from source):

  • Full replacement of value: $set: {value} (setting.repository.ts:37) — never merge.
  • group is sent with the save and persisted if present (setting.repository.ts:37); omitting it keeps the existing group (only value+group are ever written — the DTO's label/description are dropped by settings.service.ts:25).
  • Upsert idempotent ({upsert:true, new:true}); server returns the saved doc → detail reconciles to server truth.
  • No optimistic write for explicit-save editors; boolean toggle is optimistic with rollback (00-shared/06 §3.5).

Validation errors (client-first, server shadows):

FieldClient ruleServerUI
key (create/rename)non-empty trimmed stringno decorators — missing key → Mongoose required → 500 (OQ-3)required field error
group∈ enum@IsEnum(SettingGroup) (400 VALIDATION_ERROR)dropdown restricts options
valueany JSONvalue: unknown — any JSON acceptedtyped editors
label / description@IsString() optionalhidden (unused by service)

Save button: loading spinner replaces label (Mo.m-base); disabled while pending (anti-double-submit 00-shared/08 §6); on success AppSnackbar "Saved" (<3 s) + HapticFeedback.lightImpact().

Error surfaces: 400 → inline field errors (from details[], http-exception.filter.ts:103-108); 5xx → snackbar generic + requestId, form kept (retry safe — upsert idempotent).


S3 — New Setting (FAB sheet)

AppBottomSheet (full height on phone, centered card ≤520 on desktop):

  • key field (required, autofocus, mono hint "e.g. attendance.lateThreshold").
  • group AppDropdown (enum, default GENERAL — setting.schema.ts:24-25).
  • value: type unknown at creation → JSON editor (accepts string/number/bool/object); after first save the row gains its concrete type chip.
  • CTA "Create" → PUT /settings; success → snackbar + list refetch; 500 duplicate-key (recreate-after-delete, OQ-5) → banner explaining the key is soft-deleted.

S4 — Delete flow

AppDialog (destructive): title "Delete {key}?"; body explains soft-delete + that the key cannot be re-created today (OQ-5 warning). Confirm → DELETE /settings/:key (settings.controller.ts:49-53); loading on confirm; success → row fade-out (Mo.m-base) + snackbar; 404 → row removed silently.


AppBanner(info): "Attendance rules, theme and working days also live under Organization" → /organization (embedded settings surface, organization.schema.ts:96-112). One-time dismiss persisted in shared_preferences. Rationale: dual-surface confusion is a known risk (design-docs/organizations/09 §8 — "Which settings apply?").


Shared micro-interactions & motion (all screens)

  • Route transitions Mo.m-base; AppBar El-1; cards El-e1; sheets/dialogs e-4/e-5.
  • Snackbar durations: success < 3 s, error ≥ 4 s (00-shared/03 A).
  • Keyboard: Enter saves editors, Esc closes sheet/confirm, Ctrl+F search (desktop), Ctrl/Cmd+Enter submits JSON editor (00-shared/08 §2).
  • Safe areas: bottom Save all bar respects nav bar height on phone.
  • Reduced motion: no stagger, no shake — fades/instants only (00-shared/08 §4).