From ac4f91de8797715676d79bd0f78e1051299280e4 Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Fri, 12 Jun 2026 23:30:17 +0900 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20W13=20=E2=80=94=20adopt=20the?= =?UTF-8?q?=20Google=20AI=20Studio=20light=20theme=20globally=20(TRUSCA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promotes the PR #394 dev prototype to the default theme after user review (full adoption including pill buttons; risk-low colour separated): - index.css: :root tokens move to the AIS light palette — white canvas, #0b57d0 Google-blue primary, #d3e3fd tonal secondary, #dadce0 borders, #f8f9fa muted, radius 8px (derived scale 6/8/10/14), shadow-sm now a zero-alpha no-op (flat cards), md/lg follow Google elevation-1/-2. Motion, layout density, and typography tokens unchanged. - risk-low: #2563eb (blue-600) -> #0f766e (teal-700) so Low badges no longer share a hue with the new blue primary. 5.47:1 on white and 4.76:1 on its own tint (both AA, better than the old blue). Badge low text follows the hue family: text-blue-700 -> text-teal-800 (6.59:1). - button.tsx: rounded-full pills on every variant/size (AIS silhouette); per-size rounded-md remnants removed so twMerge doesn't undo the pill. - AppShell: sidebar nav items become pills to match. - Prototype apparatus deleted (theme-ais.css, devTheme.ts, AisThemeToggle, its unit test, main.tsx DEV branch) - the tokens now live in :root. - /dev/design-preview re-titled to the W13 skin; side-by-side prototype section removed; radius labels updated; gallery test follows. - design-system.md (EN/KO): W13 section + token/contrast/radius/shadow tables updated; W11 kept as collapsed history. ko-style lint clean. Visual-regression baselines intentionally NOT updated here - they must be regenerated on a linux runner (visual-regression.yml --update-snapshots path) after merge; the nightly gate will flag until then. --- apps/frontend/src/components/AppShell.tsx | 14 +- apps/frontend/src/components/ui/badge.tsx | 8 +- apps/frontend/src/components/ui/button.tsx | 33 ++-- apps/frontend/src/index.css | 147 ++++++++++-------- apps/frontend/src/lib/devTheme.ts | 48 ------ apps/frontend/src/main.tsx | 8 - .../frontend/src/pages/dev/AisThemeToggle.tsx | 33 ---- .../src/pages/dev/DesignSystemPreview.tsx | 101 ++---------- apps/frontend/src/styles/theme-ais.css | 101 ------------ apps/frontend/tailwind.config.ts | 13 +- apps/frontend/tests/unit/devTheme.test.ts | 64 -------- .../pages/dev/DesignSystemPreview.test.tsx | 13 +- docs-site/docs/reference/design-system.md | 138 ++++++++-------- .../current/reference/design-system.md | 138 ++++++++-------- 14 files changed, 291 insertions(+), 568 deletions(-) delete mode 100644 apps/frontend/src/lib/devTheme.ts delete mode 100644 apps/frontend/src/pages/dev/AisThemeToggle.tsx delete mode 100644 apps/frontend/src/styles/theme-ais.css delete mode 100644 apps/frontend/tests/unit/devTheme.test.ts diff --git a/apps/frontend/src/components/AppShell.tsx b/apps/frontend/src/components/AppShell.tsx index 419c081a..6bc61f4d 100644 --- a/apps/frontend/src/components/AppShell.tsx +++ b/apps/frontend/src/components/AppShell.tsx @@ -34,7 +34,6 @@ import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; import { deriveInitials } from "@/lib/initials"; import { cn } from "@/lib/utils"; -import { AisThemeToggle } from "@/pages/dev/AisThemeToggle"; import { useAuthStore } from "@/stores/authStore"; import { useUIStore } from "@/stores/uiStore"; @@ -161,10 +160,11 @@ function NavItemLink({ title={collapsed ? label : undefined} className={({ isActive }) => cn( - // W11-F polish — sidebar nav hover/active transitions land on the - // W11-A 150 ms ease-out-soft tokens for parity with every other - // hoverable affordance (buttons, dropdown items, tabs). - "flex items-center rounded-md py-2 text-sm font-medium transition-colors duration-fast ease-out-soft", + // W13 — pill nav items match the AIS button shape; the active + // bg-primary/10 + text-primary tint then reads exactly like the + // AIS selected-nav state. Hover/active transitions stay on the + // W11 150 ms ease-out-soft tokens. + "flex items-center rounded-full py-2 text-sm font-medium transition-colors duration-fast ease-out-soft", collapsed ? "justify-center px-2" : "gap-2 px-3", "hover:bg-accent hover:text-accent-foreground", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", @@ -384,10 +384,6 @@ export function AppShell() {
- {/* Dev-only AIS theme prototype switch — lets the design review - happen on real screens (dashboard, project list), not just the - /dev/design-preview gallery. Compiled out of prod builds. */} - {import.meta.env.DEV ? : null} {/* Global ⌘K palette trigger (W9-#54). The button is a discoverability affordance — the keyboard shortcut works whether or not this button is on screen. */} diff --git a/apps/frontend/src/components/ui/badge.tsx b/apps/frontend/src/components/ui/badge.tsx index c6496cb2..86ad8336 100644 --- a/apps/frontend/src/components/ui/badge.tsx +++ b/apps/frontend/src/components/ui/badge.tsx @@ -49,17 +49,17 @@ const badgeVariants = cva( // critical text-red-700 → 5.54:1 // high text-orange-800 → 6.47:1 // medium text-yellow-800 → 5.91:1 - // low text-blue-700 → 5.83:1 + // low text-teal-800 → 6.59:1 (W13 — token moved to + // teal-700, text follows the hue) // info text-slate-600 → 6.41:1 // // The dot indicators (SeverityBadge / DependencyScopeBadge / chart // legends) still use `bg-risk-X` raw so the brand colour stays - // recognisable — only the text shade is darkened. Token values in - // `index.css` are NOT changed (W11 prohibition: "Severity 색 변경 0"). + // recognisable — only the text shade is darkened. critical: "border-transparent bg-risk-critical/10 text-red-700", high: "border-transparent bg-risk-high/10 text-orange-800", medium: "border-transparent bg-risk-medium/15 text-yellow-800", - low: "border-transparent bg-risk-low/10 text-blue-700", + low: "border-transparent bg-risk-low/10 text-teal-800", info: "border-transparent bg-risk-info/15 text-slate-600", success: "border-transparent bg-emerald-100 text-emerald-700", }, diff --git a/apps/frontend/src/components/ui/button.tsx b/apps/frontend/src/components/ui/button.tsx index eea5203e..03169f27 100644 --- a/apps/frontend/src/components/ui/button.tsx +++ b/apps/frontend/src/components/ui/button.tsx @@ -5,25 +5,26 @@ import { forwardRef, type ButtonHTMLAttributes } from "react"; import { cn } from "@/lib/utils"; /** - * Button — W11-A polish. + * Button — W13 AIS adoption (pill shape) on top of the W11-A polish. * - * Changes vs. previous baseline: - * - `transition-colors` → `transition-all duration-fast ease-out-soft` so - * hover/focus is a 150 ms eased transition (Linear polish) covering - * background AND shadow. - * - Focus ring kept at `ring-2` + `ring-offset-2` (matches new --ring token - * which now lines up with --primary for a coherent focus signal). - * - Default variant picks up `shadow-sm` so the primary CTA reads as a - * raised affordance against the off-white page background (Vercel - * deployments-1 "Visit" button reference). - * - Outline variant gets `shadow-sm` too — light elevation, no border - * contrast bump needed. + * - `rounded-full` pills are the Google AI Studio button shape. This is + * deliberately NOT driven by the --radius token — raising the token to + * a pill value would full-round cards/dialogs via the tailwind.config + * calc() derivations. Inputs keep the token radius on purpose. + * - `transition-all duration-fast ease-out-soft` — 150 ms eased hover / + * focus transition (W11 Linear polish, retained). + * - Focus ring `ring-2` + `ring-offset-2` (matches --ring, now the blue + * primary, for a coherent focus signal). + * - shadow-sm on default/destructive/outline resolves to a zero-alpha + * no-op under W13 tokens (AIS keeps in-flow surfaces flat); kept in + * the class lists so a future token change re-enables elevation + * without touching this file. * * The hex colors themselves are NOT in this file — they flow from the - * tokens declared in `src/index.css` (W11-A). + * tokens declared in `src/index.css` (W13). */ const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all duration-fast ease-out-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium ring-offset-background transition-all duration-fast ease-out-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { @@ -40,8 +41,8 @@ const buttonVariants = cva( }, size: { default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-6", + sm: "h-8 px-3 text-xs", + lg: "h-10 px-6", icon: "h-9 w-9", }, }, diff --git a/apps/frontend/src/index.css b/apps/frontend/src/index.css index 5de542c0..6b744271 100644 --- a/apps/frontend/src/index.css +++ b/apps/frontend/src/index.css @@ -3,26 +3,28 @@ @tailwind utilities; /* --------------------------------------------------------------------------- - * TrustedOSS Portal — design tokens + * TRUSCA — design tokens * - * W11-A (2026-05-27) — Vercel base + Linear polish (option C, light single). - * SoT: docs/ux/design-philosophy-evolution-plan-2026-05-27.md §4. + * W13 (2026-06-12) — Google AI Studio light tone, adopted from the dev + * prototype (PR #394) after user review. Supersedes the W11-A Vercel + + * Linear palette; the W11 typography hierarchy, focus ring, and motion + * polish are retained unchanged. * - * Tone reference: - * - Vercel light deployments / domains / deployment detail - * → background / surface / border / sidebar tint / dense rows / primary - * near-black button - * - Linear feature / dashboard - * → typography hierarchy (heading semibold), focus ring (visible 2 + 2 - * offset), hover transitions (150 ms ease-out), elevation shadows, - * dropdown polish. Dark variant intentionally NOT absorbed — Linear - * is dark-native, we re-interpret only the polish layer in light. + * Tone reference — Google AI Studio (light): + * - plain white canvas, grey panel tint, #dadce0 hairline borders + * - Google-blue primary CTA + tonal (light-blue) secondary + * - pill buttons (see button.tsx) and flat cards (border + tone, no + * shadow); shadows are reserved for floating surfaces + * - fonts stay Inter + JetBrains Mono (Google Sans is proprietary) * * Layout invariants (unchanged): * sidebar 224 px · header 48 px · table row 40 px · Inter + JetBrains Mono. + * Density is kept — only the skin changed. * - * Risk severity colors (unchanged — domain semantics fixed): - * Critical / High / Medium / Low / Info. + * Risk severity colors: + * Critical / High / Medium / Info unchanged. Low moved blue-600 → teal-700 + * in W13 (user decision): the old blue collided with the new Google-blue + * primary, making Low badges read like CTAs / links. * * Forward-compat note: * Dark mode tokens are NOT defined in this phase (2026-05-27 user decision). @@ -34,79 +36,83 @@ @layer base { :root { /* ------------------------------------------------------------------- - * shadcn/ui base tokens — re-tuned to Vercel light palette. + * shadcn/ui base tokens — Google AI Studio light palette (W13). * * HSL is shadcn convention. Hex equivalents are noted for review; * never reference the hex directly in components (use the CSS var or * the Tailwind utility — e.g. `bg-background`, `text-foreground`). * ----------------------------------------------------------------- */ - /* Page background — slight off-white so elevated surfaces (cards, - * popovers) read as raised. Vercel uses ~#fafafa for the canvas. */ - --background: 0 0% 98%; /* #fafafa */ - --foreground: 240 6% 10%; /* #18181b — warm near-black (was #0f172a navy) */ + /* Page background — plain white canvas (AIS). Cards sit flush on it + * and separate via border + tone, not elevation. */ + --background: 0 0% 100%; /* #ffffff */ + --foreground: 0 0% 12%; /* #1f1f1f — Google grey-900 */ - /* Elevated surfaces sit on top of --background. Pure white reads as - * "card on page" against the off-white canvas. */ + /* Surfaces share the white canvas; --muted carries the grey panel / + * table-header tint instead. */ --card: 0 0% 100%; /* #ffffff */ - --card-foreground: 240 6% 10%; + --card-foreground: 0 0% 12%; --popover: 0 0% 100%; - --popover-foreground: 240 6% 10%; + --popover-foreground: 0 0% 12%; /* Muted = table header / sidebar tint / placeholder fill. */ - --muted: 240 5% 96%; /* #f4f4f5 */ - --muted-foreground: 240 4% 46%; /* #71717a — passes AA on --muted */ - - /* Border / input outline — softer than slate-200, more neutral. */ - --border: 240 5% 91%; /* #e5e5ea */ - --input: 240 5% 91%; - - /* Primary CTA — Vercel-style warm near-black instead of navy. - * Reads as "important action" without leaning blue. */ - --primary: 240 6% 10%; /* #18181b */ - --primary-foreground: 0 0% 98%; - - /* Secondary / accent share the muted tint — used for ghost-style - * surfaces and hover row backgrounds. */ - --secondary: 240 5% 96%; - --secondary-foreground: 240 6% 10%; - --accent: 240 5% 96%; - --accent-foreground: 240 6% 10%; - - /* Destructive — aligned with risk-critical hex so destructive - * buttons share the visual language of a Critical severity badge. */ - --destructive: 0 72% 51%; /* ~#dc2626 */ - --destructive-foreground: 0 0% 98%; - - /* Focus ring — matches primary tone so the 2 px outline reads as - * "the same thing this button is", per Linear polish. */ - --ring: 240 6% 10%; + --muted: 210 17% 98%; /* #f8f9fa */ + --muted-foreground: 213 5% 39%; /* #5f6368 — Google grey-700, passes AA */ + + /* Border / input outline — the AIS standard hairline. */ + --border: 220 9% 87%; /* #dadce0 */ + --input: 220 9% 87%; + + /* Primary CTA — Google blue. Hover flows through `bg-primary/90`. */ + --primary: 217 90% 43%; /* #0b57d0 */ + --primary-foreground: 0 0% 100%; + + /* Secondary = Google "tonal" button (light-blue fill, deep-blue text); + * accent = the faint blue-grey tint AIS uses on hover rows and ghost + * surfaces. */ + --secondary: 217 91% 91%; /* #d3e3fd */ + --secondary-foreground: 217 90% 15%; /* #041e49 */ + --accent: 213 43% 96%; /* #f0f4f9 */ + --accent-foreground: 0 0% 12%; + + /* Destructive — Google red. Near risk-critical (#dc2626) so destructive + * buttons still share the visual language of a Critical badge. */ + --destructive: 4 71% 50%; /* #d93025 */ + --destructive-foreground: 0 0% 100%; + + /* Focus ring — matches the blue primary so the 2 px outline reads as + * "the same thing this button is". */ + --ring: 217 90% 43%; /* ------------------------------------------------------------------- - * Radius hierarchy (Linear polish — different sizes for different - * affordances so depth reads at a glance). + * Radius hierarchy (W13 — one step rounder than W11 across the board). * - * --radius-sm 4 px — small inputs, badges, chips - * --radius 6 px — buttons, cards, table chrome (DEFAULT) - * --radius-lg 8 px — drawer, large surfaces - * --radius-xl 12 px — modals, dialogs + * --radius-sm 6 px — small inputs, badges, chips + * --radius 8 px — cards, inputs, table chrome (DEFAULT) + * --radius-lg 10 px — drawer, large surfaces + * --radius-xl 14 px — modals, dialogs * - * shadcn convention reads `--radius` as the card default; smaller / - * larger variants derive via `calc()` in tailwind.config.ts. + * Buttons are pills (rounded-full in button.tsx), NOT derived from + * this token — raising --radius to a pill value would full-round + * cards and dialogs via the calc() derivations in tailwind.config.ts. * ----------------------------------------------------------------- */ - --radius: 0.375rem; + --radius: 0.5rem; /* ------------------------------------------------------------------- - * Elevation — subtle, Vercel-style. Light shadows only; no glow. - * Used by Card (sm), Dropdown / Popover (md), Drawer / Dialog (lg). + * Elevation — AIS keeps in-flow surfaces flat (cards/buttons separate + * via border + tone), so shadow-sm is a zero-alpha no-op. It must stay + * a VALID box-shadow value (consumers do `box-shadow: var(--shadow-sm)` + * and an empty string / `none` inside a list breaks them). md / lg + * follow the Google elevation-1 / -2 recipes for floating surfaces + * (dropdown / popover / drawer / dialog). * ----------------------------------------------------------------- */ - --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.04); + --shadow-sm: 0 0 0 0 rgb(0 0 0 / 0); --shadow-md: - 0 2px 8px -2px rgb(0 0 0 / 0.08), - 0 1px 2px 0 rgb(0 0 0 / 0.04); + 0 1px 2px 0 rgb(60 64 67 / 0.3), + 0 1px 3px 1px rgb(60 64 67 / 0.15); --shadow-lg: - 0 10px 28px -8px rgb(0 0 0 / 0.12), - 0 3px 8px -3px rgb(0 0 0 / 0.06); + 0 1px 3px 0 rgb(60 64 67 / 0.3), + 0 4px 8px 3px rgb(60 64 67 / 0.15); /* ------------------------------------------------------------------- * Motion (Linear polish) — short, ease-out. Three steps cover the @@ -122,14 +128,19 @@ --ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* ------------------------------------------------------------------- - * Risk severity (unchanged — domain meaning is fixed). + * Risk severity — domain meaning is fixed. * Used directly by recharts / lucide as raw hex, also exposed as * Tailwind `text-risk-critical` / `bg-risk-high/10` etc. + * + * W13: --risk-low moved blue-600 (#2563eb) → teal-700 (#0f766e) so Low + * badges no longer share a hue with the Google-blue primary CTA. + * Contrast (WCAG): 5.47:1 on white, 4.76:1 on its own 10 % tint — + * both clear AA, slightly better than the old blue (5.17 / 4.49). * ----------------------------------------------------------------- */ --risk-critical: #dc2626; --risk-high: #ea580c; --risk-medium: #ca8a04; - --risk-low: #2563eb; + --risk-low: #0f766e; --risk-info: #71717a; /* Layout density (unchanged). */ diff --git a/apps/frontend/src/lib/devTheme.ts b/apps/frontend/src/lib/devTheme.ts deleted file mode 100644 index aeec4c7e..00000000 --- a/apps/frontend/src/lib/devTheme.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Dev-only AIS theme prototype switch (see src/styles/theme-ais.css). - * - * Deliberately NOT part of uiStore: the persisted store schema - * (`trustedoss-ui`) stays untouched, and discarding the prototype is a - * file deletion, not a store migration. State lives in its own - * localStorage key and is mirrored onto `document.documentElement` so - * portal surfaces (Dialog / Popover / Toast render into document.body) - * pick the theme up too — a wrapper class on the app content would miss - * them. - * - * Only imported from DEV-guarded code paths (main.tsx dev branch, the - * dev toggle component), so production builds tree-shake the module. - */ - -export const AIS_THEME_CLASS = "theme-ais"; -export const AIS_STORAGE_KEY = "trusca-dev-theme-ais"; - -function readFlag(): boolean { - try { - return window.localStorage.getItem(AIS_STORAGE_KEY) === "1"; - } catch { - return false; - } -} - -export function isAisThemeEnabled(): boolean { - return document.documentElement.classList.contains(AIS_THEME_CLASS); -} - -export function setAisTheme(enabled: boolean): void { - document.documentElement.classList.toggle(AIS_THEME_CLASS, enabled); - try { - if (enabled) { - window.localStorage.setItem(AIS_STORAGE_KEY, "1"); - } else { - window.localStorage.removeItem(AIS_STORAGE_KEY); - } - } catch { - // Storage unavailable (private mode etc.) — theme still applies for - // the current page, it just won't survive a reload. - } -} - -/** Restore the persisted flag on boot (any route, login included). */ -export function initAisTheme(): void { - document.documentElement.classList.toggle(AIS_THEME_CLASS, readFlag()); -} diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx index 25fa4c30..1a1eee2f 100644 --- a/apps/frontend/src/main.tsx +++ b/apps/frontend/src/main.tsx @@ -6,14 +6,6 @@ import { AppProviders } from "@/components/AppProviders"; import "@/lib/i18n"; import "@/index.css"; -if (import.meta.env.DEV) { - // AIS theme prototype (dev only) — dynamic imports keep the CSS chunk and - // the helper module out of production builds entirely; Vite replaces DEV - // with `false` and drops the whole branch. - void import("@/styles/theme-ais.css"); - void import("@/lib/devTheme").then((m) => m.initAisTheme()); -} - const container = document.getElementById("root"); if (!container) { throw new Error("Root container #root not found"); diff --git a/apps/frontend/src/pages/dev/AisThemeToggle.tsx b/apps/frontend/src/pages/dev/AisThemeToggle.tsx deleted file mode 100644 index caa60562..00000000 --- a/apps/frontend/src/pages/dev/AisThemeToggle.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Dev-only toggle chip for the AIS theme prototype. - * - * Mounted in the AppShell header and at the top of /dev/design-preview, - * always behind `import.meta.env.DEV` so production builds drop it. - * Copy is intentionally static English (designer-facing artifact, same - * convention as DesignSystemPreview — no t() keys). - */ -import { useState } from "react"; - -import { Button } from "@/components/ui/button"; -import { isAisThemeEnabled, setAisTheme } from "@/lib/devTheme"; - -export function AisThemeToggle() { - const [enabled, setEnabled] = useState(isAisThemeEnabled); - - return ( - - ); -} diff --git a/apps/frontend/src/pages/dev/DesignSystemPreview.tsx b/apps/frontend/src/pages/dev/DesignSystemPreview.tsx index 5265a35e..084e7519 100644 --- a/apps/frontend/src/pages/dev/DesignSystemPreview.tsx +++ b/apps/frontend/src/pages/dev/DesignSystemPreview.tsx @@ -2,9 +2,11 @@ * Design System Preview — W11-A, expanded into a living reference in W12-E. * * Dev-only sample page. Originally the visual confirm gate for the W11-A - * token redefinition (Vercel base + Linear polish, light single-theme); W12-E - * grew it into a living component reference + a manual review / visual- - * regression surface covering the W12 primitives. + * token redefinition; W12-E grew it into a living component reference + a + * manual review / visual-regression surface covering the W12 primitives. + * W13 (2026-06-12) re-skinned the token set to the Google AI Studio light + * tone (white canvas, blue primary, pill buttons) after the PR #394 + * prototype review. * * Routing: * - Mounted at `/dev/design-preview` (see router.tsx). @@ -34,7 +36,6 @@ import { } from "lucide-react"; import { EmptyState } from "@/components/EmptyState"; -import { AisThemeToggle } from "@/pages/dev/AisThemeToggle"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -79,48 +80,6 @@ function Section({ ); } -/** - * Shared sample block for the AIS side-by-side comparison. Rendered twice — - * once under the current tokens, once inside a `.theme-ais` wrapper — so the - * exact same markup resolves against both token sets. Inline (non-portal) - * components only; Dialog / Toast surfaces are reviewed via the global - * toggle, which flips the class on and therefore covers portals. - */ -function AisSampleBlock() { - return ( -
- {/* Labels intentionally avoid "Deploy" / "Delete" — the gallery unit - test queries those as unique buttons in the main Buttons section. */} -
- - - - -
- - - - trusca-frontend - - Last scan 2h ago · 14 components · 3 findings - - - - Critical - Low - Clean - - -
- ); -} - function Swatch({ token, varName }: { token: string; varName: string }) { return (
@@ -147,43 +106,17 @@ export function DesignSystemPreview() { Design system · living reference (dev only)

- Vercel base + Linear polish + TRUSCA — Google AI Studio light

- Sample of the new token set applied to two foundational - components. The rest of the app still uses these same tokens — - walk a few pages after this to spot any regressions before we - green-light Phase B. + Living sample of the W13 token set (adopted 2026-06-12): white + canvas, Google-blue primary, tonal secondary, pill buttons, flat + cards. The rest of the app uses these same tokens — walk a few + pages after this to spot any regressions.

-
- - - Global flip of the TRUSCA / Google AI Studio theme prototype — - persists across reloads, covers portals (dialogs, toasts), and - follows you onto real screens. - -
-
-
-
- Current — Vercel + Linear - -
-
- Prototype — Google AI Studio light -
- -
-
-
-
- -
+
@@ -216,7 +149,7 @@ export function DesignSystemPreview() {
-
+
@@ -235,7 +168,7 @@ export function DesignSystemPreview() {
-
+
@@ -329,16 +262,16 @@ export function DesignSystemPreview() {
- rounded-sm · 4px + rounded-sm · 6px
- rounded-md · 6px + rounded-md · 8px
- rounded-lg · 8px + rounded-lg · 10px
- rounded-xl · 12px + rounded-xl · 14px

diff --git a/apps/frontend/src/styles/theme-ais.css b/apps/frontend/src/styles/theme-ais.css deleted file mode 100644 index 56f0618c..00000000 --- a/apps/frontend/src/styles/theme-ais.css +++ /dev/null @@ -1,101 +0,0 @@ -/* --------------------------------------------------------------------------- - * TRUSCA — Google AI Studio light theme PROTOTYPE (dev only). - * - * Scoped override of the design tokens defined in src/index.css. The base - * `:root` values are untouched; this file redefines the SAME variable names - * under `.theme-ais` so the prototype can be flipped on/off at runtime: - * - * - Global flip: `document.documentElement.classList.add("theme-ais")` - * (see src/lib/devTheme.ts) — covers Radix portals (Dialog / Popover / - * Toast render into document.body, which is still a descendant of html). - * - Local flip: a `

` wrapper, used by the - * side-by-side comparison section in /dev/design-preview. Portals are - * NOT covered by the wrapper form. - * - * Cascade note: index.css declares tokens inside `@layer base`; this file is - * intentionally NOT layered, so it wins regardless of import order. Keep it - * plain CSS — no `@layer`, no `@apply` (it must not create new utilities). - * - * This file is loaded only behind `import.meta.env.DEV` in main.tsx, so the - * production bundle contains none of it. - * - * PROTOTYPE-ONLY: if the AIS direction is adopted, promote these values into - * the `:root` block of index.css and DELETE this file plus the dev toggle. - * - * Format contract (same as index.css): shadcn-family tokens are HSL triplets - * because tailwind.config.ts wraps them as `hsl(var(--X) / )` — a hex - * value here would silently break alpha variants like `bg-primary/10` (the - * active sidebar nav tint). Risk severity tokens stay raw hex but are NOT - * overridden — domain semantics are fixed. - * ------------------------------------------------------------------------- */ - -.theme-ais { - /* Surfaces — AIS uses a plain white canvas; panels/table headers get the - * Google grey tint and separation comes from borders, not elevation. */ - --background: 0 0% 100%; /* #ffffff */ - --foreground: 0 0% 12%; /* #1f1f1f — Google grey-900 */ - --card: 0 0% 100%; - --card-foreground: 0 0% 12%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 12%; - --muted: 210 17% 98%; /* #f8f9fa */ - --muted-foreground: 213 5% 39%; /* #5f6368 — Google grey-700, AA on muted */ - --border: 220 9% 87%; /* #dadce0 — AIS standard hairline */ - --input: 220 9% 87%; - - /* Primary CTA — Google blue replaces the warm near-black. Hover keeps - * working through the existing `bg-primary/90` utility. */ - --primary: 217 90% 43%; /* #0b57d0 */ - --primary-foreground: 0 0% 100%; - - /* Secondary becomes the Google "tonal" button (light blue fill, deep - * blue text); accent is the faint blue-grey hover tint AIS uses on rows - * and ghost surfaces. */ - --secondary: 217 91% 91%; /* #d3e3fd */ - --secondary-foreground: 217 90% 15%; /* #041e49 */ - --accent: 213 43% 96%; /* #f0f4f9 */ - --accent-foreground: 0 0% 12%; - - --destructive: 4 71% 50%; /* #d93025 — Google red */ - --destructive-foreground: 0 0% 100%; - - /* Focus ring follows primary, so the outline reads blue like AIS. */ - --ring: 217 90% 43%; - - /* Rounder overall: derived scale becomes sm 6 / md 8 / lg 10 / xl 14 px. - * Pill buttons need more than a token bump — see the rule below. */ - --radius: 0.5rem; - - /* Elevation — AIS keeps cards flat (border + tone only) and reserves - * shadows for floating surfaces. Must stay a VALID box-shadow value - * (an empty string or `none` would break `boxShadow: var(--shadow-sm)` - * consumers), hence the zero-alpha shadow. md/lg follow the Google - * elevation-1/-2 recipes. */ - --shadow-sm: 0 0 0 0 rgb(0 0 0 / 0); - --shadow-md: - 0 1px 2px 0 rgb(60 64 67 / 0.3), - 0 1px 3px 1px rgb(60 64 67 / 0.15); - --shadow-lg: - 0 1px 3px 0 rgb(60 64 67 / 0.3), - 0 4px 8px 3px rgb(60 64 67 / 0.15); - - /* NOT overridden, deliberately: - * --duration-* / --ease-out — motion timing is kept as-is. - * --risk-* — severity semantics are fixed. Known - * follow-up: --risk-low (#2563eb) sits in - * the same blue family as the new primary; - * whether to separate them is an adoption- - * phase decision, not a prototype one. - * --layout-* / --table-row — density and chrome sizes are unchanged - * so e2e selectors and layout math hold. */ -} - -/* PROTOTYPE-ONLY pill buttons: raising --radius to 9999px would full-round - * cards and dialogs too (they derive via calc()), so instead we re-round the - * two affordances AIS renders as pills — buttons (every shadcn Button variant - * has `rounded-md` in its cva base) and the sidebar NavLink items. Inputs - * keep the 8px radius on purpose. If adopted, delete this rule and add a - * proper `rounded-full` shape to buttonVariants instead. */ -.theme-ais :is(button, a).rounded-md { - border-radius: 9999px; -} diff --git a/apps/frontend/tailwind.config.ts b/apps/frontend/tailwind.config.ts index bbee6e06..81289642 100644 --- a/apps/frontend/tailwind.config.ts +++ b/apps/frontend/tailwind.config.ts @@ -91,12 +91,15 @@ const config: Config = { row: "var(--table-row)", }, borderRadius: { - // W11-A — radius hierarchy. Different sizes for different affordances. + // W13 — radius hierarchy, one step rounder than W11 (--radius 8px). // - // sm 4 px — chips / small inputs - // md 6 px — buttons / cards / table chrome (= --radius default) - // lg 8 px — drawer, large panels - // xl 12 px — modals, dialogs + // sm 6 px — chips / small inputs + // md 8 px — cards / inputs / table chrome (= --radius default) + // lg 10 px — drawer, large panels + // xl 14 px — modals, dialogs + // + // Buttons are pills (`rounded-full` in button.tsx), not part of + // this scale. // // shadcn's older `rounded-lg`/`rounded-md`/`rounded-sm` mapping // (lg = --radius, md = lg-2, sm = lg-4) still works for existing diff --git a/apps/frontend/tests/unit/devTheme.test.ts b/apps/frontend/tests/unit/devTheme.test.ts deleted file mode 100644 index 17461e48..00000000 --- a/apps/frontend/tests/unit/devTheme.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Dev-only AIS theme prototype switch (src/lib/devTheme.ts). - * - * The contract under test: the class and the localStorage flag move - * together, and initAisTheme() restores the persisted flag on boot — that is - * what keeps the theme stable across reloads on any route, login included. - */ -import { beforeEach, describe, expect, it } from "vitest"; - -import { - AIS_STORAGE_KEY, - AIS_THEME_CLASS, - initAisTheme, - isAisThemeEnabled, - setAisTheme, -} from "@/lib/devTheme"; - -describe("devTheme", () => { - beforeEach(() => { - window.localStorage.removeItem(AIS_STORAGE_KEY); - document.documentElement.classList.remove(AIS_THEME_CLASS); - }); - - it("setAisTheme(true) applies the html class and persists the flag", () => { - setAisTheme(true); - - expect(document.documentElement.classList.contains(AIS_THEME_CLASS)).toBe( - true, - ); - expect(window.localStorage.getItem(AIS_STORAGE_KEY)).toBe("1"); - expect(isAisThemeEnabled()).toBe(true); - }); - - it("setAisTheme(false) removes the class and clears the flag", () => { - setAisTheme(true); - setAisTheme(false); - - expect(document.documentElement.classList.contains(AIS_THEME_CLASS)).toBe( - false, - ); - expect(window.localStorage.getItem(AIS_STORAGE_KEY)).toBeNull(); - expect(isAisThemeEnabled()).toBe(false); - }); - - it("initAisTheme restores a persisted flag on boot", () => { - window.localStorage.setItem(AIS_STORAGE_KEY, "1"); - - initAisTheme(); - - expect(document.documentElement.classList.contains(AIS_THEME_CLASS)).toBe( - true, - ); - }); - - it("initAisTheme clears a stale class when no flag is persisted", () => { - document.documentElement.classList.add(AIS_THEME_CLASS); - - initAisTheme(); - - expect(document.documentElement.classList.contains(AIS_THEME_CLASS)).toBe( - false, - ); - }); -}); diff --git a/apps/frontend/tests/unit/pages/dev/DesignSystemPreview.test.tsx b/apps/frontend/tests/unit/pages/dev/DesignSystemPreview.test.tsx index 8f90bba5..b3fb28e6 100644 --- a/apps/frontend/tests/unit/pages/dev/DesignSystemPreview.test.tsx +++ b/apps/frontend/tests/unit/pages/dev/DesignSystemPreview.test.tsx @@ -17,11 +17,11 @@ import { describe, expect, it } from "vitest"; import { DesignSystemPreview } from "@/pages/dev/DesignSystemPreview"; -describe("DesignSystemPreview (W11-A)", () => { +describe("DesignSystemPreview (W13)", () => { it("renders the page header", () => { render(); expect( - screen.getByRole("heading", { name: /Vercel base \+ Linear polish/i }), + screen.getByRole("heading", { name: /Google AI Studio light/i }), ).toBeInTheDocument(); }); @@ -36,10 +36,11 @@ describe("DesignSystemPreview (W11-A)", () => { it("renders the four radius hierarchy samples", () => { render(); - expect(screen.getByText(/rounded-sm · 4px/)).toBeInTheDocument(); - expect(screen.getByText(/rounded-md · 6px/)).toBeInTheDocument(); - expect(screen.getByText(/rounded-lg · 8px/)).toBeInTheDocument(); - expect(screen.getByText(/rounded-xl · 12px/)).toBeInTheDocument(); + // W13 — one step rounder than W11 (--radius 8px; see tailwind.config.ts). + expect(screen.getByText(/rounded-sm · 6px/)).toBeInTheDocument(); + expect(screen.getByText(/rounded-md · 8px/)).toBeInTheDocument(); + expect(screen.getByText(/rounded-lg · 10px/)).toBeInTheDocument(); + expect(screen.getByText(/rounded-xl · 14px/)).toBeInTheDocument(); }); it("renders the three shadow elevation samples", () => { diff --git a/docs-site/docs/reference/design-system.md b/docs-site/docs/reference/design-system.md index 8b9a8e9e..42c9bd7e 100644 --- a/docs-site/docs/reference/design-system.md +++ b/docs-site/docs/reference/design-system.md @@ -1,14 +1,14 @@ --- id: design-system title: Design system -description: TrustedOSS Portal design system — tokens (colour, spacing, radius, shadow, motion, typography), component conventions, micro-interactions, accessibility, and the W11 visual-identity refresh. +description: TRUSCA design system — tokens (colour, spacing, radius, shadow, motion, typography), component conventions, micro-interactions, accessibility, and the W13 Google AI Studio re-skin. sidebar_label: Design system sidebar_position: 10 --- # Design system -The portal frontend follows a single, light-mode design system inspired by **Vercel** (light base — surfaces, dense rows, sidebar tint) and **Linear** (typography hierarchy, motion, focus polish). Dark mode is deferred to +. +The portal frontend follows a single, light-mode design system in the **Google AI Studio** tone (W13, 2026-06-12): white canvas, Google-blue primary, tonal secondary, pill buttons, flat bordered cards. The typography hierarchy, motion, and focus polish introduced with W11 (Linear-inspired) are retained. Dark mode is deferred to v2.5+. :::note Audience Frontend contributors, designers, and reviewers. The tokens here are the canonical reference — components should never hard-code hex values or magic spacing. @@ -22,52 +22,63 @@ This page is the single source of truth for visual decisions. The implementation ## Philosophy -TrustedOSS Portal is a **risk-first, information-dense, modern enterprise SCA** tool. The visual identity must: +TRUSCA is a **risk-first, information-dense, modern enterprise SCA** tool. The visual identity must: 1. **Communicate severity at a glance.** Risk colours (Critical / High / Medium / Low / Info) appear next to a textual label and an icon or dot — colour is never the sole signal. 2. **Pack data without feeling cramped.** Compact 40 px table rows; 224 px sidebar; 48 px header; 16 / 20 / 24 px card padding scale. -3. **Read as a modern enterprise product.** Warm near-black (`#18181b`) instead of navy (`#0f172a`); off-white canvas (`#fafafa`) so cards lift visually; subtle shadows; semibold headings; visible focus rings. +3. **Read as a modern product (Google AI Studio tone).** White canvas (`#ffffff`) with `#dadce0` hairline borders; Google-blue primary (`#0b57d0`) with a tonal light-blue secondary; pill buttons; flat cards that separate via border + tone rather than shadow; semibold headings; visible focus rings. Fonts stay Inter + JetBrains Mono (Google Sans is proprietary). 4. **Move only as much as needed.** Motion is short and ease-out — 150 ms for hover / focus, 200 ms for drawer slide, 250 ms for page-level chrome. No bounce, no fade-in delays. -### W11 (2026-05-27) — visual refresh +### W13 (2026-06-12) — Google AI Studio re-skin -The W11 milestone replaced the previous "BD-style 2015" aesthetic with the current Vercel+Linear blend. The structural decisions (sidebar nav, 40 px row, drawer-for-detail, risk semantics) carry over unchanged. What changed: +W13 replaced the W11 Vercel + Linear skin with the Google AI Studio light tone, adopted after a dev-toggle prototype review (PR #394). Structural decisions (sidebar nav, 40 px row, drawer-for-detail, risk semantics) and the W11 polish layer (typography, motion, focus ring) carry over unchanged. What changed: -| Surface | Before | After | +| Surface | W11 (before) | W13 (after) | |---|---|---| -| Primary CTA | `#0f172a` cool navy | `#18181b` warm near-black | -| Page background | `#ffffff` pure white | `#fafafa` off-white canvas | -| Card surface | grey-tinted | `#ffffff` pure white (lifts off the canvas) | -| Border | `slate-200` | `#e5e5ea` neutral hairline | -| Radius | 8 px uniform | hierarchy — sm 4 / md 6 / lg 8 / xl 12 | -| Shadow | none / default | sm (card) / md (popover) / lg (drawer · dialog) | -| Motion | default browser | 150 / 200 / 250 ms ease-out | -| Heading weight | bold | semibold + tracking-tight | -| Focus ring | shadcn default | 2 px outline + 2 px offset (a11y) | -| Detail surface | drawer-only | dual surface — drawer (quick check) + page nav (deep work) | - -The risk colour palette (Critical / High / Medium / Low / Info) is intentionally **unchanged** — the brand semantics are fixed across releases. Where the raw severity hex fails WCAG AA as body text on a light tint, the badge text shade is darkened within the same hue family (see [Severity colour accessibility](#severity-colour-accessibility) below). +| Primary CTA | `#18181b` warm near-black | `#0b57d0` Google blue | +| Secondary | muted grey tint | `#d3e3fd` tonal light blue | +| Page background | `#fafafa` off-white canvas | `#ffffff` white canvas | +| Card surface | white, lifts via shadow-sm | white, flat — separates via border + tone | +| Hover / ghost tint | muted grey | `#f0f4f9` faint blue-grey | +| Border | `#e5e5ea` | `#dadce0` AIS hairline | +| Button shape | rounded-md (6 px) | pill (`rounded-full`) | +| Radius scale | sm 4 / md 6 / lg 8 / xl 12 | sm 6 / md 8 / lg 10 / xl 14 | +| Shadow | sm (card) / md / lg | sm = none (flat); md / lg = Google elevation-1/-2 | +| Focus ring | near-black | `#0b57d0` blue (matches primary) | +| `--risk-low` | `#2563eb` blue-600 | `#0f766e` teal-700 (no longer collides with the blue primary) | + +Risk severity colours for Critical / High / Medium / Info are **unchanged** — the domain semantics are fixed. Low is the single W13 exception (user decision): the old blue read like a CTA / link next to the new Google-blue primary. Where a severity hex fails WCAG AA as body text on a light tint, the badge text shade is darkened within the same hue family (see [Severity colour accessibility](#severity-colour-accessibility) below). + +
+W11 (2026-05-27) — the previous Vercel + Linear refresh + +W11 replaced the original "BD-style 2015" aesthetic (navy `#0f172a`, uniform 8 px radius, default easing) with a Vercel light base + Linear polish: warm near-black `#18181b` primary, `#fafafa` canvas, radius / shadow / motion token hierarchy, semibold headings, visible focus rings, and the drawer + page dual detail surface. The polish layer survives in W13; the colour skin does not. + +
## Colour tokens All colour decisions reference the CSS custom properties declared in `index.css`. Components should never reference hex values directly — use the Tailwind utility (`bg-background`, `text-foreground`, `bg-risk-critical/10`) or the CSS variable. -### Neutral palette (Vercel base) +### Neutral palette (Google AI Studio light) | Token | Hex | HSL | Use | |---|---|---|---| -| `--background` | `#fafafa` | `0 0% 98%` | Page canvas. Lets cards lift visually. | -| `--card` | `#ffffff` | `0 0% 100%` | Elevated surfaces — cards, popovers, drawer body, tooltip. | -| `--foreground` | `#18181b` | `240 6% 10%` | Body text. Warm near-black, not navy. | -| `--muted` | `#f4f4f5` | `240 5% 96%` | Subtle fills — table headers, sidebar tint, placeholder backgrounds, disabled inputs. | -| `--muted-foreground` | `#71717a` | `240 4% 46%` | Secondary text, captions, table column headers. | -| `--border` | `#e5e5ea` | `240 5% 91%` | Hairline borders. Decorative separator only — never the sole means of identifying a UI region. | -| `--input` | `#e5e5ea` | `240 5% 91%` | Input outline. | -| `--primary` | `#18181b` | `240 6% 10%` | Primary CTA — "the important action on the page". | -| `--primary-foreground` | `#fafafa` | `0 0% 98%` | Text on primary. | -| `--destructive` | `#dc2626` | `0 72% 51%` | Destructive CTA. Aligned with `--risk-critical` so destructive buttons share severity-badge visual language. | -| `--destructive-foreground` | `#fafafa` | `0 0% 98%` | Text on destructive. | -| `--ring` | `#18181b` | `240 6% 10%` | Focus ring. Matches primary so the outline reads as "the same action this is". | +| `--background` | `#ffffff` | `0 0% 100%` | Page canvas — plain white (AIS). | +| `--card` | `#ffffff` | `0 0% 100%` | Cards, popovers, drawer body, tooltip — flush with the canvas, separated by border + tone. | +| `--foreground` | `#1f1f1f` | `0 0% 12%` | Body text — Google grey-900. | +| `--muted` | `#f8f9fa` | `210 17% 98%` | Subtle fills — table headers, sidebar tint, placeholder backgrounds, disabled inputs. | +| `--muted-foreground` | `#5f6368` | `213 5% 39%` | Secondary text, captions, table column headers — Google grey-700. | +| `--border` | `#dadce0` | `220 9% 87%` | Hairline borders — the AIS standard. Decorative separator only, never the sole means of identifying a UI region. | +| `--input` | `#dadce0` | `220 9% 87%` | Input outline. | +| `--primary` | `#0b57d0` | `217 90% 43%` | Primary CTA — Google blue, "the important action on the page". | +| `--primary-foreground` | `#ffffff` | `0 0% 100%` | Text on primary. | +| `--secondary` | `#d3e3fd` | `217 91% 91%` | Tonal button fill (Google "tonal" pattern). | +| `--secondary-foreground` | `#041e49` | `217 90% 15%` | Text on tonal fill. | +| `--accent` | `#f0f4f9` | `213 43% 96%` | Hover rows, ghost-button hover — faint blue-grey tint. | +| `--destructive` | `#d93025` | `4 71% 50%` | Destructive CTA — Google red, near `--risk-critical` so destructive buttons share severity-badge visual language. | +| `--destructive-foreground` | `#ffffff` | `0 0% 100%` | Text on destructive. | +| `--ring` | `#0b57d0` | `217 90% 43%` | Focus ring. Matches primary so the outline reads as "the same action this is". | ### Risk severity (domain semantics — fixed) @@ -76,10 +87,10 @@ All colour decisions reference the CSS custom properties declared in `index.css` | `--risk-critical` | `#dc2626` | Critical CVE, forbidden licence, build-blocking finding. | | `--risk-high` | `#ea580c` | High-severity CVE, conditional licence at risk. | | `--risk-medium` | `#ca8a04` | Medium CVE, conditional licence awaiting review. | -| `--risk-low` | `#2563eb` | Low CVE, informational status. | +| `--risk-low` | `#0f766e` | Low CVE, informational status. Teal-700 since W13 — moved out of the blue family so Low badges don't read like the blue primary CTA. | | `--risk-info` | `#71717a` | Neutral informational state. | -The severity hex values are **never changed** between releases. They appear in: +The severity hex values are stable across releases (the W13 Low move is the documented exception). They appear in: - Recharts fills and chart legends (raw hex via the `--risk-X` variable). - `bg-risk-X/N` tints on badges and dot indicators. @@ -109,26 +120,26 @@ When severity colour is used as **body text** (a coloured word inside a badge or ## Radius hierarchy -Different affordances use different radii so depth reads at a glance. +Different affordances use different radii so depth reads at a glance. W13 moved the whole scale one step rounder. | Token | Value | Affordance | |---|---|---| -| `--radius-sm` | 4 px | Small inputs, badges, chips. | -| `--radius` | 6 px | **Default** — buttons, cards, table chrome. | -| `--radius-lg` | 8 px | Drawer, large surfaces. | -| `--radius-xl` | 12 px | Modals, dialogs. | +| `--radius-sm` | 6 px | Small inputs, badges, chips. | +| `--radius` | 8 px | **Default** — cards, inputs, table chrome. | +| `--radius-lg` | 10 px | Drawer, large surfaces. | +| `--radius-xl` | 14 px | Modals, dialogs. | -The Tailwind config derives `rounded-sm`, `rounded-md`, `rounded-lg`, `rounded-xl` from these tokens via `calc()`. +The Tailwind config derives `rounded-sm`, `rounded-md`, `rounded-lg`, `rounded-xl` from these tokens via `calc()`. **Buttons are pills** (`rounded-full` in `button.tsx`) and deliberately not part of this scale — raising `--radius` to a pill value would full-round cards and dialogs through the `calc()` derivations. ## Shadow scale -Vercel-style subtle elevation. Light shadows only — no glow. +AIS keeps in-flow surfaces flat — cards and buttons separate via border + tone. Shadows are reserved for floating surfaces and follow the Google elevation recipes. | Token | Value | Use | |---|---|---| -| `--shadow-sm` | `0 1px 2px 0 rgb(0 0 0 / 0.04)` | Cards, stat tiles. | -| `--shadow-md` | `0 2px 8px -2px rgb(0 0 0 / 0.08), 0 1px 2px 0 rgb(0 0 0 / 0.04)` | Dropdown, popover, tooltip. | -| `--shadow-lg` | `0 10px 28px -8px rgb(0 0 0 / 0.12), 0 3px 8px -3px rgb(0 0 0 / 0.06)` | Drawer, dialog. | +| `--shadow-sm` | `0 0 0 0 rgb(0 0 0 / 0)` | Intentionally a zero-alpha no-op (flat cards / buttons). Kept a valid box-shadow value so `var(--shadow-sm)` consumers don't break, and so a future token change can re-enable elevation in one place. | +| `--shadow-md` | `0 1px 2px 0 rgb(60 64 67 / 0.3), 0 1px 3px 1px rgb(60 64 67 / 0.15)` | Dropdown, popover, tooltip (Google elevation-1). | +| `--shadow-lg` | `0 1px 3px 0 rgb(60 64 67 / 0.3), 0 4px 8px 3px rgb(60 64 67 / 0.15)` | Drawer, dialog (Google elevation-2). | ## Motion @@ -202,10 +213,12 @@ Do not hand-roll a `

` block — extend `PageHeader` if a new layout `apps/frontend/src/components/ui/button.tsx` -- Default variant uses `bg-primary text-primary-foreground` — solid warm near-black. +- **Pill shape** (`rounded-full`, W13) on every variant and size — the AIS button silhouette. Inputs keep the token radius; only buttons (and sidebar nav items) are pills. +- Default variant uses `bg-primary text-primary-foreground` — solid Google blue. +- `secondary` variant is the Google **tonal** button — light-blue fill (`--secondary`), deep-blue text. - `outline` variant uses `border-input bg-background` — for secondary actions. -- `ghost` variant uses no background, hover tint only — for nav items and toolbar actions. -- `destructive` uses `bg-destructive` — Critical-aligned red, reserved for irreversible actions (delete, revoke, reject). +- `ghost` variant uses no background, hover tint only (`--accent`) — for nav items and toolbar actions. +- `destructive` uses `bg-destructive` — Google red near `--risk-critical`, reserved for irreversible actions (delete, revoke, reject). - Hover and focus transitions use `transition-colors duration-fast ease-out` (150 ms). - All variants include the focus ring. @@ -217,9 +230,9 @@ Do not hand-roll a `

` block — extend `PageHeader` if a new layout ### Card -- Pure-white surface (`bg-card`) on the off-white canvas — lifts visually without a heavy shadow. -- `rounded-md` (6 px) by default; `rounded-lg` (8 px) for primary content cards. -- `shadow-sm` for stats / tiles; `shadow-md` for elevated popovers. +- White surface (`bg-card`) flush on the white canvas — separation comes from the `--border` hairline and the `--muted` tone, not elevation (AIS flat-card pattern; `shadow-sm` resolves to a no-op). +- `rounded-md` (8 px) by default; `rounded-lg` (10 px) for primary content cards. +- `shadow-md` only on floating surfaces (popover / dropdown), never on in-flow cards. ### Table @@ -303,28 +316,30 @@ The portal targets **WCAG 2.1 Level AA**. Three policies make this concrete. | Pair | Ratio | Note | |---|---|---| -| `--foreground` on `--background` | 16.97:1 | Body text. AAA. | -| `--foreground` on `--card` | 17.72:1 | Body text on card. AAA. | -| `--muted-foreground` on `--background` | 4.63:1 | Captions, secondary text. AA. | -| `--muted-foreground` on `--card` | 4.83:1 | Captions on card. AA. | -| `--primary-foreground` on `--primary` | 16.97:1 | Primary button label. AAA. | -| `--destructive-foreground` on `--destructive` | 4.63:1 | Destructive button label. AA. | -| `--ring` on `--background` | 16.97:1 | Focus ring. AAA. | +| `--foreground` on `--background` / `--card` | 16.48:1 | Body text. AAA. | +| `--muted-foreground` on `--background` / `--card` | 6.05:1 | Captions, secondary text. AA. | +| `--muted-foreground` on `--muted` | 5.74:1 | Captions on tinted fills. AA. | +| `--primary-foreground` on `--primary` | 6.39:1 | Primary button label. AA. | +| `--secondary-foreground` on `--secondary` | 12.57:1 | Tonal button label. AAA. | +| `--destructive-foreground` on `--destructive` | 4.77:1 | Destructive button label. AA. | +| `--ring` on `--background` | 6.39:1 | Focus ring. AA (UI ≥ 3:1). | -Decorative borders (`--border` on `--background`, 1.20:1) are **intentionally low-contrast** — they are visual separators, not informative UI elements, and WCAG 1.4.11 exempts them. +Decorative borders (`--border` on `--background`, 1.37:1) are **intentionally low-contrast** — they are visual separators, not informative UI elements, and WCAG 1.4.11 exempts them. ### Severity colour accessibility -Severity hex values (`#dc2626` / `#ea580c` / `#ca8a04` / `#2563eb` / `#71717a`) are brand-fixed. Used as **body text** on a light tint they would measure as low as 2.5:1, which fails AA. The fix is structural, not chromatic — when the severity tone is used as text, the rendered text colour uses a deeper shade from the same Tailwind hue family: +Severity hex values (`#dc2626` / `#ea580c` / `#ca8a04` / `#0f766e` / `#71717a`) are brand-fixed. Used as **body text** on a light tint some would measure as low as 2.5:1, which fails AA. The fix is structural, not chromatic — when the severity tone is used as text, the rendered text colour uses a deeper shade from the same Tailwind hue family: | Tone | Tint background | Text colour | Contrast | |---|---|---|---| | `critical` | `bg-risk-critical/10` | `text-red-700` (`#b91c1c`) | 5.54:1 | | `high` | `bg-risk-high/10` | `text-orange-800` (`#9a3412`) | 6.47:1 | | `medium` | `bg-risk-medium/15` | `text-yellow-800` (`#854d0e`) | 5.91:1 | -| `low` | `bg-risk-low/10` | `text-blue-700` (`#1d4ed8`) | 5.83:1 | +| `low` | `bg-risk-low/10` | `text-teal-800` (`#115e59`) | 6.59:1 | | `info` | `bg-risk-info/15` | `text-slate-600` (`#52525b`) | 6.41:1 | +The raw Low token itself (teal-700 `#0f766e`) also clears AA where it's used directly as text (5.47:1 on white, 4.76:1 on its own 10 % tint) — slightly better than the blue-600 it replaced. + The **dot indicators** (in `SeverityBadge`, chart legends, status pills) continue to use the raw `bg-risk-X` token — colour identity stays recognisable; only text shade is darkened. The reference implementation is `apps/frontend/src/components/ui/badge.tsx` (W11-H). ### Colour is not the only signal @@ -373,8 +388,9 @@ All interactive elements are reachable by `Tab` and operable by `Enter` / `Space | W12-C | 2026-06-11 | **Craft elevation — motion (CSS-only).** Route-change entrance fade (`
` keyed on pathname, 250 ms), sidebar collapse aligned to 250 ms, and a global `prefers-reduced-motion` guard. No new dependency (tailwindcss-animate only). Skeleton doc corrected to the real 2000 ms `animate-pulse`. | | W12-D | 2026-06-12 | **Craft elevation — empty / loading polish.** EmptyState gains a layered icon medallion + optional `illustration` slot; new `TableRowsSkeleton` renders per-column loading cells (replacing single full-width bars) on the Scans and Admin Users tables. | | W12-E/F | 2026-06-12 | **Craft elevation — guardrails + docs.** Grew `/dev/design-preview` into a living component reference (typography, badges, empty / loading, feedback) and added a "Frontend UI" section to the contributor coding standards. Visual-regression baseline expansion (4 → ~15) is a CI / operator follow-up — correct linux baselines cannot be generated from a darwin dev box. | +| W13 | 2026-06-12 | **Google AI Studio re-skin (TRUSCA).** Token promotion from the PR #394 dev prototype after user review: white canvas, `#0b57d0` blue primary, `#d3e3fd` tonal secondary, `#dadce0` borders, radius scale +2 px, flat cards (shadow-sm → no-op, md/lg → Google elevation recipes), pill buttons (`rounded-full` in `button.tsx` + sidebar nav), `--risk-low` blue-600 → teal-700 to separate Low badges from the blue primary. Layout density, motion, typography, and the other severity colours unchanged. Visual-regression baselines require a linux refresh after merge. | -The previous "BD-style 2015" aesthetic (`#0f172a` navy, pure white canvas, uniform 8 px radius, no shadow, default browser easing) is fully retired by W11. +The W11 Vercel + Linear skin is retired by W13; the earlier "BD-style 2015" aesthetic was retired by W11. ## See also diff --git a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/design-system.md b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/design-system.md index 367345bf..0b98ff73 100644 --- a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/design-system.md +++ b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/design-system.md @@ -1,14 +1,14 @@ --- id: design-system title: 디자인 시스템 -description: TrustedOSS Portal 디자인 시스템 — 토큰(색·spacing·radius·shadow·motion·typography)·컴포넌트 규약·마이크로인터랙션·접근성·W11 시각 정체성 재정의. +description: TRUSCA 디자인 시스템 — 토큰(색·spacing·radius·shadow·motion·typography)·컴포넌트 규약·마이크로인터랙션·접근성·W13 Google AI Studio 재스킨. sidebar_label: 디자인 시스템 sidebar_position: 10 --- # 디자인 시스템 -포털 프론트엔드는 **Vercel** (light base — surface · dense row · sidebar tint) 과 **Linear** (타이포 hierarchy · motion · focus polish) 에 영감을 받은 단일 light 모드 디자인 시스템을 따릅니다. Dark 모드는 + 이후로 미룹니다. +포털 프론트엔드는 **Google AI Studio** 톤(W13, 2026-06-12)의 단일 light 모드 디자인 시스템을 따릅니다 — 흰 canvas, Google 블루 primary, tonal secondary, 필(pill) 버튼, 보더로 구분되는 평면 카드. W11에서 도입한 타이포 hierarchy·motion·focus polish(Linear 영감)는 그대로 유지합니다. Dark 모드는 v2.5+ 이후로 미룹니다. :::note 대상 독자 프론트엔드 기여자, 디자이너, 리뷰어. 본 문서의 토큰은 canonical reference 입니다 — 컴포넌트는 hex 값이나 magic spacing 을 직접 박지 않습니다. @@ -22,52 +22,63 @@ sidebar_position: 10 ## 철학 -TrustedOSS Portal 은 **리스크 우선 · 정보 밀도 · 모던 엔터프라이즈** SCA 도구입니다. 시각 정체성이 충족해야 할 것: +TRUSCA 는 **리스크 우선 · 정보 밀도 · 모던 엔터프라이즈** SCA 도구입니다. 시각 정체성이 충족해야 할 것: 1. **한눈에 심각도 전달.** Severity 색 (Critical / High / Medium / Low / Info) 은 항상 텍스트 라벨 + 아이콘/dot 와 함께 — 색은 신호의 단독 수단이 아닙니다. 2. **답답하지 않게 데이터 밀도.** 40 px compact 행 · 224 px 사이드바 · 48 px 헤더 · 16/20/24 px 카드 padding 표준화. -3. **모던 엔터프라이즈 제품 톤.** Navy `#0f172a` 가 아닌 warm near-black `#18181b` · 순수 흰색이 아닌 off-white canvas `#fafafa` · subtle shadow · semibold heading · 가시 focus ring. +3. **모던 제품 톤 (Google AI Studio).** 흰 canvas `#ffffff` + `#dadce0` hairline border · Google 블루 primary `#0b57d0` + tonal 라이트블루 secondary · 필 버튼 · shadow 대신 보더+톤으로 구분되는 평면 카드 · semibold heading · 가시 focus ring. 폰트는 Inter + JetBrains Mono 유지(Google Sans 는 비공개 폰트). 4. **필요한 만큼만 움직임.** Hover/focus 150 ms · drawer slide 200 ms · 페이지 크롬 250 ms. ease-out 만. Bounce 없음, fade-in delay 없음. -### W11 (2026-05-27) — 시각 재정의 +### W13 (2026-06-12) — Google AI Studio 재스킨 -W11 마일스톤에서 기존 "BD-style 2015" 미감을 Vercel+Linear 혼합으로 교체했습니다. 구조적 결정 (사이드바 nav · 40 px row · drawer-for-detail · severity 의미) 은 유지. 바뀐 것: +W13 에서 W11 의 Vercel + Linear 스킨을 Google AI Studio light 톤으로 교체했습니다. dev 토글 프로토타입(PR #394) 검토 후 채택한 결정입니다. 구조적 결정 (사이드바 nav · 40 px row · drawer-for-detail · severity 의미) 과 W11 polish 레이어 (타이포 · motion · focus ring) 는 유지. 바뀐 것: -| Surface | Before | After | +| Surface | W11 (before) | W13 (after) | |---|---|---| -| Primary CTA | `#0f172a` cool navy | `#18181b` warm near-black | -| 페이지 배경 | `#ffffff` 순백 | `#fafafa` off-white canvas | -| 카드 surface | grey 톤 | `#ffffff` 순백 (canvas 위로 떠 보임) | -| Border | `slate-200` | `#e5e5ea` neutral hairline | -| Radius | 8 px 일관 | 계층 — sm 4 / md 6 / lg 8 / xl 12 | -| Shadow | 없음 / 기본 | sm (card) / md (popover) / lg (drawer · dialog) | -| Motion | 브라우저 기본 | 150 / 200 / 250 ms ease-out | -| Heading weight | bold | semibold + tracking-tight | -| Focus ring | shadcn 기본 | 2 px outline + 2 px offset (a11y) | -| 디테일 surface | 드로어 전용 | dual surface — 드로어 (빠른 확인) + 페이지 nav (깊은 작업) | - -Severity 팔레트 (Critical / High / Medium / Low / Info) 는 **의도적으로 유지** — 브랜드 의미가 릴리스 간 고정입니다. Severity hex 가 light tint 위 본문 텍스트로 사용될 때 WCAG AA 가 안 나오면, 같은 hue family 의 더 짙은 shade 로 텍스트 색만 어둡게 — 아래 [Severity 색 접근성](#severity-색-접근성) 참고. +| Primary CTA | `#18181b` warm near-black | `#0b57d0` Google 블루 | +| Secondary | muted grey tint | `#d3e3fd` tonal 라이트블루 | +| 페이지 배경 | `#fafafa` off-white canvas | `#ffffff` 흰 canvas | +| 카드 surface | 흰색, shadow-sm 으로 떠 보임 | 흰색, 평면 — 보더+톤으로 구분 | +| Hover / ghost tint | muted grey | `#f0f4f9` 옅은 blue-grey | +| Border | `#e5e5ea` | `#dadce0` AIS hairline | +| 버튼 형태 | rounded-md (6 px) | 필 (`rounded-full`) | +| Radius 스케일 | sm 4 / md 6 / lg 8 / xl 12 | sm 6 / md 8 / lg 10 / xl 14 | +| Shadow | sm (card) / md / lg | sm = 없음 (평면); md/lg = Google elevation-1/-2 | +| Focus ring | near-black | `#0b57d0` 블루 (primary 와 매칭) | +| `--risk-low` | `#2563eb` blue-600 | `#0f766e` teal-700 (블루 primary 와 더 이상 충돌하지 않음) | + +Critical / High / Medium / Info 의 severity 색은 **유지** — 도메인 의미가 고정입니다. Low 만 W13 의 단일 예외(사용자 결정)입니다: 기존 블루가 새 Google 블루 primary 옆에서 CTA/링크처럼 읽혔습니다. Severity hex 가 light tint 위 본문 텍스트로 사용될 때 WCAG AA 가 안 나오면, 같은 hue family 의 더 짙은 shade 로 텍스트 색만 어둡게 — 아래 [Severity 색 접근성](#severity-색-접근성) 참고. + +
+W11 (2026-05-27) — 직전의 Vercel + Linear 재정의 + +W11 은 원래의 "BD-style 2015" 미감(navy `#0f172a` · 8 px 일관 radius · 브라우저 기본 easing)을 Vercel light base + Linear polish 로 교체했습니다: warm near-black `#18181b` primary · `#fafafa` canvas · radius/shadow/motion 토큰 계층 · semibold heading · 가시 focus ring · 드로어+페이지 dual 디테일 surface. polish 레이어는 W13 에 살아남았고, 색 스킨은 교체되었습니다. + +
## 색 토큰 모든 색 결정은 `index.css` 의 CSS custom property 를 참조합니다. 컴포넌트는 hex 값을 직접 박지 않습니다 — Tailwind utility (`bg-background`, `text-foreground`, `bg-risk-critical/10`) 나 CSS 변수를 사용합니다. -### Neutral 팔레트 (Vercel base) +### Neutral 팔레트 (Google AI Studio light) | 토큰 | Hex | HSL | 용도 | |---|---|---|---| -| `--background` | `#fafafa` | `0 0% 98%` | 페이지 canvas. 카드가 시각적으로 뜨도록. | -| `--card` | `#ffffff` | `0 0% 100%` | Elevated surface — 카드 · popover · 드로어 본문 · 툴팁. | -| `--foreground` | `#18181b` | `240 6% 10%` | 본문 텍스트. Warm near-black (navy 아님). | -| `--muted` | `#f4f4f5` | `240 5% 96%` | 미묘한 fill — 테이블 헤더 · 사이드바 tint · placeholder · disabled input. | -| `--muted-foreground` | `#71717a` | `240 4% 46%` | 보조 텍스트 · caption · 테이블 컬럼 헤더. | -| `--border` | `#e5e5ea` | `240 5% 91%` | Hairline border. 장식용 separator 만 — UI 영역 식별의 유일한 수단이 되지 않습니다. | -| `--input` | `#e5e5ea` | `240 5% 91%` | Input outline. | -| `--primary` | `#18181b` | `240 6% 10%` | Primary CTA — "이 페이지의 중요 액션". | -| `--primary-foreground` | `#fafafa` | `0 0% 98%` | Primary 위 텍스트. | -| `--destructive` | `#dc2626` | `0 72% 51%` | Destructive CTA. `--risk-critical` 와 같아서 destructive 버튼이 severity 와 같은 시각 언어. | -| `--destructive-foreground` | `#fafafa` | `0 0% 98%` | Destructive 위 텍스트. | -| `--ring` | `#18181b` | `240 6% 10%` | Focus ring. Primary 와 매칭 — outline 이 액션과 같은 색 패밀리로 읽힘. | +| `--background` | `#ffffff` | `0 0% 100%` | 페이지 canvas — 순백 (AIS). | +| `--card` | `#ffffff` | `0 0% 100%` | 카드 · popover · 드로어 본문 · 툴팁 — canvas 와 같은 면이며 보더+톤으로 구분. | +| `--foreground` | `#1f1f1f` | `0 0% 12%` | 본문 텍스트 — Google grey-900. | +| `--muted` | `#f8f9fa` | `210 17% 98%` | 미묘한 fill — 테이블 헤더 · 사이드바 tint · placeholder · disabled input. | +| `--muted-foreground` | `#5f6368` | `213 5% 39%` | 보조 텍스트 · caption · 테이블 컬럼 헤더 — Google grey-700. | +| `--border` | `#dadce0` | `220 9% 87%` | Hairline border — AIS 표준. 장식용 separator 만 — UI 영역 식별의 유일한 수단이 되지 않습니다. | +| `--input` | `#dadce0` | `220 9% 87%` | Input outline. | +| `--primary` | `#0b57d0` | `217 90% 43%` | Primary CTA — Google 블루, "이 페이지의 중요 액션". | +| `--primary-foreground` | `#ffffff` | `0 0% 100%` | Primary 위 텍스트. | +| `--secondary` | `#d3e3fd` | `217 91% 91%` | Tonal 버튼 fill (Google "tonal" 패턴). | +| `--secondary-foreground` | `#041e49` | `217 90% 15%` | Tonal fill 위 텍스트. | +| `--accent` | `#f0f4f9` | `213 43% 96%` | Hover 행 · ghost 버튼 hover — 옅은 blue-grey tint. | +| `--destructive` | `#d93025` | `4 71% 50%` | Destructive CTA — Google red. `--risk-critical` 과 비슷해 destructive 버튼이 severity 와 같은 시각 언어. | +| `--destructive-foreground` | `#ffffff` | `0 0% 100%` | Destructive 위 텍스트. | +| `--ring` | `#0b57d0` | `217 90% 43%` | Focus ring. Primary 와 매칭 — outline 이 액션과 같은 색 패밀리로 읽힘. | ### Severity (도메인 의미 — 고정) @@ -76,10 +87,10 @@ Severity 팔레트 (Critical / High / Medium / Low / Info) 는 **의도적으로 | `--risk-critical` | `#dc2626` | Critical CVE · forbidden 라이선스 · build-blocking finding. | | `--risk-high` | `#ea580c` | High-severity CVE · conditional 라이선스 위험. | | `--risk-medium` | `#ca8a04` | Medium CVE · 검토 대기 conditional 라이선스. | -| `--risk-low` | `#2563eb` | Low CVE · 정보성 상태. | +| `--risk-low` | `#0f766e` | Low CVE · 정보성 상태. W13 부터 teal-700 — Low 배지가 블루 primary CTA 처럼 읽히지 않도록 블루 계열에서 분리. | | `--risk-info` | `#71717a` | 중립 정보. | -Severity hex 는 **릴리스 간 변경하지 않습니다**. 사용처: +Severity hex 는 릴리스 간 안정적으로 유지합니다 (W13 의 Low 이동이 문서화된 단일 예외). 사용처: - Recharts fill · 차트 범례 (raw hex, `--risk-X` 변수 참조). - 배지 · dot 인디케이터의 `bg-risk-X/N` tint. @@ -109,26 +120,26 @@ Severity 톤이 **본문 텍스트** 로 사용될 때 (배지 안의 색 단어 ## Radius 계층 -Affordance 별로 다른 radius — depth 가 한눈에 읽힘. +Affordance 별로 다른 radius — depth 가 한눈에 읽힘. W13 에서 전체 스케일을 한 단계 더 둥글게 올렸습니다. | 토큰 | 값 | Affordance | |---|---|---| -| `--radius-sm` | 4 px | 작은 input · 배지 · 칩. | -| `--radius` | 6 px | **기본** — 버튼 · 카드 · 테이블 크롬. | -| `--radius-lg` | 8 px | 드로어 · 큰 surface. | -| `--radius-xl` | 12 px | 모달 · 다이얼로그. | +| `--radius-sm` | 6 px | 작은 input · 배지 · 칩. | +| `--radius` | 8 px | **기본** — 카드 · input · 테이블 크롬. | +| `--radius-lg` | 10 px | 드로어 · 큰 surface. | +| `--radius-xl` | 14 px | 모달 · 다이얼로그. | -Tailwind config 가 `calc()` 로 `rounded-sm`/`rounded-md`/`rounded-lg`/`rounded-xl` 를 이 토큰에서 파생. +Tailwind config 가 `calc()` 로 `rounded-sm`/`rounded-md`/`rounded-lg`/`rounded-xl` 를 이 토큰에서 파생. **버튼은 필 형태** (`button.tsx` 의 `rounded-full`) 로 이 스케일에 포함하지 않습니다 — `--radius` 를 필 값으로 올리면 `calc()` 파생을 통해 카드·다이얼로그까지 완전히 둥글어지기 때문입니다. ## Shadow 스케일 -Vercel 톤의 subtle elevation. 가벼운 그림자만 — glow 없음. +AIS 는 본문 흐름 위의 surface 를 평면으로 둡니다 — 카드·버튼은 보더+톤으로 구분합니다. Shadow 는 떠 있는 surface 전용이며 Google elevation 레시피를 따릅니다. | 토큰 | 값 | 용도 | |---|---|---| -| `--shadow-sm` | `0 1px 2px 0 rgb(0 0 0 / 0.04)` | 카드 · stat 타일. | -| `--shadow-md` | `0 2px 8px -2px rgb(0 0 0 / 0.08), 0 1px 2px 0 rgb(0 0 0 / 0.04)` | 드롭다운 · popover · 툴팁. | -| `--shadow-lg` | `0 10px 28px -8px rgb(0 0 0 / 0.12), 0 3px 8px -3px rgb(0 0 0 / 0.06)` | 드로어 · 다이얼로그. | +| `--shadow-sm` | `0 0 0 0 rgb(0 0 0 / 0)` | 의도적 zero-alpha no-op (평면 카드·버튼). `var(--shadow-sm)` 소비자가 깨지지 않도록 유효한 box-shadow 값을 유지 — 미래에 한 곳만 바꿔 elevation 을 되살릴 수 있습니다. | +| `--shadow-md` | `0 1px 2px 0 rgb(60 64 67 / 0.3), 0 1px 3px 1px rgb(60 64 67 / 0.15)` | 드롭다운 · popover · 툴팁 (Google elevation-1). | +| `--shadow-lg` | `0 1px 3px 0 rgb(60 64 67 / 0.3), 0 4px 8px 3px rgb(60 64 67 / 0.15)` | 드로어 · 다이얼로그 (Google elevation-2). | ## Motion @@ -202,10 +213,12 @@ stacked 변형에는 선택적 `meta` 슬롯도 있습니다 — 부제 아래 `apps/frontend/src/components/ui/button.tsx` -- 기본 variant: `bg-primary text-primary-foreground` — solid warm near-black. +- **필 형태** (`rounded-full`, W13) — 모든 variant·size 에 적용되는 AIS 버튼 실루엣. Input 은 토큰 radius 유지, 필은 버튼(과 사이드바 nav 항목)만. +- 기본 variant: `bg-primary text-primary-foreground` — solid Google 블루. +- `secondary` variant: Google **tonal** 버튼 — 라이트블루 fill (`--secondary`) + 짙은 블루 텍스트. - `outline` variant: `border-input bg-background` — 보조 액션. -- `ghost` variant: 배경 없음, hover tint 만 — nav 항목 · toolbar 액션. -- `destructive`: `bg-destructive` — Critical-aligned 빨강, 되돌릴 수 없는 액션 전용 (delete · revoke · reject). +- `ghost` variant: 배경 없음, hover tint (`--accent`) 만 — nav 항목 · toolbar 액션. +- `destructive`: `bg-destructive` — `--risk-critical` 에 가까운 Google red, 되돌릴 수 없는 액션 전용 (delete · revoke · reject). - Hover / focus transition: `transition-colors duration-fast ease-out` (150 ms). - 모든 variant 에 focus ring 포함. @@ -217,9 +230,9 @@ stacked 변형에는 선택적 `meta` 슬롯도 있습니다 — 부제 아래 ### Card -- Off-white canvas 위 순백 surface (`bg-card`) — 무거운 shadow 없이 시각적으로 뜸. -- 기본 `rounded-md` (6 px) · 메인 콘텐츠 카드는 `rounded-lg` (8 px). -- Stat / 타일은 `shadow-sm` · 떠 있는 popover 는 `shadow-md`. +- 흰 canvas 와 같은 면의 흰 surface (`bg-card`) — 구분은 `--border` hairline 과 `--muted` 톤으로 (AIS 평면 카드 패턴; `shadow-sm` 은 no-op). +- 기본 `rounded-md` (8 px) · 메인 콘텐츠 카드는 `rounded-lg` (10 px). +- `shadow-md` 는 떠 있는 surface (popover · 드롭다운) 전용 — 본문 흐름 위 카드에는 쓰지 않습니다. ### Table @@ -303,28 +316,30 @@ W11-F polish phase 가 모든 인터랙티브 transition 의 타이밍 · easing | 쌍 | 비율 | 비고 | |---|---|---| -| `--foreground` on `--background` | 16.97:1 | 본문. AAA. | -| `--foreground` on `--card` | 17.72:1 | 카드 위 본문. AAA. | -| `--muted-foreground` on `--background` | 4.63:1 | Caption · 보조 텍스트. AA. | -| `--muted-foreground` on `--card` | 4.83:1 | 카드 위 caption. AA. | -| `--primary-foreground` on `--primary` | 16.97:1 | Primary 버튼 라벨. AAA. | -| `--destructive-foreground` on `--destructive` | 4.63:1 | Destructive 버튼 라벨. AA. | -| `--ring` on `--background` | 16.97:1 | Focus ring. AAA. | +| `--foreground` on `--background` / `--card` | 16.48:1 | 본문. AAA. | +| `--muted-foreground` on `--background` / `--card` | 6.05:1 | Caption · 보조 텍스트. AA. | +| `--muted-foreground` on `--muted` | 5.74:1 | Tint fill 위 caption. AA. | +| `--primary-foreground` on `--primary` | 6.39:1 | Primary 버튼 라벨. AA. | +| `--secondary-foreground` on `--secondary` | 12.57:1 | Tonal 버튼 라벨. AAA. | +| `--destructive-foreground` on `--destructive` | 4.77:1 | Destructive 버튼 라벨. AA. | +| `--ring` on `--background` | 6.39:1 | Focus ring. AA (UI ≥ 3:1). | -장식용 border (`--border` on `--background`, 1.20:1) 는 **의도적으로 저대비** — 시각 분리용일 뿐 정보성 UI 요소가 아니며 WCAG 1.4.11 이 면제 대상. +장식용 border (`--border` on `--background`, 1.37:1) 는 **의도적으로 저대비** — 시각 분리용일 뿐 정보성 UI 요소가 아니며 WCAG 1.4.11 이 면제 대상. ### Severity 색 접근성 -Severity hex (`#dc2626` / `#ea580c` / `#ca8a04` / `#2563eb` / `#71717a`) 는 브랜드 고정. Light tint 위 **본문 텍스트** 로 쓰이면 2.5:1 까지 떨어져 AA 실패. 해결은 색이 아닌 구조 — severity 톤이 텍스트로 쓰일 때 렌더링 텍스트 색은 같은 Tailwind hue family 의 더 짙은 shade: +Severity hex (`#dc2626` / `#ea580c` / `#ca8a04` / `#0f766e` / `#71717a`) 는 브랜드 고정. Light tint 위 **본문 텍스트** 로 쓰이면 일부는 2.5:1 까지 떨어져 AA 실패. 해결은 색이 아닌 구조 — severity 톤이 텍스트로 쓰일 때 렌더링 텍스트 색은 같은 Tailwind hue family 의 더 짙은 shade: | Tone | Tint 배경 | 텍스트 색 | 대비 | |---|---|---|---| | `critical` | `bg-risk-critical/10` | `text-red-700` (`#b91c1c`) | 5.54:1 | | `high` | `bg-risk-high/10` | `text-orange-800` (`#9a3412`) | 6.47:1 | | `medium` | `bg-risk-medium/15` | `text-yellow-800` (`#854d0e`) | 5.91:1 | -| `low` | `bg-risk-low/10` | `text-blue-700` (`#1d4ed8`) | 5.83:1 | +| `low` | `bg-risk-low/10` | `text-teal-800` (`#115e59`) | 6.59:1 | | `info` | `bg-risk-info/15` | `text-slate-600` (`#52525b`) | 6.41:1 | +Low 토큰 자체(teal-700 `#0f766e`)도 텍스트로 직접 쓰이는 곳에서 AA 를 통과합니다 (흰 배경 5.47:1, 자기 10 % tint 위 4.76:1) — 교체 전 blue-600 보다 약간 높습니다. + **Dot 인디케이터** (SeverityBadge · 차트 범례 · status pill) 는 계속 raw `bg-risk-X` 토큰 사용 — 색 정체성은 그대로, 텍스트 shade 만 어둡게. 참조 구현은 `apps/frontend/src/components/ui/badge.tsx` (W11-H). ### 색이 신호의 단독 수단 아님 @@ -373,8 +388,9 @@ Severity 가 표시되는 모든 곳에서 색은 다음 중 하나와 짝지움 | W12-C | 2026-06-11 | **Craft 격상 — 모션 (CSS-only).** 라우트 전환 진입 페이드(`
` 을 pathname 으로 key, 250 ms), 사이드바 접기 250 ms 정렬, 전역 `prefers-reduced-motion` 가드. 새 의존성 없음(tailwindcss-animate 만). 스켈레톤 문서를 실제 2000 ms `animate-pulse` 로 정정. | | W12-D | 2026-06-12 | **Craft 격상 — 빈 상태 · 로딩 폴리시.** EmptyState 에 레이어드 아이콘 메달리온 + 선택적 `illustration` 슬롯 추가, 신규 `TableRowsSkeleton` 이 Scans · Admin Users 테이블에서 컬럼별 로딩 셀(전폭 바 대체)을 렌더링. | | W12-E/F | 2026-06-12 | **Craft 격상 — 가드레일 + 문서.** `/dev/design-preview` 를 살아있는 컴포넌트 레퍼런스(타이포그래피 · 배지 · 빈 / 로딩 · 피드백)로 확장하고, 기여자 coding standards 에 "프론트엔드 UI" 섹션 추가. 시각 회귀 베이스라인 확장(4 → ~15)은 CI / 운영자 후속 — darwin 개발 머신에서는 올바른 linux 베이스라인을 생성할 수 없음. | +| W13 | 2026-06-12 | **Google AI Studio 재스킨 (TRUSCA).** PR #394 dev 프로토타입을 사용자 검토 후 토큰 승격: 흰 canvas · `#0b57d0` 블루 primary · `#d3e3fd` tonal secondary · `#dadce0` border · radius 스케일 +2 px · 평면 카드 (shadow-sm → no-op, md/lg → Google elevation) · 필 버튼 (`button.tsx` `rounded-full` + 사이드바 nav) · `--risk-low` blue-600 → teal-700 (Low 배지를 블루 primary 와 분리). 레이아웃 밀도 · motion · 타이포 · 다른 severity 색은 불변. 시각 회귀 베이스라인은 머지 후 linux 재생성 필요. | -이전 "BD-style 2015" 미감 (`#0f172a` navy · 순백 canvas · 일관 8 px radius · shadow 없음 · 브라우저 기본 easing) 은 W11 로 완전 은퇴. +W11 의 Vercel + Linear 스킨은 W13 으로 은퇴. 그 이전 "BD-style 2015" 미감은 W11 로 은퇴. ## 참고