Merge branch 'docs/design-policies-section-12-resolution' into 'main'

docs(design): DESIGN.md v1 — resolve Section 12 open items

See merge request 2-6/2-6-4/terravault/ordinis!192
This commit is contained in:
Александр Зимин
2026-05-14 15:21:58 +00:00
+187 -14
View File
@@ -327,21 +327,188 @@ because empty space looks empty.
--- ---
## 12. Open Items (gaps to address in /design-consultation sprint) ## 12. Responsive Breakpoints
- Responsive breakpoint policy (mobile 375, tablet 768, desktop 1024, Tailwind defaults — `sm=640 / md=768 / lg=1024 / xl=1280 / 2xl=1536`. Ordinis
wide 1440 — confirm + lint) is an admin tool — desktop primary. Policy:
- Touch target enforcement (P1 in TODO 6 a11y findings)
- Loading state taxonomy (skeleton vs spinner vs LoadingBlock vs Suspense - **Target primary:** `lg` (1024) and up — full layout fidelity
— when to use which) - **Tolerate:** `sm``md` (640-1023) — degraded but functional (sidebar
- Motion duration policy (currently ad-hoc — set 150ms quick / 300ms medium collapses to hamburger; tables horizontally scroll; tabs may wrap)
/ 500ms entrance) - **Minimum viable:** 375px (iPhone SE narrow). No layout breaks; some
- Long-content overflow rules (truncate vs wrap vs ellipsis vs scroll — density compromises accepted.
per surface type) - **Avoid breakpoint ping-pong:** don't redefine layout at every step.
- Dark mode parity audit (some tokens have alpha inconsistency between Standardize page padding: `px-4 sm:px-6 lg:px-8`. Big shifts only at `lg`
themes) (sidebar collapse) and `xl` (max-width caps).
- Notifications channel UI policy (in-app vs email vs both — currently
every event goes to all enabled channels; should be user-configurable) Explicit non-goal: native mobile-app feel. Users on phones are doing
inspection / approval triage, not maker-flow data entry. See Section 19
disclosure.
---
## 13. Touch Targets
WCAG AAA recommends 44×44 minimum. Ordinis policy:
- **All interactive elements** (`button`, `a` with `href`, `[role=button]`,
inputs) — `min-h-[44px]` enforced.
- **Icon-only buttons** (Bell, theme switch, language switch, hamburger) —
`size-11` (44×44) wrapper instead of current `size-7`. Visual icon stays
small; hit area is the padded wrapper.
- **Tabs** — `h-11` instead of `h-10`. Lose 4px header height, gain WCAG
compliance.
- **Sidebar nav rows** — full row click area (already correct);
`py-2.5` ensures ≥ 44px tall.
- **Table action links** ("Open →") — wrap in `<Button variant="ghost" size="sm">`
which already has 44px hit area, despite small visual text.
- **Exception:** icons inside larger clickable containers (e.g. unread dot
inside a notification row that's clickable as a whole) — hit area belongs
to parent, not the icon itself.
Enforcement: a follow-up MR sweeps current 2832px buttons → 44px wrappers.
Tracked in TODOS.md TODO 6 punch list.
---
## 14. Loading State Taxonomy
| Surface | Pattern | Rationale |
|---|---|---|
| Inline cell / quick value | `<LoadingBlock size="sm">` | Local replacement, no layout shift |
| Panel / table / drawer body | `<LoadingBlock size="md">` | Replaces whole section, matches Panel padding |
| Whole route mount | Skeleton matching real shape (TanStack Router pending state) | First paint should hint at content layout |
| Suspense fallback | `<LoadingBlock size="md">` | Same as panel — Suspense boundaries wrap panels |
| Inflight mutation | Button `loading` prop (replaces label with spinner) | Local feedback at the trigger |
| Optimistic updates | No loading affordance — use `useOptimistic` | Most modern pattern |
Never use a bare `<Spinner>` outside `<Button loading>`. Spinners detached
from context are anxiety-inducing — replace with a skeleton or LoadingBlock
sized to the missing content.
---
## 15. Motion Durations
Four-step scale. Implemented via Tailwind utility `duration-{step}` mapped
to CSS custom properties so the same scale applies to bespoke transitions.
| Step | Range | Use |
|---|---|---|
| `micro` | 50100ms | Hover, focus rings, toggle thumb, button press |
| `short` | 150250ms | Dropdown / popover open, success/error flash on form input, badge appearance |
| `medium` | 300400ms | Drawer slide-in, modal fade, ReviewDrawer decision flash, tab content fade |
| `long` | 500700ms | Route entrance choreography, hero reveals, complex sequence |
### Easing
- **Entrance** (hidden → visible, off-screen → on-screen): `ease-out` —
start fast, settle in
- **Exit** (visible → hidden): `ease-in` — start gentle, accelerate away
- **Move** (element relocates / resizes in-place): `ease-in-out`
- **Linear**: only for indeterminate loops (spinner rotation, progress
bar bounce)
### Rules
- Animate `transform` and `opacity` only — never width/height/top/left
(triggers layout reflow on every frame)
- `transition: all` is banned — list properties explicitly
- Respect `prefers-reduced-motion`: replace entrance animation with instant
state swap; spinner becomes pulsing dot
- No animation > 700ms without user-controlled trigger (clicking expand, etc.)
---
## 16. Long-Content Overflow Rules
| Surface | Pattern |
|---|---|
| Table cells (string columns) | `truncate` + native `title={fullValue}` tooltip |
| Drawer / Modal title | `truncate` (single line, no wrap) |
| Body description text | Wrap naturally; `max-w-prose` on wide screens |
| Code blocks, JSON diff panes | `overflow-x-auto whitespace-pre font-mono` |
| Comments (makerComment / reviewComment) | `line-clamp-2` on mobile, full on `md+` |
| Notification preview text | `line-clamp-2` always (drawer list is dense) |
| Search results / lists | Wrap; `break-words` for long URLs/identifiers |
| Tab labels | Never truncate — if label is long, label is wrong |
Tooltip + truncation rule: whenever applying `truncate`, ALWAYS pair with
native `title` attribute (or `<Tooltip>` primitive for rich content). Don't
let truncation hide information without a recovery path.
---
## 17. Dark Mode Parity
Audited 2026-05-14 — found `*-bg` alpha tokens differ between themes (11%
light vs 16% dark). Drift from independent token tweaks, not intentional.
### Policy
- **Surface elevation** via opacity / shadow, NOT lightness inversion. Light
→ dark = different palette, not flipped lightness. Already implemented;
keep.
- **Accent saturation:** reduce 1020% in dark mode to avoid vibration on
dark surface. Already implemented (`--color-accent: #b05a2e` light vs
`#d97757` dark which is brighter but compensated for darker bg).
- **`*-bg` alpha consistency:** normalize to 12% in both themes via new
`--alpha-bg-tint` token. One-time MR. Visual diff acceptable (slight
saturation shift on info badges).
- **Text on accent (`--color-on-accent`):** verified ≥ 4.5:1 ratio both
themes. Light = white (#fff) on `#b05a2e` ≈ 4.8:1 OK. Dark = `#1c1a17`
on `#d97757` ≈ 7.2:1 OK.
- **`text-mute` on `bg-surface`:** light ≈ 4.6:1 (just passes), dark ≈ 4.8:1.
Borderline — re-verify after Section 13 button rework.
---
## 18. Notifications Channel UX
Phase A (current, MR !190): every fired event dispatches to all enabled
channels at backend level. EmailChannel is the only Phase A channel.
Reviewer-pool address hardcoded in `application.yml`. Bell + drawer + toast
already shipped on frontend.
### Phase B (deferred — TODO follow-up)
User-configurable preferences via new route `/me/notifications/preferences`.
Surface design when shipped:
- Table: rows = event types, columns = channels (Email / Express / Telegram /
In-App). Toggle per cell.
- Save button bottom-right; optimistic update.
- "Mute all" + "Reset to defaults" quick actions in header.
- Defaults: all on; per-user state in `user_notification_prefs` table
(migration 0021 already has it).
Anti-pattern: don't bury preferences inside profile settings. Notifications
preferences live one click from the bell — make the bell drawer footer have
a settings cog.
### Future channel ordering (Phase C, "Don't Disturb")
Per-channel priority for DND mode. Email always lowest (durable). Express/Slack
mid. Telegram highest urgency. Bell + in-app toast always on.
---
## 19. Disclosure: Known Tradeoffs
This admin UI **deliberately deprioritizes mobile-first design**. Affordances:
- Native touch targets bigger than 44px not enforced on data-cell click
areas (would explode row heights). Cells with `<UserCell>` or row-level
click handlers rely on the parent row hit area.
- Tables don't reformat into card-stacks on narrow screens — they scroll
horizontally.
- Forms don't shift to one-column layouts under 768px — they compress.
- Server-side rendering not used; client-only SPA means full-app JS bundle
required for any page.
Users on phones doing emergency approvals are supported. Users doing
maker-flow data entry on phones are not. Don't ship to a context where the
latter matters without prior architecture work.
--- ---
@@ -351,3 +518,9 @@ because empty space looks empty.
semantics, term glossary, drawer-vs-modal rules from approval workflow semantics, term glossary, drawer-vs-modal rules from approval workflow
saga codebase. Marked open items для full `/design-consultation` saga codebase. Marked open items для full `/design-consultation`
sprint. Authored as part of TODO 5 closeout. sprint. Authored as part of TODO 5 closeout.
- **2026-05-14 v1** — Section 12 (open items) resolved via `/design-consultation`.
Added Sections 1219 with concrete policies: responsive breakpoints, touch
targets, loading taxonomy, motion durations, overflow rules, dark mode
parity, notifications channel UX, known-tradeoffs disclosure. Touch target
follow-up MR pending; dark mode alpha normalization MR pending; Phase B
notifications prefs TODO.