# 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`** — `

{app_name}

` → `{app_name}` (`PageTitle` is `text-2xl font-semibold tracking-tight`, restoring the missing `tracking-tight`). Import `PageTitle` from `@/components/ui/page-title`. - **`fields/field-list.tsx:97`** — the hand-rolled type-tag `` → `` (from `@/components/ui/badge`). - **Icon sizing** — `h-4 w-4` → `size-4` in the 3 **app-source** sites: `shell/theme-switch.tsx:39`, `shell/user-menu.tsx:27`, `shell/header-search.tsx:23`. (Leave `components/ui/select.tsx` — kit-internal.) - **Icon dismiss buttons** → kit `Button variant="ghost" size="icon-sm"`: - `objects/objects-page.tsx:54` (plain `` (import `Button`). - `objects/object-detail-drawer.tsx:33` (Base UI ``) → keep `DrawerClose` for its close-on-click semantics, render it AS the kit Button via the render prop: `}>` (mirrors the `AlertDialogTrigger render={