diff --git a/docs/superpowers/specs/2026-06-08-design-kit-consistency-design.md b/docs/superpowers/specs/2026-06-08-design-kit-consistency-design.md new file mode 100644 index 0000000..6ca201a --- /dev/null +++ b/docs/superpowers/specs/2026-06-08-design-kit-consistency-design.md @@ -0,0 +1,122 @@ +# Design-Kit Consistency — Design + +**Date:** 2026-06-08 +**Status:** Approved (brainstorming) — ready for implementation planning. +**Issue:** #66 (dead Card, duplicated segmented-control + selected-row recipes, `useLang`/`focusRing` drift, misc kit one-offs). + +## Context + +A frontend deep audit found subtler design-system inconsistencies the `check:colors` guard doesn't catch +(the app is already hex-free + token-based + dark-mode-clean). These are duplicated class recipes and +non-adoption of the `ui/*` kit. All fixes are behavior-preserving; `check:colors`/`check:size`/the existing +component tests are the guards. State re-verified against the current code (post #62/#64). + +## Components + +### New shared helpers + +**`lib/use-lang.ts`** — `useLang(): "sv" | "en"` +```ts +import { useTranslation } from "react-i18next"; + +/** The instance's active UI language, narrowed to the two supported locales. */ +export function useLang(): "sv" | "en" { + const { i18n } = useTranslation(); + return i18n.language.startsWith("sv") ? "sv" : "en"; +} +``` +Replaces the inline `const lang = i18n.language.startsWith("sv") ? "sv" : "en";` in **6** components: +`objects/object-detail.tsx`, `objects/field-input.tsx`, `vocab/vocabulary-terms.tsx`, +`vocab/vocabulary-list.tsx`, `fields/field-list.tsx`, `authorities/authorities-page.tsx`. Each switches to +`const lang = useLang();` and drops the now-unused `i18n` from its `useTranslation()` destructure where +`i18n` is otherwise unused. (Left untouched: `shell/lang-switch.tsx` — derives from a different `locale` +var; `i18n/index.ts` — the infra `languageChanged` handler.) + +**`lib/class-recipes.ts`** — two shared class helpers +```ts +import { cn } from "@/lib/utils"; +import { focusRing } from "./focus-ring"; + +/** Segmented-control / filter-pill item. Unifies the active/inactive token recipe + + * focus ring; callers pass their contextual padding/size via `className`. */ +export function segmentClass(active: boolean, className?: string): string { + return cn("rounded-md", focusRing, active ? "bg-primary text-primary-foreground" : "border", className); +} + +/** Selected vs idle row background for master-detail / list rows. */ +export function rowStateClass(active: boolean): string { + return active ? "bg-primary/10" : "hover:bg-muted"; +} +``` +- **`segmentClass`** is adopted at the 3 segmented sites, each keeping its contextual padding: + - `objects/objects-table.tsx:174` → `segmentClass(active, "px-2 py-1")` + - `search/search-panel.tsx:76` → `segmentClass(active, "px-2 py-0.5")` + - `authorities/authorities-page.tsx:41` (NavLink) → `segmentClass(isActive, "px-3 py-1 text-sm")` + This DRYs the bug-prone recipe (the active/inactive token pair + `focusRing` — the part that drifted and + caused the #62 missing-ring bug); contextual sizing is intentionally preserved per site. +- **`rowStateClass`** is adopted at the 4 selected-row sites: + - `objects/objects-table.tsx:252` → `rowStateClass(selected)` + - `vocab/vocabulary-list.tsx:113` → `rowStateClass(isActive)` + - `search/search-result-row.tsx:15` → `rowStateClass(isActive)` + - `fields/field-list.tsx:86` → `rowStateClass(def.key === selectedKey)` — **fixes** this site, which + currently uses `… ? "bg-primary/10" : ""` (dropping the `hover:bg-muted` idle hover the others have). + +### One-off cleanups +- **Delete `components/ui/card.tsx`** — zero importers (no app/test/story references; no `card.stories`). +- **`shell/sidebar.tsx:46,88`** — replace the raw `focus-visible:ring-3 focus-visible:ring-ring/50` + string (inside the existing `cn(...)`) with the imported `focusRing` constant (adds `outline-none`, + matching every other call site). +- **`auth/login-page.tsx:49`** — `