--- version: alpha name: Rembric description: >- Dark monochrome zinc design system with the Rembric lime as the single accent for the self-hosted MCP memory dashboard. A Next.js App Router + React 19 surface on Tailwind v4 + shadcn/Radix, with a `--radius` token scale. The canonical identity doc is `.agents/skills/rembric-dashboard-ui/SKILL.md`; the formal OpenSpec spec change is pending. colors: primary: '#c6f24e' on-primary: '#09090b' background: '#09090b' card: '#101012' raised: '#18181b' accent: '#1c1c1f' border: '#1f1f23' input: '#27272a' ring: '#3f3f46' foreground: '#fafafa' foreground-secondary: '#a1a1aa' foreground-muted: '#71717a' warn: '#ff8c00' destructive: 'oklch(0.704 0.191 22.216)' typography: display: fontFamily: Geist fontSize: 3rem fontWeight: 700 lineHeight: 0.95 letterSpacing: -0.025em display-lg: fontFamily: Geist fontSize: 5rem fontWeight: 700 lineHeight: 0.9 letterSpacing: -0.035em h1: fontFamily: Geist fontSize: 2.4rem fontWeight: 700 lineHeight: 1 letterSpacing: -0.02em h2: fontFamily: Geist Mono fontSize: 0.78rem fontWeight: 600 lineHeight: 1.2 letterSpacing: 0.14em h3: fontFamily: Geist Mono fontSize: 0.72rem fontWeight: 600 lineHeight: 1.2 letterSpacing: 0.12em body-md: fontFamily: Geist fontSize: 0.92rem fontWeight: 400 lineHeight: 1.55 body-sm: fontFamily: Geist fontSize: 0.82rem fontWeight: 400 lineHeight: 1.5 label-md: fontFamily: Geist Mono fontSize: 0.72rem fontWeight: 500 lineHeight: 1 letterSpacing: 0.12em label-sm: fontFamily: Geist Mono fontSize: 0.66rem fontWeight: 500 lineHeight: 1 letterSpacing: 0.14em mono-md: fontFamily: Geist Mono fontSize: 0.78rem fontWeight: 400 lineHeight: 1.4 mono-sm: fontFamily: Geist Mono fontSize: 0.7rem fontWeight: 400 lineHeight: 1.4 spacing: xs: 4px sm: 8px md: 12px base: 16px lg: 24px xl: 32px 2xl: 48px 3xl: 64px rounded: button: 10px input: 12px card: 16px components: button-primary: backgroundColor: '{colors.primary}' textColor: '{colors.on-primary}' typography: '{typography.label-md}' rounded: '{rounded.button}' padding: '16px' height: '44px' button-primary-hover: backgroundColor: '{colors.primary}' textColor: '{colors.on-primary}' button-secondary: backgroundColor: '{colors.raised}' textColor: '{colors.foreground}' rounded: '{rounded.button}' padding: '16px' height: '44px' button-warn: backgroundColor: 'transparent' textColor: '{colors.warn}' rounded: '{rounded.button}' padding: '16px' height: '44px' button-warn-hover: backgroundColor: '{colors.warn}' textColor: '{colors.on-primary}' button-danger: backgroundColor: 'transparent' textColor: '{colors.destructive}' rounded: '{rounded.button}' padding: '16px' height: '44px' button-danger-hover: backgroundColor: '{colors.destructive}' textColor: '{colors.on-primary}' button-sm: typography: '{typography.label-sm}' padding: '10px' height: '28px' input: backgroundColor: '{colors.input}' textColor: '{colors.foreground}' typography: '{typography.body-sm}' rounded: '{rounded.input}' padding: '12px' height: '44px' status-pill: backgroundColor: '{colors.raised}' textColor: '{colors.foreground}' typography: '{typography.label-sm}' rounded: '9999px' padding: '8px' stat-card: backgroundColor: '{colors.card}' textColor: '{colors.foreground}' rounded: '{rounded.card}' padding: '24px' card: backgroundColor: '{colors.card}' textColor: '{colors.foreground}' rounded: '{rounded.card}' modal: backgroundColor: '{colors.raised}' textColor: '{colors.foreground}' rounded: '{rounded.card}' padding: '0px' row-hover: backgroundColor: '{colors.accent}' caption: textColor: '{colors.foreground-muted}' typography: '{typography.label-sm}' divider: backgroundColor: '{colors.border}' height: '1px' --- # Rembric — Dashboard Design System Single source of truth for the **current** visual identity and UI patterns served at `/dashboard/*`. The YAML frontmatter above carries the machine-readable design tokens; the prose below tells you **why** those values exist and how to apply them. The canonical, always-current identity doc is `.agents/skills/rembric-dashboard-ui/SKILL.md` (v1.1). This file is the repo-root overview that defers to it and to the live implementations. The identity was **redesigned (2026-09, branch `feat/dashboard-identity-redesign`)**; the previous identity is preserved as history at the end of this file. If you are building or extending a dashboard page, read the whole file before touching code. The recipes at the bottom show the canonical patterns — match them rather than inventing new ones. ## Overview Rembric's identity is **dark monochrome zinc with a single lime accent**. The product is a single-tenant, self-hosted memory + sessions + dashboard for AI coding agents; it should feel operational, terse, and unambiguous — a control room, not a SaaS landing page. Visual choices: - **Dark canvas, one accent.** Near-black zinc surfaces (`#09090b`) carried by a layered surface ladder (`#101012` cards, `#18181b` raised/popover/muted) and 1-px `#1f1f23` borders. Lime (`#c6f24e`) is the **only** accent — it marks primary actions, active state, and the chart's save series, never wallpaper. Amber (`#ff8c00`) and destructive stay for warn / danger tones. - **Geist, self-hosted.** Geist drives UI and display output; Geist Mono owns labels, meta, counters and tabular data. Both are self-hosted woff2 — no CDN at runtime, no second display face. - **Depth without heavy chrome.** Soft shadows only (`shadow-lg shadow-black/20` – `shadow-black/40`) plus **radial lime glows**; hierarchy comes from the surface ladder and 1-px lines, not from hard borders. - **Rounded, restrained geometry.** `--radius: 1rem`; cards `rounded-2xl` (16), inputs `rounded-xl` (12), buttons `rounded-lg` (10), status pills `rounded-full`. Shape is a token decision, never a hard-coded zero. - **Floating shell, no sidebar.** A floating centered command bar replaces the old fixed rail; login and OAuth consent render chrome-free. - **One product, one operator.** No theming, no light mode, no per-user preferences. The surface looks the same for every operator. Emotional response: precise, sober, in-control. The interface should read as a control room — fast to scan, hard to misread. ## Colors The palette is a zinc surface ladder plus a single lime accent, two alert tones, and a zinc chart ladder that uses lime as `chart-1`. - **Primary (#c6f24e — Lime)**: the only "happy" colour. Reserved for primary actions, active nav items, active-state pills, and the chart's save series. - **On-primary (#09090b)**: text colour on lime surfaces, so primary buttons read as lime fill + near-black ink. - **Background (#09090b)**: the canvas — every page and the floating command bar share it. - **Card (#101012)**: the base contained surface for cards, panels, and stat tiles. - **Raised (#18181b)**: raised / popover / secondary / muted surfaces that lift above a card (dropdowns, sheets, secondary buttons, empty pills). - **Accent (#1c1c1f)**: hover and subtle-selected background (row-hover, active menu items). - **Border (#1f1f23)**: 1-px borders on cards, tables, and chrome. - **Input (#27272a)**: form-field background; **Ring (#3f3f46)**: focus rings. - **Foreground (#fafafa)**: primary text. **Foreground-secondary (#a1a1aa)**: secondary copy. **Foreground-muted (#71717a)**: labels, captions, meta. - **Warn (#ff8c00 — Amber)**: reversible-but-cautious actions (abandon, archive, supersede) and the chart's consolidation-ops series. - **Destructive**: irreversible / impactful actions (delete session, revoke token). Kept as the shadcn `--destructive` token. - **Charts (`--chart-1`…`--chart-5`)**: `#c6f24e` → `#a1a1aa` → `#52525b` → `#3f3f46` → `#27272a`; the zinc ladder with lime reserved for the primary series. ### The chrome-only rule On **light** surfaces lime is **chrome only** — fills, borders, dots, and bars. **Never lime text on a light background.** On the dark dashboard lime text is legitimate (active nav item, active-state pill, positive metric), but it stays rationed: one lime decision per screen. ### Implementation Tokens are defined as CSS custom properties in `apps/web/src/app/globals.css` and mapped into Tailwind v4 via `@theme inline`, so utilities such as `bg-background`, `bg-card`, `bg-popover`, `text-foreground`, `text-muted-foreground`, `border-border`, `bg-primary`, `text-warn`, and `text-destructive` resolve to the tokens. Use the utilities and variables, never raw hex. | Token | CSS variable | Tailwind utility | | ---------------------- | --------------------------- | ----------------------------- | | `primary` | `var(--primary)` | `bg-primary` / `text-primary` | | `on-primary` | `var(--primary-foreground)` | `text-primary-foreground` | | `background` | `var(--background)` | `bg-background` | | `card` | `var(--card)` | `bg-card` | | `raised` | `var(--popover)` | `bg-popover` / `bg-secondary` | | `accent` | `var(--accent)` | `bg-accent` | | `border` | `var(--border)` | `border-border` | | `input` | `var(--input)` | `bg-input` | | `ring` | `var(--ring)` | `ring-ring` | | `foreground` | `var(--foreground)` | `text-foreground` | | `foreground-secondary` | (zinc step) | `text-zinc-400` | | `foreground-muted` | `var(--muted-foreground)` | `text-muted-foreground` | | `warn` | `var(--warn)` | `text-warn` | | `destructive` | `var(--destructive)` | `text-destructive` | ## Typography Two families, each with a deliberate role: - **Geist** drives UI and display output — titles, body copy, stat values. It is wired to `--font-sans` and `--font-display`, giving the product a single clean voice instead of a three-face stack. - **Geist Mono** owns labels, counters, meta, and tabular data — table numbers, durations, IDs, code snippets, section labels. Its tabular cadence is what makes the dashboard read as an operator console. Both are vendored as woff2 under `apps/web/src/app/fonts/` and loaded in `apps/web/src/app/layout.tsx` with `next/font/local`. Use the CSS variables, never the family name directly: | Stack | CSS variable | | ------- | --------------------------------------------- | | UI | `var(--font-geist)` → `var(--font-sans)` | | Display | `var(--font-geist)` → `var(--font-display)` | | Mono | `var(--font-geist-mono)` → `var(--font-mono)` | No CDN fonts and no second display face. ## Layout The dashboard is laid out by a **floating command bar over a fluid content column**. There is **no sidebar rail** — the old fixed 196 px rail is gone. ### Shell `apps/web/src/components/dashboard/command-bar.tsx` renders `CommandFrame`: a floating, centered header (`rounded-2xl border bg-card/90 backdrop-blur`) with the primary nav, a "More" dropdown for secondary entries, badge counters, and a mobile `Sheet` drawer. `apps/web/src/app/dashboard/layout.tsx` wraps every authenticated route in it. Nav data lives in `apps/web/src/lib/nav.ts` (`NAV` / `NAV_GROUPS`); add entries there and both the desktop bar and the mobile sheet pick them up. ### Chrome-free screens `/dashboard/login` and `/dashboard/oauth-consent` render **without** the frame. They are listed in `CHROME_FREE_PATHS` in `apps/web/src/lib/nav.ts`, and `CommandFrame` short-circuits on `isChromeFreePath()` — a route renders standalone as soon as it is added to that list. ### Spacing A strict **8-px scale** governs spacing: | Token | Value | Use | | ------ | ----- | ------------------------------ | | `xs` | 4px | inner gaps in compact rows | | `sm` | 8px | between siblings, button gaps | | `md` | 12px | small block padding | | `base` | 16px | default block padding | | `lg` | 24px | section padding, card body | | `xl` | 32px | main vertical rhythm | | `2xl` | 48px | wide-screen horizontal padding | | `3xl` | 64px | page bottom padding | ### Responsive Below **768 px** the command bar collapses to a compact bar with a `Sheet` drawer, page content stacks to a single column, and tables stay scrollable inside their panel rather than pushing page width. Minimum supported viewport: **320 px** with no page-level horizontal scroll. Touch targets stay **≥44 × 44 px** on touch layouts. ## Elevation & Depth Depth is conveyed by **tonal layers, soft shadows, and glows** rather than heavy borders: - **Tonal layers**: canvas `#09090b`; cards `#101012`; raised surfaces `#18181b`; hover accent `#1c1c1f`. Each step is small — felt, not seen. - **Soft shadows only**: `shadow-lg` with `shadow-black/20`–`shadow-black/40`. There is no hard drop-shadow vocabulary and no stacked shadow decoration. - **Radial lime glows**: a faint radial gradient anchors key surfaces, e.g. `bg-[radial-gradient(640px_260px_at_50%_-60px,rgba(198,242,78,0.09),transparent_70%)]`. This is the signature "lit from within" depth cue from the redesign mockups. - **1-px borders** in `#1f1f23` separate chrome and panels where a line is clearer than a tonal step. Hovers shift tone (surface or lime), they do not enlarge or float the element. ## Shapes The shape language is driven by the `--radius` token scale (`--radius: 1rem`) and expressed in Tailwind utilities: | Role | Radius | Utility | | -------------- | ------ | -------------- | | Cards / panels | 16 px | `rounded-2xl` | | Inputs | 12 px | `rounded-xl` | | Buttons | 10 px | `rounded-lg` | | Status pills | full | `rounded-full` | Keep the scale restrained. Character comes from the monochrome palette, the lime accent, the typography, and 1-px lines — not from heavy rounding or mixed radii. Don't hard-code a corner radius; use the scale so shape stays consistent across primitives. ## Components Server components compose the new identity directly. Shared primitives live in `apps/web/src/components/dashboard/` and `apps/web/src/components/ui/`. ### Buttons `Button` (shadcn) drives tone, and tone semantics are **load-bearing**: | Variant | Tone | When to use | | --------------- | ---------- | --------------------------------------------------------------------------------- | | `default` | Lime fill | Confirm / save / sign-in / filter — the affirmative action | | `secondary` | Raised | Cancel, dismissive, navigation | | `outline`/ghost | Borderless | Secondary or low-emphasis controls | | `destructive` | Red | Irreversible (delete session, revoke token, bulk remove) | | warn-toned | Amber | Reversible-but-cautious (abandon session, archive memory, mark relation orphaned) | Primary buttons are **lime fill + near-black text**. Every destructive button (warn/danger) MUST live inside a `` inside its `` so the confirmation dialog opens before submit. ### StatusPill `StatusPill` (`apps/web/src/components/dashboard/ui.tsx`) is the shared, app-wide status pill: a `rounded-full` container with a dot plus a tinted background/border per tone (`TONE_PILL`), driven by the `STATUS_TONE` map (active/judged → lime, abandoned/superseded → amber, archived/ended/pending → dim, orphaned/deleted → danger). Use it for every status; never hand-roll a second badge. ### Spectrum data-table (list views) Every list/table page goes through `DataTable` from `@/components/spectrumui/data-table` with `variant="panel"`. The shared engine provides: - **quick-filter pills** with live counts, - **client search**, - **selectable rows** with a rising **bulk-action bar**, - a contextual **`⋯` row menu** (`rowActions`), - a **detail disclosure** (`renderDetail`), - and `pageSize` for client-side pagination. `apps/web/src/components/dashboard/sessions-table.tsx` is the **canonical implementation** to copy. Siblings in the same pattern: `judgments-table.tsx`, `prompts-table.tsx`, `memories-table.tsx`, `entities-table.tsx`, `tokens-table.tsx`. ### Forms & mutations Every mutation is a **server action through `ActionForm`** (`@/components/dashboard/action-form`). Put a `` (`csrf-field.tsx`) in it and start the action with `guardAction(formData, FORM_NAME)` / `guardFailure(...)` (`@/lib/actions/guard`), which enforces session + admin + CSRF. The action returns `ActionState = { error: string | null }`. Inputs use the shadcn primitives bound to the tokens (`bg-input`, `border-border`, `rounded-xl`). There is no second styling system. ### ConfirmSubmit `ConfirmSubmit` (`confirm-submit.tsx`) wires a Radix `AlertDialog` to the enclosing `ActionForm` via `useActionFormId`, so it only works **inside** an ``, wrapping a `