From 9d86e00784a0d34b2999c848e8137a6bfdf16f0b Mon Sep 17 00:00:00 2001 From: Tobias Studer Date: Tue, 2 Jun 2026 12:10:04 +0200 Subject: [PATCH 01/19] =?UTF-8?q?feat(design):=20P0=20=E2=80=94=20Nothing?= =?UTF-8?q?=20design=20tokens=20&=20font=20stack=20(spec=20028)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-key the shadcn semantic token layer in globals.css onto the Nothing monochrome+red system (light=:root, dark=.dark, next-themes class strategy unchanged). shadcn --accent stays a neutral hover bg; the single red interrupt and all error/destructive states route through --destructive (#d71921, =--ring). Adds ink/faint/success/warning/seg-empty/destructive-subtle tokens registered in @theme inline. Light-mode status colors darkened for WCAG (see implementation notes). Swaps Inter for Space Grotesk (UI) / Space Mono (data) / Doto (hero) via next/font. Build green across all 40 routes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/nothing-design/SKILL.md | 177 ++++ .../nothing-design/references/components.md | 153 +++ .../references/platform-mapping.md | 64 ++ .../nothing-design/references/tokens.md | 142 +++ specs/028-nothing-design-redesign/README.md | 109 ++ .../implementation-notes.html | 163 +++ .../implementation-plan.html | 977 ++++++++++++++++++ .../mockups/_CONTRACT.md | 175 ++++ .../mockups/assignments.html | 562 ++++++++++ .../mockups/budget.html | 436 ++++++++ .../mockups/claude.html | 593 +++++++++++ .../mockups/copilot.html | 425 ++++++++ .../mockups/dashboard.html | 568 ++++++++++ .../mockups/index.html | 236 +++++ .../mockups/invoices.html | 269 +++++ .../mockups/login.html | 175 ++++ .../mockups/reports.html | 633 ++++++++++++ .../mockups/requests.html | 351 +++++++ .../mockups/settings.html | 531 ++++++++++ .../mockups/styles/nothing.css | 463 +++++++++ .../mockups/styles/theme.js | 31 + .../mockups/tools.html | 370 +++++++ .../mockups/users.html | 290 ++++++ src/app/globals.css | 185 ++-- src/app/layout.tsx | 31 +- 25 files changed, 8042 insertions(+), 67 deletions(-) create mode 100644 .claude/skills/nothing-design/SKILL.md create mode 100644 .claude/skills/nothing-design/references/components.md create mode 100644 .claude/skills/nothing-design/references/platform-mapping.md create mode 100644 .claude/skills/nothing-design/references/tokens.md create mode 100644 specs/028-nothing-design-redesign/README.md create mode 100644 specs/028-nothing-design-redesign/implementation-notes.html create mode 100644 specs/028-nothing-design-redesign/implementation-plan.html create mode 100644 specs/028-nothing-design-redesign/mockups/_CONTRACT.md create mode 100644 specs/028-nothing-design-redesign/mockups/assignments.html create mode 100644 specs/028-nothing-design-redesign/mockups/budget.html create mode 100644 specs/028-nothing-design-redesign/mockups/claude.html create mode 100644 specs/028-nothing-design-redesign/mockups/copilot.html create mode 100644 specs/028-nothing-design-redesign/mockups/dashboard.html create mode 100644 specs/028-nothing-design-redesign/mockups/index.html create mode 100644 specs/028-nothing-design-redesign/mockups/invoices.html create mode 100644 specs/028-nothing-design-redesign/mockups/login.html create mode 100644 specs/028-nothing-design-redesign/mockups/reports.html create mode 100644 specs/028-nothing-design-redesign/mockups/requests.html create mode 100644 specs/028-nothing-design-redesign/mockups/settings.html create mode 100644 specs/028-nothing-design-redesign/mockups/styles/nothing.css create mode 100644 specs/028-nothing-design-redesign/mockups/styles/theme.js create mode 100644 specs/028-nothing-design-redesign/mockups/tools.html create mode 100644 specs/028-nothing-design-redesign/mockups/users.html diff --git a/.claude/skills/nothing-design/SKILL.md b/.claude/skills/nothing-design/SKILL.md new file mode 100644 index 00000000..32f7f421 --- /dev/null +++ b/.claude/skills/nothing-design/SKILL.md @@ -0,0 +1,177 @@ +--- +name: nothing-design +description: This skill should be used when the user explicitly says "Nothing style", "Nothing design", "/nothing-design", or directly asks to use/apply the Nothing design system. NEVER trigger automatically for generic UI or design tasks. +version: 3.0.0 +allowed-tools: [Read, Write, Edit, Glob, Grep] +--- + +# Nothing-Inspired UI/UX Design System + +A senior product designer's toolkit trained in Swiss typography, industrial design (Braun, Teenage Engineering), and modern interface craft. Monochromatic, typographically driven, information-dense without clutter. Dark and light mode with equal rigor. + +**Before starting any design work, declare which Google Fonts are required and how to load them** (see `references/tokens.md` Section 1). Never assume fonts are already available. + +--- + +## 1. DESIGN PHILOSOPHY + +- **Subtract, don't add.** Every element must earn its pixel. Default to removal. +- **Structure is ornament.** Expose the grid, the data, the hierarchy itself. +- **Monochrome is the canvas.** Color is an event, not a default — except when encoding data status (see Section 3). +- **Type does the heavy lifting.** Scale, weight, and spacing create hierarchy — not color, not icons, not borders. +- **Both modes are first-class.** Dark mode: OLED black. Light mode: warm off-white. Neither is "derived" — both get full design attention. Ask the user which mode to start with. +- **Industrial warmth.** Technical and precise, but never cold. A human hand should be felt. + +--- + +## 2. CRAFT RULES — HOW TO COMPOSE + +### 2.1 Visual Hierarchy: The Three-Layer Rule + +Every screen has exactly **three layers of importance.** Not two, not five. Three. + +| Layer | What | How | +|-------|------|-----| +| **Primary** | The ONE thing the user sees first. A number, a headline, a state. | Doto or Space Grotesk at display size. `--text-display`. 48–96px breathing room. | +| **Secondary** | Supporting context. Labels, descriptions, related data. | Space Grotesk at body/subheading. `--text-primary`. Grouped tight (8–16px) to the primary. | +| **Tertiary** | Metadata, navigation, system info. Visible but never competing. | Space Mono at caption/label. `--text-secondary` or `--text-disabled`. ALL CAPS. Pushed to edges or bottom. | + +**The test:** Squint at the screen. Can you still tell what's most important? If two things compete, one needs to shrink, fade, or move. + +**Common mistake:** Making everything "secondary." Evenly-sized elements with even spacing = visual flatness. Be brave — make the primary absurdly large and the tertiary absurdly small. The contrast IS the hierarchy. + +### 2.2 Font Discipline + +Per screen, use maximum: +- **2 font families** (Space Grotesk + Space Mono. Doto only for hero moments.) +- **3 font sizes** (one large, one medium, one small) +- **2 font weights** (Regular + one other — usually Light or Medium, rarely Bold) + +Think of it as a budget. Every additional size/weight costs visual coherence. Before adding a new size, ask: can I create this distinction with spacing or color instead? + +| Decision | Size | Weight | Color | +|----------|:---:|:---:|:---:| +| Heading vs. body | Yes | No | No | +| Label vs. value | No | No | Yes | +| Active vs. inactive nav | No | No | Yes | +| Hero number vs. unit | Yes | No | No | +| Section title vs. content | Yes | Optional | No | + +**Rule of thumb:** If reaching for a new font-size, it's probably a spacing problem. Add distance instead. + +### 2.3 Spacing as Meaning + +Spacing is the primary tool for communicating relationships. + +``` +Tight (4–8px) = "These belong together" (icon + label, number + unit) +Medium (16px) = "Same group, different items" (list items, form fields) +Wide (32–48px) = "New group starts here" (section breaks) +Vast (64–96px) = "This is a new context" (hero to content, major divisions) +``` + +**If a divider line is needed, the spacing is probably wrong.** Dividers are a symptom of insufficient spacing contrast. Use them only in data-dense lists where items are structurally identical. + +### 2.4 Container Strategy (prefer top) + +1. **Spacing alone** (proximity groups items) +2. A single divider line +3. A subtle border outline +4. A surface card with background change + +Each step down adds visual weight. Use the lightest tool that works. Never box the most important element — let it float on the background. + +### 2.5 Color as Hierarchy + +In a monochrome system, the gray scale IS the hierarchy. Max 4 levels per screen: + +``` +--text-display (100%) → Hero numbers. One per screen. +--text-primary (90%) → Body text, primary content. +--text-secondary (60%) → Labels, captions, metadata. +--text-disabled (40%) → Disabled, timestamps, hints. +``` + +**Red (#D71921) is not part of the hierarchy.** It's an interrupt — "look HERE, NOW." If nothing is urgent, no red on the screen. + +**Data status colors** (success green, warning amber, accent red) are exempt from the "one accent" rule when encoding data values. Apply color to the **value itself**, not labels or row backgrounds. See `references/tokens.md` for the full color system. + +### 2.6 Consistency vs. Variance + +**Be consistent in:** Font families, label treatment (always Space Mono ALL CAPS), spacing rhythm, color roles, component shapes, alignment. + +**Break the pattern in exactly ONE place per screen:** An oversized number, a circular widget among rectangles, a red accent among grays, a Doto headline, a vast gap where everything else is tight. + +This single break IS the design. Without it: sterile grid. With more than one: visual chaos. + +### 2.7 Compositional Balance + +**Asymmetry > symmetry.** Centered layouts feel generic. Favor deliberately unbalanced composition: +- **Large left, small right:** Hero metric + metadata stack. +- **Top-heavy:** Big headline near top, sparse content below. +- **Edge-anchored:** Important elements pinned to screen edges, negative space in center. + +Balance heavy elements with more empty space, not with more heavy elements. + +### 2.8 The Nothing Vibe + +1. **Confidence through emptiness.** Large uninterrupted background areas. Resist filling space. +2. **Precision in the small things.** Letter-spacing, exact gray values, 4px gaps. Micro-decisions compound into craft. +3. **Data as beauty.** `36GB/s` in Space Mono at 48px IS the visual. No illustrations needed. +4. **Mechanical honesty.** Controls look like controls. A toggle = physical switch. A gauge = instrument. +5. **One moment of surprise.** A dot-matrix headline. A circular widget. A red dot. Restraint makes the one expressive moment powerful. +6. **Percussive, not fluid.** Imagine UI sounds: click not swoosh, tick not chime. Design transitions that feel mechanical and precise. + +### 2.9 Visual Variety in Data-Dense Screens + +When 3+ data sections appear on one screen, vary the visual form: + +| Form | Best for | Weight | +|------|----------|--------| +| Hero number (large Doto/Space Mono) | Single key metric | Heavy — use once | +| Segmented progress bar | Progress toward goal | Medium | +| Concentric rings / arcs | Multiple related percentages | Medium | +| Inline compact bar | Secondary metrics in rows | Light | +| Number-only with status color | Values without proportion | Lightest | +| Sparkline | Trends over time | Medium | +| Stat row (label + value) | Simple data points | Light | + +Lead section → heaviest treatment. Secondary → different form. Tertiary → lightest. The FORM varies, the VOICE stays the same. + +--- + +## 3. ANTI-PATTERNS — WHAT TO NEVER DO + +- No gradients in UI chrome +- No shadows. No blur. Flat surfaces, border separation. +- No skeleton loading screens. Use `[LOADING...]` text or segmented spinner. +- No toast popups. Use inline status text: `[SAVED]`, `[ERROR: ...]` +- No sad-face illustrations, cute mascots, or multi-paragraph empty states +- No zebra striping in tables +- No filled icons, multi-color icons, or emoji as UI +- No parallax, scroll-jacking, or gratuitous animation +- No spring/bounce easing. Use subtle ease-out only. +- No border-radius > 16px on cards. Buttons are pill (999px) or technical (4–8px). +- Data visualization: differentiate with **opacity** (100%/60%/30%) or **pattern** (solid/striped/dotted) before introducing color. + +--- + +## 4. WORKFLOW + +1. **Declare fonts** — tell the user which Google Fonts to load (see `references/tokens.md`) +2. **Ask mode** — dark or light? Neither is default. +3. **Sketch hierarchy** — identify the 3 layers before writing any code +4. **Compose** — apply craft rules (Sections 2.1–2.9) +5. **Check tokens** — consult `references/tokens.md` for exact values +6. **Build components** — consult `references/components.md` for patterns +7. **Adapt to platform** — consult `references/platform-mapping.md` for output conventions + +--- + +## 5. REFERENCE FILES + +For detailed token values, component specs, and platform-specific guidance: + +- **`references/tokens.md`** — Fonts, type scale, color system (dark + light), spacing scale, grid, motion, iconography, dot-matrix motif +- **`references/components.md`** — Cards, buttons, inputs, lists, tables, nav, tags, segmented controls, progress bars, charts, widgets, overlays, state patterns +- **`references/platform-mapping.md`** — HTML/CSS, SwiftUI, React/Tailwind, Paper output conventions diff --git a/.claude/skills/nothing-design/references/components.md b/.claude/skills/nothing-design/references/components.md new file mode 100644 index 00000000..d25768db --- /dev/null +++ b/.claude/skills/nothing-design/references/components.md @@ -0,0 +1,153 @@ +# Nothing Design System — Components + +## 1. CARDS / SURFACES + +- Background: `--surface` or `--surface-raised` +- Border: `1px solid --border`, or none. Radius: 12–16px cards, 8px compact, 4px technical +- Padding: 16–24px. No shadows. Flat surfaces, border separation. + +--- + +## 2. BUTTONS + +| Variant | Background | Border | Text | Radius | +|---------|-----------|--------|------|--------| +| Primary | `--text-display` (#FFF) | none | `--black` | 999px (pill) | +| Secondary | transparent | `1px solid --border-visible` | `--text-primary` | 999px | +| Ghost | transparent | none | `--text-secondary` | 0 | +| Destructive | transparent | `1px solid --accent` | `--accent` | 999px | + +All buttons: `Space Mono`, 13px, ALL CAPS, letter-spacing 0.06em, padding 12px 24px. Min height 44px. + +--- + +## 3. INPUTS + +- Underline preferred (`1px solid --border-visible` bottom) or full border 8px radius +- Label above: `--label` style (Space Mono, ALL CAPS, `--text-secondary`) +- Focus: border → `--text-primary`. Error: border → `--accent`, message below in `--accent` +- Data-entry fields: `Space Mono` for input text + +--- + +## 4. LISTS / DATA ROWS + +- Dividers: `1px solid --border`, full-width. Row padding: 12–16px vertical +- Left: label (Space Mono caps, `--text-secondary`). Right: value (`--text-primary`) +- Never alternating row backgrounds. Use dividers. + +**Stat rows:** Label left (Space Mono, ALL CAPS, `--text-secondary`), value right (color = status color), unit adjacent in `--label` size. Trend arrow same color as value. + +**Hierarchical rows:** Sub-items indented 16–24px, same divider treatment. No tree lines or expand/collapse — indentation IS the hierarchy. + +--- + +## 5. TABLES / DATA GRIDS + +- Header: `--label` style, bottom border `--border-visible` +- Cell text: `Space Mono` numeric, `Space Grotesk` text. Cell padding: 12px 16px +- Numbers right, text left. No zebra striping, no cell backgrounds. +- Active row: `--surface-raised` background, left `2px solid --accent` indicator + +--- + +## 6. NAVIGATION + +- Bottom bar mobile, horizontal text bar desktop +- Labels: Space Mono, ALL CAPS. Active: `--text-display` + dot/underline. Inactive: `--text-disabled` +- Bracket `[ HOME ] GALLERY INFO` or pipe `HOME | GALLERY | INFO` +- **Back button:** Circular 40–44px, `--surface` bg, thin chevron `<`, top-left 16px from edges + +--- + +## 7. TAGS / CHIPS + +- Border: `1px solid --border-visible`, no fill. Text: Space Mono, `--caption`, ALL CAPS +- Radius: 999px (pill) or 4px (technical). Padding: 4px 12px. Active: `--text-display` border+text + +--- + +## 8. SEGMENTED CONTROL + +- Container: `1px solid --border-visible`, pill or 8px rounded +- Active: `--text-display` bg, `--black` text (inverted). Inactive: transparent, `--text-secondary` +- Text: Space Mono, ALL CAPS, `--label` size. Height: 36–44px. Transition: 200ms ease-out +- Max 2–4 segments + +--- + +## 9. DATE / PERIOD NAVIGATION + +- Layout: `< LABEL >` — back arrow, label, forward arrow +- Label: Space Mono/Grotesk, ALL CAPS. Arrows: thin chevrons, `--text-secondary`, 44px touch +- No calendar popovers — linear stepping IS the interaction + +--- + +## 10. TOGGLES / SWITCHES + +- Pill track, circle thumb. Off: `--border-visible` track, `--text-disabled` thumb +- On: `--text-display` track, `--black` thumb. Min touch target: 44px + +--- + +## 11. SEGMENTED PROGRESS BARS + +The signature data visualization. Discrete blocks — mechanical, instrument-like. + +**Anatomy:** Label + value above, full-width bar of discrete rectangular segments with 2px gaps below. + +**Segments:** Square-ended blocks, no border-radius. Filled = solid status color. Empty = `--border` (dark) / `#E0E0E0` (light). + +| State | Fill | When | +|-------|------|------| +| Neutral | `--text-display` | Within normal range | +| Over limit | `--accent` | Exceeds target | +| Good | `--success` | Healthy range | +| Moderate | `--warning` | Caution zone | + +**Overflow:** Filled segments continue past "full" mark in status color (typically red). + +**Sizes:** Hero 16–20px, Standard 8–12px, Compact 4–6px height. + +Always pair with numeric readout. Bar = proportion, number = precision. + +--- + +## 12. OTHER DATA VISUALIZATION + +- **Bar charts:** Vertical, white fill, `--border` remainder. Square ends. +- **Gauges:** Thin stroke circles + tick marks, numeric readout centered/adjacent. +- **Dot grids:** Vary opacity/size for heat maps. Uniform spacing. +- **Category differentiation:** Opacity → pattern → line style → color (last resort). +- Always show numeric value alongside any visual. + +**Charts:** Line 1.5–2px `--text-display`, average dashed 1px `--text-secondary`. Axis labels: Space Mono, `--caption`. Grid: `--border`, horizontal only. No area fill, no legend boxes — label lines directly. + +--- + +## 13. WIDGETS (DASHBOARD CARDS) + +- `--surface` bg, 16px radius. Hero metric: large Doto/Space Mono, left-aligned +- Unit: `--label` size, adjacent. Category: ALL CAPS Space Mono top-left +- Instrument gauges: compass, thermometer, dial motifs + +--- + +## 14. OVERLAYS & LAYERING + +No shadows. Layering through background contrast and borders. + +- **Modals:** Backdrop `rgba(0,0,0,0.8)`, dialog `--surface` + `1px solid --border-visible` + 16px radius, centered max 480px. Close: `[ X ]` top-right ghost button. +- **Bottom sheets:** `--surface`, 2px handle bar centered, 16px top radius, drag-to-dismiss. Full-page sheets: title centered + dismiss button right, sections with `--text-secondary` headings. +- **Dropdowns:** `--surface-raised`, `1px solid --border-visible` 8px radius, 44px items. Selected: left 2px accent bar. No shadow. +- **Toasts:** None. Use inline status text: `[SAVED]`, `[ERROR: ...]`. Space Mono, `--caption`, near trigger. + +--- + +## 15. STATE PATTERNS + +- **Error:** Input border → `--accent` + message below. Form-level: summary box `1px solid --accent`. Inline: `[ERROR]` prefix. Never red backgrounds or alert banners. +- **Empty:** Centered, 96px+ padding. Headline `--text-secondary`, 1 sentence description `--text-disabled`. Optional dot-matrix illustration. No mascots. +- **Loading:** Segmented spinner (hardware-style), or segmented bar + percentage. No skeletons — use `[LOADING]` bracket text. +- **Disabled:** Opacity 0.4 or `--text-disabled`. Borders fade to `--border`. diff --git a/.claude/skills/nothing-design/references/platform-mapping.md b/.claude/skills/nothing-design/references/platform-mapping.md new file mode 100644 index 00000000..5d5201ab --- /dev/null +++ b/.claude/skills/nothing-design/references/platform-mapping.md @@ -0,0 +1,64 @@ +# Nothing Design System — Platform Mapping + +## 1. HTML / CSS / WEB + +Load fonts via Google Fonts `` or `@import`. Use CSS custom properties, `rem` for type, `px` for spacing/borders. Dark/light via `prefers-color-scheme` or class toggle. + +```css +:root { + --black: #000000; + --surface: #111111; + --surface-raised: #1A1A1A; + --border: #222222; + --border-visible: #333333; + --text-disabled: #666666; + --text-secondary: #999999; + --text-primary: #E8E8E8; + --text-display: #FFFFFF; + --accent: #D71921; + --accent-subtle: rgba(215,25,33,0.15); + --success: #4A9E5C; + --warning: #D4A843; + --interactive: #5B9BF6; + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + --space-xl: 32px; + --space-2xl: 48px; + --space-3xl: 64px; + --space-4xl: 96px; +} +``` + +--- + +## 2. SWIFTUI / iOS + +Register fonts in Info.plist, bundle `.ttf` files. Use `@Environment(\.colorScheme)` for mode switching. + +```swift +extension Color { + static let ndBlack = Color(hex: "000000") + static let ndSurface = Color(hex: "111111") + static let ndSurfaceRaised = Color(hex: "1A1A1A") + static let ndBorder = Color(hex: "222222") + static let ndBorderVisible = Color(hex: "333333") + static let ndTextDisabled = Color(hex: "666666") + static let ndTextSecondary = Color(hex: "999999") + static let ndTextPrimary = Color(hex: "E8E8E8") + static let ndTextDisplay = Color.white + static let ndAccent = Color(hex: "D71921") + static let ndSuccess = Color(hex: "4A9E5C") + static let ndWarning = Color(hex: "D4A843") + static let ndInteractive = Color(hex: "5B9BF6") +} +``` + +Light mode values in tokens.md Dark/Light table. Derive Font extension from font stack table (trivial: `.custom("Doto"/"SpaceGrotesk-Regular"/"SpaceMono-Regular", size:)`). + +--- + +## 3. PAPER (DESIGN TOOL) + +Use `get_font_family_info` to verify fonts before writing styles. Direct hex values (no CSS variables). Dark mode as default canvas, light mode as separate artboard. diff --git a/.claude/skills/nothing-design/references/tokens.md b/.claude/skills/nothing-design/references/tokens.md new file mode 100644 index 00000000..38578adc --- /dev/null +++ b/.claude/skills/nothing-design/references/tokens.md @@ -0,0 +1,142 @@ +# Nothing Design System — Tokens + +## 1. TYPOGRAPHY + +### Font Stack + +| Role | Font | Fallback | Weight | +|------|------|----------|--------| +| **Display** | `"Doto"` | `"Space Mono", monospace` | 400–700, variable dot-size | +| **Body / UI** | `"Space Grotesk"` | `"DM Sans", system-ui, sans-serif` | Light 300, Regular 400, Medium 500, Bold 700 | +| **Data / Labels** | `"Space Mono"` | `"JetBrains Mono", "SF Mono", monospace` | Regular 400, Bold 700 | + +**Why these fonts:** Doto = variable dot-matrix (closest to NDot 57). Space Grotesk + Space Mono by Colophon Foundry — same foundry as Nothing's actual typefaces. Shared design DNA. + +### Type Scale + +| Token | Size | Line Height | Letter Spacing | Use | +|-------|------|-------------|----------------|-----| +| `--display-xl` | 72px | 1.0 | -0.03em | Hero numbers, time displays | +| `--display-lg` | 48px | 1.05 | -0.02em | Section heroes, percentages | +| `--display-md` | 36px | 1.1 | -0.02em | Page titles | +| `--heading` | 24px | 1.2 | -0.01em | Section headings | +| `--subheading` | 18px | 1.3 | 0 | Subsections | +| `--body` | 16px | 1.5 | 0 | Body text | +| `--body-sm` | 14px | 1.5 | 0.01em | Secondary body | +| `--caption` | 12px | 1.4 | 0.04em | Timestamps, footnotes | +| `--label` | 11px | 1.2 | 0.08em | ALL CAPS monospace labels | + +### Typographic Rules + +- **Doto:** 36px+ only, tight tracking, never for body text +- **Labels:** Always Space Mono, ALL CAPS, 0.06–0.1em spacing, 11–12px ("instrument panel" labels) +- **Data/Numbers:** Always Space Mono. Units as `--label` size, slightly raised, adjacent +- **Hierarchy:** display (Doto) > heading (Space Grotesk) > label (Space Mono caps) > body (Space Grotesk). Four levels max. + +--- + +## 2. COLOR SYSTEM + +### Primary Palette (Dark Mode) + +| Token | Hex | Contrast on #000 | Role | +|-------|-----|-------------------|------| +| `--black` | `#000000` | — | Primary background (OLED) | +| `--surface` | `#111111` | 1.3:1 | Elevated surfaces, cards | +| `--surface-raised` | `#1A1A1A` | 1.5:1 | Secondary elevation | +| `--border` | `#222222` | — | Subtle dividers (decorative only) | +| `--border-visible` | `#333333` | — | Intentional borders, wireframe lines | +| `--text-disabled` | `#666666` | 4.0:1 | Disabled text, decorative elements | +| `--text-secondary` | `#999999` | 6.3:1 | Labels, captions, metadata | +| `--text-primary` | `#E8E8E8` | 16.5:1 | Body text | +| `--text-display` | `#FFFFFF` | 21:1 | Headlines, hero numbers | + +### Accent & Status Colors + +| Token | Hex | Usage | +|-------|-----|-------| +| `--accent` | `#D71921` | Signal light: active states, destructive, urgent. One per screen as UI element. Never decorative. | +| `--accent-subtle` | `rgba(215,25,33,0.15)` | Accent tint backgrounds | +| `--success` | `#4A9E5C` | Confirmed, completed, connected | +| `--warning` | `#D4A843` | Caution, pending, degraded | +| `--error` | `#D71921` | Shares accent red — errors ARE the accent moment | +| `--info` | `#999999` | Uses secondary text color | +| `--interactive` | `#007AFF` / `#5B9BF6` | Tappable text: links, picker values. Not for buttons. | + +**Data status colors:** `--success` = good/in range, `--warning` = moderate/attention, `--accent` = bad/over limit, `--text-primary` = neutral. Apply color to **value**, not label or background. Labels stay `--text-secondary`. Trend arrows inherit value color. + +### Dark / Light Mode + +| Token | Dark | Light | +|-------|------|-------| +| `--black` | `#000000` | `#F5F5F5` | +| `--surface` | `#111111` | `#FFFFFF` | +| `--surface-raised` | `#1A1A1A` | `#F0F0F0` | +| `--border` | `#222222` | `#E8E8E8` | +| `--border-visible` | `#333333` | `#CCCCCC` | +| `--text-disabled` | `#666666` | `#999999` | +| `--text-secondary` | `#999999` | `#666666` | +| `--text-primary` | `#E8E8E8` | `#1A1A1A` | +| `--text-display` | `#FFFFFF` | `#000000` | +| `--interactive` | `#5B9BF6` | `#007AFF` | + +**Identical across modes:** Accent red, status colors, ALL CAPS labels, fonts, type scale, spacing, component shapes. + +**Dark feel:** Instrument panel in a dark room. OLED black, white data glowing. +**Light feel:** Printed technical manual. Off-white paper (#F5F5F5), black ink. Cards = `#FFFFFF` on off-white page = subtle elevation without shadows. + +--- + +## 3. SPACING + +### Spacing Scale (8px base) + +| Token | Value | Use | +|-------|-------|-----| +| `--space-2xs` | 2px | Optical adjustments only | +| `--space-xs` | 4px | Icon-to-label gaps, tight padding | +| `--space-sm` | 8px | Component internal spacing | +| `--space-md` | 16px | Standard padding, element gaps | +| `--space-lg` | 24px | Group separation | +| `--space-xl` | 32px | Section margins | +| `--space-2xl` | 48px | Major section breaks | +| `--space-3xl` | 64px | Page-level vertical rhythm | +| `--space-4xl` | 96px | Hero breathing room | + +--- + +## 4. MOTION & INTERACTION + +- **Duration:** 150–250ms micro, 300–400ms transitions +- **Easing:** `cubic-bezier(0.25, 0.1, 0.25, 1)` — subtle ease-out. No spring/bounce. +- Prefer opacity over position. Elements fade, don't slide. +- Hover: border/text brightens. No scale, no shadows. +- No parallax, scroll-jacking, gratuitous animation. + +--- + +## 5. ICONOGRAPHY + +- Monoline, 1.5px stroke, no fill. 24x24 base, 20x20 live area. Round caps/joins. +- Color inherits text color. Max 5–6 strokes. +- Preferred: Lucide (thin), Phosphor (thin). Never filled or multi-color. + +--- + +## 6. DOT-MATRIX MOTIF + +**When to use:** Hero typography (Doto), decorative grid backgrounds, dot-grid data viz, loading indicators, empty state illustrations. + +### CSS Implementation +```css +.dot-grid { + background-image: radial-gradient(circle, var(--border-visible) 1px, transparent 1px); + background-size: 16px 16px; +} +.dot-grid-subtle { + background-image: radial-gradient(circle, var(--border) 0.5px, transparent 0.5px); + background-size: 12px 12px; +} +``` + +Dots 1–2px, uniform 12–16px grid. Opacity 0.1–0.2 for backgrounds, full for data. Never as container border or button style. diff --git a/specs/028-nothing-design-redesign/README.md b/specs/028-nothing-design-redesign/README.md new file mode 100644 index 00000000..850bd21e --- /dev/null +++ b/specs/028-nothing-design-redesign/README.md @@ -0,0 +1,109 @@ +# 028 — Nothing Design Redesign + +## Overview + +This spec proposes replacing the AI Developer Hub's current, inconsistent theming with one +coherent visual system inspired by Nothing's instrument-panel / printed-manual aesthetic. The +existing UI leans on a green-primary shadcn theme (`--primary: oklch(0.78 0.19 120)`) applied +unevenly across screens, with mixed type, ad-hoc accent usage, and shadow-heavy surfaces. + +**Goal:** a single source of truth — `mockups/styles/nothing.css` — that styles every screen with +the same monochrome canvas, one disciplined red interrupt, a type-driven hierarchy, and flat +bordered surfaces. Both dark and light are first-class, not an afterthought. Dark reads as an +instrument panel; light reads as a printed manual. + +The `mockups/` folder contains a standalone, framework-free HTML mockup per screen plus a gallery +index, all sharing `nothing.css`. These are static design references — the eventual implementation +maps these tokens onto the app's real `src/app/globals.css`. + +## Font declaration + +Three families, each with one job. Maximum two families visible per screen plus a single Doto hero +moment. + +| Role | Family | Usage | +| --- | --- | --- | +| **Display / hero** | **Doto** | Hero display numbers and titles only, 36px+. | +| **UI / body** | **Space Grotesk** | All interface chrome, headings, and prose. | +| **Labels + all data** | **Space Mono** | ALL-CAPS tracked labels, plus every number / metric / table value. | + +Exact `@import` line used at the top of `mockups/styles/nothing.css`: + +```css +@import url('https://fonts.googleapis.com/css2?family=Doto:wght@400;500;700&family=Space+Grotesk:wght@300;400;500;700&family=Space+Mono:wght@400;700&display=swap'); +``` + +These are exposed as tokens: + +```css +--font-display: "Doto", "Space Mono", monospace; +--font-ui: "Space Grotesk", "DM Sans", system-ui, sans-serif; +--font-mono: "Space Mono", "JetBrains Mono", "SF Mono", monospace; +``` + +## How to view + +Open **`mockups/index.html`** in a browser. It is a self-contained gallery — no build step, no +server. Each card links to a sibling screen mockup. Use the Dark / Light toggle (top-right of every +page) to verify both modes; the choice persists across pages via `localStorage`. + +## Screens + +| # | Screen | Route | Mockup | +| --- | --- | --- | --- | +| 01 | Dashboard | `/` | `mockups/dashboard.html` | +| 02 | AI Tools | `/tools` | `mockups/tools.html` | +| 03 | Users | `/users` | `mockups/users.html` | +| 04 | License Assignments | `/assignments` | `mockups/assignments.html` | +| 05 | Access Requests | `/requests` | `mockups/requests.html` | +| 06 | Budget | `/budget` | `mockups/budget.html` | +| 07 | Reports | `/reports` | `mockups/reports.html` | +| 08 | GitHub Copilot | `/copilot` | `mockups/copilot.html` | +| 09 | Claude Console | `/claude` | `mockups/claude.html` | +| 10 | Invoices | `/invoices` | `mockups/invoices.html` | +| 11 | Settings | `/settings` | `mockups/settings.html` | +| 12 | Sign In | `/login` | `mockups/login.html` | + +## Nothing principles applied + +- **Monochrome canvas + one red interrupt.** Greyscale carries the entire hierarchy. The `--accent` + red (`#d71921`) appears at most once per screen — an active state, a destructive action, or the + single urgent number. If nothing is urgent, there is no red. +- **Type-driven 3-layer hierarchy.** Exactly three layers per screen: one large primary (hero number + or title), secondary context, and small tertiary metadata. The size contrast *is* the hierarchy — + no boxes or color needed to separate them. +- **Flat bordered surfaces / no shadows.** Cards and panels are defined by 1px borders and spacing, + never drop shadows, gradients, or blur. Status colors apply to value text only, never to row + backgrounds or labels. +- **Segmented-bar data viz.** The signature visualization is a segmented progress bar (~20 discrete + segments) rather than smooth fills; overflow segments turn red. Charts are thin inline SVG lines, + labeled directly with no legend boxes or area fills. +- **Both modes first-class.** Every value is a token — no hardcoded `#fff` / `#000`. Flipping the + toggle yields two intentional looks: dark = instrument panel, light = printed manual. + +## Migration notes + +`mockups/styles/nothing.css` is authored as the proposed replacement for the app's +`src/app/globals.css` design tokens. The Nothing tokens map directly onto the existing shadcn token +names, so adoption is largely a values swap (plus wiring the three font families and switching the +dark-mode selector). The headline change is retiring the **oklch green primary** +(`oklch(0.78 0.19 120)`) in favor of a monochrome canvas with a single red interrupt. + +Key token swaps (current `globals.css` → proposed Nothing equivalent): + +| shadcn token | Current value | Nothing equivalent (dark / light) | +| --- | --- | --- | +| `--background` | `oklch(0.16 0 0)` dark · `oklch(1 0 0)` light | `--black` `#000000` / `#f5f5f5` | +| `--foreground` | `oklch(0.97 0 0)` dark · `oklch(0.145 0 0)` light | `--text-primary` `#e8e8e8` / `#1a1a1a` (display text → `--text-display` `#ffffff` / `#000000`) | +| `--card` / `--popover` | `oklch(0.21 0 0)` dark · `oklch(1 0 0)` light | `--surface` `#111111` / `#ffffff` (raised → `--surface-raised` `#1a1a1a` / `#f0f0f0`) | +| `--primary` | `oklch(0.78 0.19 120)` **green** (both modes) | `--text-display` (monochrome) — primary actions become high-contrast greyscale, **not** a brand hue | +| `--accent` | `oklch(0.25 0.03 120)` / `oklch(0.95 0.03 120)` green-tinted | `--accent` `#d71921` red interrupt (subtle fill → `--accent-subtle`) | +| `--destructive` | `oklch(0.704 0.191 22.216)` / `oklch(0.577 0.245 27.325)` | `--error` `#d71921` (unified with the single accent red) | +| `--border` / `--input` | `oklch(1 0 0 / 10%)` dark · `oklch(0.91 0 0)` light | `--border` `#222222` / `#e8e8e8` (visible → `--border-visible` `#333333` / `#cccccc`) | +| `--ring` | `oklch(0.78 0.19 120)` green | `--border-visible` / `--text-primary` focus, no green ring | +| Fonts | shadcn default (system / Geist) | `--font-ui` Space Grotesk, `--font-mono` Space Mono, `--font-display` Doto (see `@import` above) | + +Status colors (`--success #4a9e5c`, `--warning #d4a843`) are new value-text-only tokens with no +direct shadcn equivalent; the existing rainbow `--chart-1..5` would collapse to greyscale + +segmented bars. The app's `.dark` class selector maps to the mockup's `:root` (dark) default and +`:root[data-theme="light"]` override pattern. diff --git a/specs/028-nothing-design-redesign/implementation-notes.html b/specs/028-nothing-design-redesign/implementation-notes.html new file mode 100644 index 00000000..9ca516ec --- /dev/null +++ b/specs/028-nothing-design-redesign/implementation-notes.html @@ -0,0 +1,163 @@ + + + + + + AI Developer Hub — Nothing Redesign · Implementation Notes + + + + + +
+
+
+ SPEC 028 · IMPLEMENTATION NOTES +

Nothing Redesign — Build Log

+
+
+ + +
+
+ +

A running record of how the implementation interprets, extends, or departs from + spec 028 and its implementation-plan.html. Updated continuously as phases land. + Categories: decisions, deviations, + tradeoffs, open questions.

+ + +
+
00

Phase status

+
+
P0 · Tokens & fontsDONE · build green
+
P1 · Primitives & overlaysPENDING
+
P2 · App shell & layoutPENDING
+
P3 · Shared tables & chartsPENDING
+
P4 · Page migrationPENDING
+
P5 · QA gate & PRPENDING
+
+
+ + +
+
01

Design decisions

+

Choices made where the spec or plan left room for interpretation.

+ +
+ Decision + P0shadcn --accent stays NEUTRAL; the Nothing red routes through --destructive +

The plan's flagged naming collision: shadcn's --accent is a subtle hover background used + all over (bg-accent, data-[state]:bg-accent), NOT the loud Nothing red. + So --accent is mapped to a neutral surface (#f0f0f0 light / #1a1a1a + dark) and the single red interrupt + every error/destructive state route through + --destructive = #d71921 (also --ring). This dodges the collision + cleanly and means re-skinned primitives keep their hover semantics. New tokens shadcn lacks were added + and registered in @theme inline: --color-ink (100% display text), + --color-faint (40% tertiary), --color-success, --color-warning, + --color-seg-empty, --color-destructive-subtle, --color-border-strong.

+
+ +
+ Decision + P0Direct per-mode token assignment (two blocks), not a raw-token+alias indirection +

Values are assigned straight onto the shadcn var names per mode — light on :root, dark on + .dark — mirroring the file's original two-block shape. This keeps the diff legible and the + theme mechanism identical to before (next-themes attribute="class", defaultTheme + and system retained). --radius drops to 0.5rem (technical scale); + cards get an explicit 14px in P1. The color vocabulary for downstream phases: + text-ink (hero/headings, 100%), text-foreground (body, 90%), + text-muted-foreground (labels, 60%), text-faint (tertiary, 40%), + bg-primary/text-primary-foreground (greyscale fill), + text/border/bg-destructive (the one red), text-success/text-warning + (value text only).

+
+ +
+ Decision + P0next/font variable names are font-specific, not --font-sans directly +

To avoid a circular --font-sans: var(--font-sans) in Tailwind v4's @theme inline, + next/font injects --font-space-grotesk / --font-space-mono / --font-doto, + and @theme inline chains --font-sans/--font-mono/--font-display + to those (with fallbacks). The mockup vocabulary alias --font-ui is also defined + (= var(--font-sans)) so any ported Nothing class keeps resolving.

+
+ +
+ Decision + P0Nothing component classes become React components, not global CSS classes +

The mockups use generic class names (.card, .grid, .label, + .table, .row). Dumping those into globals.css would collide with + Tailwind utilities (notably .grid) and arbitrary classNames. Instead the app + keeps using re-skinned shadcn primitives (Button, Card, Badge, …) plus a small set of new, clearly-named + React components for Nothing-specific idioms (<SegmentedBar>, <Metric>, + <StatusText>, <LoadingState>, <PeriodNav>). Only + non-colliding type/utility helpers are added globally. This is the idiomatic React/Tailwind path and + keeps the mockup look without the collision risk.

+
+
+ + +
+
02

Deviations

+

Intentional departures from the spec/plan, with rationale.

+ +
+ Deviation + P0Light-mode status colors darkened for WCAG, unlike the mockups +

The mockups (and nothing.css) use the same --success #4a9e5c / + --warning #d4a843 in both modes. As value text on the light canvas those fail the + plan's own P5 a11y gate (≥4.5:1): amber #d4a843 on #f5f5f5 is ~1.9:1 and the + green is ~3:1. Since the spec mandates status colors be value-text-only AND demands ≥4.5:1, I kept the + spec values in dark mode (they pass on black) and darkened the light-mode values to + --success #2e7d32 and --warning #946c00 (both ≥4.5:1 on the light canvas). + The red #d71921 needs no per-mode variant (passes both). Trade: light-mode warning reads + more olive than the mockup amber. Contrast to be re-confirmed with axe/Lighthouse in P5; revert if you + prefer literal mockup hues over the a11y gate.

+
+
+ + +
+
03

Tradeoffs

+

Alternatives considered and why the chosen path won.

+

— none recorded yet —

+
+ + +
+
04

Open questions

+

Things worth a confirm or a later revision.

+

— none recorded yet —

+
+ +
+

AI Developer Hub · Spec 028 — Nothing Design Redesign · Implementation Notes · + Styled with the Nothing design system (tokens only). Dark default; flip the toggle top-right.

+
+ + diff --git a/specs/028-nothing-design-redesign/implementation-plan.html b/specs/028-nothing-design-redesign/implementation-plan.html new file mode 100644 index 00000000..43294635 --- /dev/null +++ b/specs/028-nothing-design-redesign/implementation-plan.html @@ -0,0 +1,977 @@ + + + + + + AI Developer Hub — Nothing Redesign · Implementation Plan + + + + + +
+ + + + +
+
+
+ SPEC 028 · IMPLEMENTATION PLAN +

Nothing Design Redesign

+
+
+ + +
+
+ +

Migrate the AI Developer Hub from a stock shadcn/ui oklch system to one coherent + Nothing design system — monochrome canvas, a single red interrupt, Space Grotesk / + Space Mono / Doto, flat surfaces, inline status text. This document is itself styled with + nothing.css: we eat our own dog food.

+ + +
+
+ 00 +

Overview & goals

+
+ +

The redesign is a presentation-layer migration of ~40 routes and 14 subsystems + to the Nothing design language. No server actions, data shapes, or business logic change. The + dominant cost drivers are the toast removal (~183 toast.* call sites), the + ~20 Recharts components, and the page-by-page Card→.card / + status→.tag / form→.field conversion across every route.

+ +

Why migrate

+
    +
  • One coherent, intentional system in both dark and light, instead of stock shadcn defaults that drift per screen.
  • +
  • Instrument-panel hierarchy: greyscale carries the structure; color is reserved as an interrupt, not decoration.
  • +
  • Remove accessibility/UX anti-patterns already proven present in the codebase: toast-only feedback, skeleton shimmer, red alert banners, calendar popovers, multi-hue charts.
  • +
  • Retire the drifting green: the app's lime --primary oklch(0.78 0.19 120) becomes greyscale fills, and the single Nothing interrupt reuses the red the app already ships as --destructive — so the accent isn't a new color, just a re-scoping of existing tokens.
  • +
+ +

Current state

+
    +
  • Tokens: oklch shadcn "new-york" semantic vars in src/app/globals.css; light is the :root default, dark is the .dark override via next-themes attribute="class", defaultTheme="system".
  • +
  • Accent: --primary = green/chartreuse, reused as --ring, --chart-1, --sidebar-primary. --destructive is a separate red.
  • +
  • Font: Inter via next/font/google applied globally to <body>; no monospace data font, no display font.
  • +
  • Feedback: Sonner <Toaster/> mounted in layout; toasts everywhere. Skeleton loaders in 15 loading.tsx route files. A destructive shadcn alert banner in the global layout. shadcn Sidebar shell with Lucide row icons + filled active background.
  • +
+ +

Target state

+
    +
  • One Nothing token layer in globals.css, re-keyed onto the existing :root(light)/.dark(dark) selectors so next-themes keeps working unchanged. shadcn semantic var names stay aliased to Nothing tokens so no primitive loses color.
  • +
  • Three-family type stack (Doto / Space Grotesk / Space Mono) via next/font; ALL-CAPS Space Mono labels, Space Mono numbers, Doto for 36px+ hero numbers only.
  • +
  • CSS-grid .app shell, text-nav sidebar with a 2px red active bar, topbar eyebrow + title + actions, inline [SAVED]/[ERROR] status, [LOADING…], segmented bars and direct-labeled charts.
  • +
+ +
+
How to read this doc & the mockups
+

The 12 mockups demonstrate layout and hierarchy and render the final accent: + the stock Nothing red #d71921, linked from nothing.css + verbatim. The brand accent was evaluated — Unic lime #A4C400 was considered + and rejected (see §01) — and the decision is to keep Nothing red, so + the mockups already show the target. Each section links the mockup(s) that map to the screens + it covers.

+
+ + +
+ + +
+
+ 01 +

Design foundation & color decisions

+
+ +

The token layer is the foundational dependency for every other subsystem. The + migration re-keys the Nothing value sets onto the app's existing :root(light) / + .dark(dark) selectors — do not copy nothing.css's + :root[data-theme="light"] selectors verbatim. Keep the next-themes class strategy.

+ +

Token migration map

+

Every current oklch shadcn token re-points to a Nothing token (kept aliased so + bg-background, text-muted-foreground, etc. keep resolving).

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Current shadcn token (oklch)Nothing tokenNotes
--backgroundvar(--black)Canvas. Dark #000 / light #F5F5F5.
--foregroundvar(--text-primary)Body text. #e8e8e8 dark / #1a1a1a light.
--card / --popovervar(--surface) / var(--surface-raised)Cards → surface; dropdowns/popovers → surface-raised. No translucency (bg-card/60 dropped).
--card-foreground / --popover-foregroundvar(--text-primary)Plus add --text-display for titles/strong cells (shadcn lacks this).
--primaryvar(--text-display) (fills) + --accent for the one interruptPrimary fills become greyscale white/black per Nothing; red is reserved for the single per-screen interrupt. --primary-foreground--black.
--secondaryvar(--surface-raised) / transparent + 1px borderCollapse shadcn secondary(fill)+outline onto one bordered .btn--secondary.
--muted / --muted-foregroundvar(--surface) / var(--text-secondary)Muted backgrounds become neutral surfaces; muted text → secondary.
--accent (shadcn hover tint)var(--surface-raised) (neutral)Naming collision: shadcn --accent is a subtle hover bg, NOT the Nothing loud interrupt. Keep bg-accent neutral; reserve the Nothing red interrupt under a deliberately-scoped token. Audit all bg-accent/data-[state]:bg-accent usages.
--destructivevar(--accent) = #D71921Unified with --accent — errors ARE the red accent (Nothing default). A --danger alias = --accent is kept only for readable code naming.
--border / --inputvar(--border) / var(--border-visible)--border for dividers; --border-visible for interactive edges/inputs.
--ringvar(--accent) (both modes)Focus moves from 3px ring to border-color brighten; red passes in both modes, so no per-mode variant.
--chart-1var(--text-display)chart-1 == primary today; re-derive as monochrome top series. Red reserved for over/active.
--chart-2..5greys via opacity ramp (--text-secondary, --border-visible)Drop the multi-hue palette + duplicated 8-color spectral FALLBACK_PALETTE. var(--destructive) chart usages → the --accent red.
--radius (0.625rem)--radius-tech 4 / --radius-compact 8 / --radius-card 14 / --radius-pillCap card radius at 14px (≤16px rule); pills for buttons/tags.
--sidebar-* (primary/accent/...)Nothing nav tokens--sidebar-primary == primary today → active nav becomes --text-display text + 2px --accent left bar.
+
+ +

Brand accent decision confirmed: Nothing red

+

The single interrupt stays the stock Nothing red #D71921 — the + mockups already render it. Error and destructive states share this same red, per the Nothing + default "errors ARE the accent moment": no separate danger color, no per-mode variant.

+ +
+
--accent#D71921 · the one interrupt (also errors)
+
--success#4A9E5C · in-range data
+
--warning#D4A843 · attention data
+
+ +
    +
  • --accent = #D71921 is the single interrupt: active nav indicator (2px left bar), the one emphasis/active-state moment per screen, segmented-bar "over/active", table active-row rail, selected dropdown/command item left bar, brand dot, error/destructive surfaces, ::selection (with --black text). Hold the rule: at most ONE red moment per screen.
  • +
  • Errors share the accent: btn--destructive, .tag--accent, .field.is-error, .error-msg, .status-text--err and the active-tab underline all resolve to the same --accent red — nothing.css verbatim, nothing to decouple. (A --danger alias = --accent may be kept purely for readable code naming.)
  • +
  • Rejected alternative — Unic lime #A4C400: brand-aligned and matches the app's current green --primary, but it would force a light-mode a11y workaround (lime fails ~2:1 on #F5F5F5, needing a darkened ~#4F6800 for any accent-as-text/border) and a danger-red decoupling so delete/error states don't read as lime. Rejected in favor of one color and one contrast story.
  • +
+ +

Contrast — red passes both modes

+

#D71921 needs no per-mode variant: it clears WCAG as text and as a graphical + element on both canvases, and as a fill it carries contrast through white/black text. There is + exactly ONE accent color to verify.

+ +
+
+ As text / thin border — pass +
+
#D71921 on #F5F5F5 (light)~4.76:1 · PASS
+
#D71921 on #000 (dark)~4.05:1 · PASS
+
#D71921 on #111 surface≥3:1 · graphical OK
+
+
+
+ As fill — text carries it +
+
#FFF text on #D71921 fill~5.2:1 · PASS
+
fill vs canvas (graphical)≥3:1 · OK
+
identical dark + lightno variant
+
+
+
+ +
    +
  • Use #D71921 verbatim in both modes for text, thin borders (2px nav bar, tab underline, table rail, .tag--active), fills, and strokes — no darkened light-mode variant needed.
  • +
  • --accent-subtle stays an rgba of #D71921 (already correct in nothing.css for both modes); no recompute.
  • +
  • Because errors share the accent, P5 verifies one accent color in light mode — no separate lime/danger contrast matrix.
  • +
  • Status colors (--success / --warning) stay on value text only, never on labels or backgrounds.
  • +
+ +

Fonts

+

Replace Inter with the three-family Nothing stack via next/font/google + (self-hosted, auto fallback-metrics) — not the mockup's render-blocking + Google @import.

+
+ + + + + + + +
FamilyRoleLoader configToken
Space GroteskUI / body (new default)weight ['300','400','500','700'], variable:'--font-sans', display:'swap', preload:true--font-sans (alias --font-ui)
Space MonoALL labels + ALL numbers/dataweight ['400','700'], variable:'--font-mono', display:'swap'--font-mono
DotoHero display 36px+ ONLYweight ['400','500','700'] (pin if variable axis unreliable), variable:'--font-display', display:'swap', preload:false--font-display
+
+
    +
  • Register --font-sans/--font-mono/--font-display in @theme inline (Tailwind v4, no tailwind.config) so font-sans/font-mono utilities AND the Nothing .ui/.mono/.doto classes both resolve.
  • +
  • Naming reconciliation: mockups use --font-ui; Tailwind expects --font-sans. Define both, with --font-ui: var(--font-sans). Chain tokens to the next/font-injected var name (not a raw string) to preserve fallback metrics.
  • +
  • The 28 existing font-mono call sites (18 files) re-point automatically to Space Mono once the token is wired — no call-site changes, just an audit for width regressions (count badges, KPI cards, table columns).
  • +
  • Transactional email (src/emails/invite-email.tsx) cannot use next/font — document the gap; recommend it stays system-font.
  • +
+ +

Dark/light strategy

+

Keep next-themes attribute="class" with the .dark class and the + three states light/dark/system. Re-key Nothing's dark values onto .dark + and light values onto plain :root. The segmented theme toggle is wired to + setTheme (next-themes), not the mockup's data-theme/ + theme.js. Decision: keep system as a third segment or as the default. + suppressHydrationWarning already present guards FOUC; decide whether to remove + disableTransitionOnChange for a subtle cross-fade.

+
+ + +
+
+ 02 +

Component migration matrix

+
+

Re-skin the 16 shadcn primitives in place (rewrite CVA/className + strings; keep imports) so the ~30 consuming components keep working. Mockup references: + tools.html + settings.html + users.html.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
shadcn primitiveNothing treatment / classEffortNotes
button.tsx — default (bg-primary green).btn .btn--primary--text-display fill, --black text, pill, Space Mono 13px CAPS, min-height 44pxMPrimary fill is greyscale white, NOT the accent. Add btn--sm (36px). Keep asChild/Slot.
button.tsx — secondary / outline.btn--secondary — transparent, 1px --border-visible, hover border→text-primaryMCollapse shadcn secondary(fill)+outline onto one bordered variant.
button.tsx — ghost.btn--ghost + .btn--icon (44×44) for icon-only row actionsSTable row View/Edit/More pattern.
button.tsx — destructive (bg-destructive).btn--destructive — transparent, 1px --danger, danger text, hover --danger-subtleMOutline (1px), not solid fill; resolves to the shared --accent red (the --danger alias).
button.tsx — linkInline text link in --interactive, underline-offset hoverSNot a .btn; --interactive is for tappable text.
button.tsx — size scale (xs/sm/lg/icon-*)Reduce to default(44) + --sm(36) + --icon; map xs→sm, lg→defaultMAudit dense table callers; sanction a 32px .icon-btn exception (users.html) vs 44px.
card.tsx (rounded-xl, shadow-sm).card / .card--raised / .card--bare / .card--pad-lg — 14px radius, no shadowSCardTitle→.heading; CardDescription→.caption t-secondary.
input.tsx (full border box).field > label.label + .control (underline) or .field--boxedMDefault is underline; label ALL-CAPS above; input text Space Mono. Error: .is-error + .error-msg.
textarea.tsx.control on a textarea; keep field-sizing-contentSNo dedicated class; boxed may read better for multi-line.
label.tsx (text-sm font-medium).label (Space Mono 11px CAPS 0.1em) for field captions; sentence-case for inline checkbox/switch labelsSTwo registers: instrument label vs body label.
checkbox.tsx (bg-primary checked)Square, 4px radius, 1px --border-visible; checked → --text-display fill + --black checkMNEW pattern (no nothing.css class). Greyscale, NOT the accent. Keep Lucide CheckIcon.
switch.tsx (bg-primary checked).switch / .switch.on — track --text-display on, thumb --blackSGreyscale, NOT the accent. Ensure ≥44px hit area.
select.tsx (trigger + content shadow-md)Trigger = .btn--secondary --sm or .field .control; content = surface-raised, 8px, 44px items, left 2px accent bar, NO shadowLBiggest restyle: drop zoom/slide; selected indicator becomes a left accent bar, not a right CheckIcon.
badge.tsx (filled pill chips).tag (+ --success/--warning/--accent/--active/--tech) — border-only, optional .led dotMCore inversion: fill→border. default→--active, destructive→--accent(danger). Add success/warning variants.
separator.tsx.divider (1px --border); vertical keeps w-pxSNear 1:1.
tooltip.tsx (dark bubble + zoom/slide)Restyle: surface-raised, 1px border-visible, Space Mono caption, opacity-fade only (drop zoom/slide/scale)MNothing prefers inline labels. Decide whether to retain hover tooltips at all; audit title= icon buttons.
tabs.tsx (segmented pill + line variant).tabs > .tab(.active) text tabs; in-content tabs use NEUTRAL underline; segmented filters → .segmentedMsettings.html overrides active underline to --border-visible so accent stays the single interrupt; only nav is accent.
table.tsx (hover bg-muted).table — label headers + border-visible; --border dividers; .num mono right; .cell-strong first col; .is-active = surface-raised + 2px left accentLNo zebra. Touches TanStack column defs in consumers — coordinate with tables work.
form.tsx (text-destructive messages)FormLabel→.label; FormDescription→.caption t-secondary; FormMessage→.error-msg / .field.is-errorMError text points at --danger (= the --accent red). Logic unchanged.
calendar.tsx (react-day-picker popover)For period selection → .period-nav linear stepper. Restyle DayPicker to tokens only for true arbitrary-date pickersLLargest semantic shift; calendar effectively deprecated for month/period navigation.
+
+

Foundational fonts/radii also flow through here: Inter→Space Grotesk/Mono/Doto; + --radius 0.625rem → Nothing 4/8/14/pill; sonner Toaster removal → inline + .status-text. All covered as cross-cutting items in §03.

+
+ + +
+
+ 03 +

Anti-patterns to resolve

+
+

Four Nothing violations, all confirmed in real code, touch nearly every interactive + screen. Three of the four already have proven classes in nothing.css; the only true gap is a + hardware-style segmented spinner (must be authored, respecting prefers-reduced-motion).

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Anti-patternWhere it livesResolutionScale
Toast popups (Sonner)Single <Toaster/> in layout.tsx:65 feeds 183 toast.* call sites / 39 files (invoice-upload 14, scheduled-jobs 12, assignment-detail 12, assignments 11, user-detail 10, budget-detail 10, github-integration 8, tool-detail 8, …)Inline [SAVED]/[ERROR: …] via .status-text/--ok/--err near each trigger; build one shared <StatusText>/useInlineStatus with aria-live="polite"; remove the global Toaster only after the primitive exists. Bulk/multi-event outcomes get an inline summary box.L
Skeleton loadersui/skeleton.tsx used 108× / 16 files; 15 are route loading.tsx (app, assignments, reports, users[/[id]], tools[/[id]], invoices, copilot[/seats,/billing,/analytics], budget[/[id]]) + sidebar SidebarMenuSkeleton + budget-report[LOADING…] via .loading-text + a NEW hardware-style segmented/dot-matrix spinner. Collapse the 15 near-identical grids to a shared <LoadingState label/>. Drop Card scaffolds.M
Alert banner (red)alert-banner.tsx (shadcn Alert variant=destructive, AlertTriangle) at layout.tsx:58; + ~8 other <Alert> consumers (api-preview, cost-tracking, sync/ingestion tables, setup/login forms)Flat inline status box: 1px border (--danger critical / --warning approaching) on BORDER + value text only, never a background fill; [ X ] ghost dismiss; .stat-row utilization rows. Keep localStorage fingerprint + aria-live.M
Calendar date popoversExactly 2 sites: assignments-client.tsx:281 & assignment-detail-client.tsx:359 (single "Assigned Date", future-date guard, dropdown caption)Per components.md §9 prefer linear .period-nav stepping; but these are single-date fields — recommend a Space Mono underline date input. Reserve .period-nav for genuine period browsing (dashboard/reports/claude month nav). Documented exception.M
Status-as-floating-popovererror-popover.tsx (ingestion/sync tables)Inline expandable .status-text--err (click-to-expand), no Popover.S
+
+ +
+
Error-heavy subsystem — one red, watch the count
+

Error/destructive is the dominant accent use here (toast.error, Alert destructive, + failed outcome badges). Under the confirmed unified-red model these all resolve to the same + --accent #D71921 — nothing to decouple. The watch-item is quantity: + a dialog or row showing an active/selected state and an error can surface two reds. + Hold "one red region per screen" — let the error be the red moment and de-emphasize the + active indicator when both are present.

+
+
+ + +
+
+ 04 +

App shell & navigation

+
+

Replace the shadcn SidebarProvider/Sidebar/SidebarInset + composition with the Nothing CSS-grid .app shell + sticky text-nav .sidebar. + The shell owns the single red accent moment per screen (active nav bar). Every mockup shares this + shell — see + dashboard.html + for the canonical rendered chrome.

+ +
+ + + + + + + + + + + + + + +
Shell elementFrom (shadcn)To (Nothing)Effort
Root gridSidebarProvider + Sidebar + SidebarInset (flex)CSS-grid .app + sticky .sidebar + .main (max-width 1280px). Keep SidebarProvider context ONLY for mobile open state — re-skin, don't delete.L
Wordmark<h2>AI Developer Hub</h2>.brand > .dot (red) + .wordmark "AI·HUB" (Space Mono CAPS 0.14em)S
Nav itemsSidebarMenuButton + Lucide icons, "Navigation" group label.nav > a.nav-item ALL-CAPS Space Mono; drop the group label; icons optional (decide monoline vs text-only). Role filtering + active-path logic carry over.M
Active statedata-[active]:bg-sidebar-accent (filled).nav-item.active::before 2px left bar in --accent + text→--text-display. THE single accent moment per screen (red, both modes).S
User chip / footerSidebarFooter DropdownMenu (My Profile / Sign Out).sidebar-footer > .user-chip (.avatar initials + name/role) + a .nav-item Sign Out. Decide where "My Profile" goes (chip→/profile, or keep a shadowless Nothing dropdown).M
Topbar<header h-14 border-b> with only SidebarTrigger.topbar > {.label.eyebrow + h1.display-md} + .topbar-actions. Needs a shared <Topbar eyebrow title actions> contract pages consume in P4.L
Mobile triggerSidebarTrigger (PanelLeftIcon)NET-NEW: re-skinned monoline trigger at mobile breakpoint; add a @media rule to off-canvas the sidebar. Mockups are desktop-only — design/approve this.M
Theme toggleSun/Moon DropdownMenu (Light/Dark/System)Segmented .theme-toggle (DARK | LIGHT [| SYSTEM]) wired to next-themes setTheme. Reconcile 2-state mockup vs 3-state app.M
Breadcrumbshadcn Breadcrumb (budget only, ChevronRight)Fold into the topbar .eyebrow as a Space Mono "SECTION / CONTEXT" trail (only 1 real consumer). Preserve aria-current / nav[aria-label=breadcrumb] if kept standalone.S
Alert bannershadcn Alert variant=destructive (red bg)Bordered inline status box (1px --danger critical / --warning approaching), value-only color, [ X ] ghost dismiss. Keep aria-live + localStorage dismiss + null guards.M
+
+ +
+
In-content tabs stay neutral
+

The shell owns the red accent (active nav bar). Therefore in-content tab underlines and + progress "within-range" fills are NEUTRAL (--border-visible / --text-display) + — proven by the scoped overrides in settings.html, reports.html, copilot.html and claude.html. + This keeps "one interrupt per screen" intact.

+
+
+ + +
+
+ 05 +

Charts & data visualization

+
+

~20 Recharts components + 2 Sparkline impls + 3 non-Recharts bar/gauge visuals, + all routed through one shared wrapper (ui/chart.tsx). Migrate the wrapper FIRST — + every chart inherits its grid/tooltip/legend/axis styling. References: + reports.html + claude.html + copilot.html + budget.html.

+ +

Recharts theming rules

+
    +
  • Monochrome-first categories: differentiate by opacity → pattern → line-style → color (color last). Use 2–3 greys (--text-display / --text-secondary / --border-visible) + an opacity ramp. Delete the duplicated 8-color spectral FALLBACK_PALETTE; define one shared getCategoryStyle(rank).
  • +
  • Reserve red for one over/active series only. Demote it from its current default --chart-1 data role; var(--destructive) chart usages → the --accent red.
  • +
  • Bars: square ends (radius=[0,0,0,0]), fill --text-display, empty/remainder --border, over-limit --accent (red).
  • +
  • Lines: 1.5–2px --text-display current; secondary by opacity; projection/average 1px dashed --text-secondary (3 3); no dots (or one end dot); horizontal-only grid --border; direct line labels, no legend boxes.
  • +
  • No area fill, no gradients, no shadows. Remove the viewerSpendArea linearGradient; restyle the tooltip card to surface-raised + 1px border-visible + 8px radius (drop shadow-xl).
  • +
  • Axis/labels: Space Mono at caption/label size, --text-disabled/--text-secondary; set tick={{fontFamily:'var(--font-mono)'}} centrally in the wrapper.
  • +
  • Light-mode strokes: the red accent passes as a stroke/axis color in light mode (~4.76:1), so no darkened variant is needed; keep non-accent series differentiated by opacity, not hue.
  • +
+ +

Segmented-bar / gauge / sparkline approach

+
    +
  • Segmented bar (signature): .seg-bar / .seg-bar--hero / .seg-bar--compact of ~20 discrete .seg; .is-filled (text-display) / .is-good / .is-warn / .is-over (accent). Build one reusable React component computing segment counts from percentages; replaces the rounded continuous SpendProgressBar, the multi-marker budget-health bar, and the per-model mini bars.
  • +
  • Gauge / proportion: the forbidden donut (copilot/activity-distribution) becomes a 4-row segmented bar or horizontal stacked proportion bar with direct .legend labels and numeric readouts (sign-off needed). Pies are banned.
  • +
  • Sparkline: standardize on ui/sparkline.tsx (already Nothing-correct: 1.5px currentColor polyline + end dot). Delete the duplicate Recharts reports/sparkline.tsx and re-point consumers.
  • +
  • Re-theme vs rebuild (P3 gate): re-theme the wrapper once and keep Recharts for line/bar (preserves tooltips/responsiveness); rebuild only the donut and area chart where Nothing forbids the idiom. Mockups render some charts as pure CSS columns/seg-bars — decide per chart family.
  • +
+ +

Ripple files: cumulative-pacing, twelve-month-bar, cost-distribution-histogram, + workspace-daily, plan-vs-actual, forecast-cumulative, daily-by-user, top-users, billing-trend, + cost-utilization, language, editor, usage-trend, global-metrics, spend-trend, my-usage. Verify each + in BOTH modes for legibility/contrast.

+
+ + +
+
+ 06 +

Dialogs & overlays

+
+

User priority — exhaustive inventory. Nothing overlay spec (components.md §14): + NO shadows; layering via background contrast + 1px --border-visible; modals = backdrop + rgba(0,0,0,0.8) + 16px radius + ghost [ X ]; dropdowns = surface-raised + + 8px + 44px items + left 2px accent bar on selected; fade-only (no zoom/slide); toasts BANNED.

+ +

Primitives (6)

+
+ + + + + + + + + + + + +
PrimitiveFromNothing patternEffort
dialog.tsx — Overlaybg-black/50 + fadeBackdrop bg-black/80; fade onlyS
dialog.tsx — Contentrounded-lg bg-background shadow-lg zoom-in-95--surface + 1px --border-visible + 16px, NO shadow, fade only; cap ~480px but allow wide overridesM
dialog.tsx — [X] closeopacity-70 icon, data-[state]:bg-accentBordered/ghost [ X ] top-right; remove accent tintS
alert-dialog.tsxshadow-lg, rounded-lg, bg-black/50Same Nothing modal; AlertDialogAction destructive → .btn--destructive (--danger outline)M
sheet.tsx4 sides, w-3/4, shadow-lg, slide 300/500ms--surface, NO shadow; right edge 1px border-visible; bottom-sheet adds 16px top radius + handle; soften slideM
popover.tsxrounded-md bg-popover shadow-md zoom+slide--surface-raised + 1px border-visible + 8px, NO shadow, fade onlyS
dropdown-menu.tsxshadow-md, focus:bg-accent, items ~32pxsurface-raised + 8px, no shadow, 44px items, selected = left 2px --accent bar; destructive item text --dangerM
command.tsxCommandDialog p-0, data-[selected]:bg-accentOn Nothing modal; selected → left 2px accent bar + surface-raised row (not full-row tint); 44px rows; input underline border-bM
+
+ +

Feature dialogs, sheets & comboboxes

+
+ + + + + + + + + + + + + + + + + + + + + + + +
ComponentTypeNothing patternEffort
edit-user-dialog.tsxDialogNothing modal + .field labels + boxed controls; footer secondary Cancel + primary Save; toast→inline statusM
invite-link-dialog.tsxDialogRead-only boxed .control; Copy = .btn--icon secondary; Send = primary; copied feedback inline [COPIED] (not toast)M
sync/backfill-dialog.tsxDialogTrigger ghost btn→.btn--ghost; date boxed control; result [SAVED]/[ERROR]M
requests/[id]/approval-dialog.tsxDialog (wide)Keep max-w-3xl; preview/source panes 1px border-visible NO bg-muted; labels .label; toast→inline. dangerouslySetInnerHTML preview staysL
requests/[id]/rejection-dialog.tsxDialogTextarea→Space Mono control; Send = .btn--destructive (--danger); toast→inlineM
requests/[id]/completion-dialog.tsxDialog (wide, wizard)Keep wide; 2-step nav; Select+Textarea+date Nothing-styled; toast→inline summaryL
invoices/sync-results-dialog.tsxDialog (wide)OutcomeBadge→.tag border-only (success/warning/accent), NO bg-green/blue/amber/red-100; scrollable .table; secondary Close + primary ApplyM
budget/.../billed-cost-dialog.tsxDialogNothing modal; 4 boxed .control; ghost Cancel + primary Save; inline statusM
settings/license-templates/template-editor-dialog.tsxDialog (max-w-4xl)Wide override; 3-col labels .label; preview pane 1px border-visible no fill; warning → 1px --warning summary (no amber bg); Delete = .btn--destructive; toast→inlineL
reset-password-dialog.tsxAlertDialogNothing alert modal; Checkbox→Nothing checkbox; Reset = .btn--destructive; chains to InviteLinkDialog; toast→inlineS
budget/.../delete-billed-cost-dialog.tsxAlertDialogRemove literal bg-destructive override; destructive Action = .btn--destructive (--danger outline)M
users/users-table.tsx (in-table)AlertDialog ×2Deactivate {name}? + batch Send invites → inherit alert mapping; Deactivate = --danger; row menu items 44px + accent barS
settings/sync/github-member-sync-sheet.tsxSheet (right)Nothing sheet; SummaryCards→.card/stat-rows; conflict box→1px --danger (no bg tint); Tabs→.tabs; Tables→.table; Loader2→[LOADING…]; toast→inline. Embeds combobox + inline form + member cardsL
error-popover.tsxPopoverInline expandable [ERROR: …] status text; or restyle popover surface; no floating revealS
user-combobox.tsxPopover + CommandTrigger→.btn--secondary; selected = left accent bar (not Check tick); single thin chevron; shouldFilter=false preservedS
user-search-combobox.tsxCommandsurface-raised 8px; status Badge→.tag (no fill); empty→[TYPE TO SEARCH]; Cancel→ghost; debounce keptS
data-table-faceted-filter.tsxPopover + CommandNothing popover; selected options→left accent bar; count→.tag; CheckIcon stroke 1.5S
DropdownMenu row actions (×10)DropdownMenuusers/tools/budget/assignments[/[id]]/claude-users/integrations/ingestion/tool-detail/user-detail — one primitive fix covers chrome; per-file work is destructive-item + label tweaksM
ui/sonner.tsx + ~25 call sitesGlobal toastRemove Toaster; per-surface inline [SAVED]/[ERROR] status with auto-clear + aria-live. Defining change of this subsystemL
+
+ +
+
Wide-modal exception
+

Keep max-w-3xl/max-w-4xl for approval / completion / template-editor / + sync-results / member-sync — these are data-dense 2–3-column editors, not confirmations. + Document as an accepted deviation from the §14 480px modal cap.

+
+
+ + +
+
+ 07 +

Page-by-page migration

+
+

All 40 routes + the AuthGuard access-denied gate, grouped by section, with mapped mockup, effort and notes. Subpages + (detail / new / import) included.

+ +

Dashboard

+
+ + + + + + +
RouteKey componentsMockupEffortNotes
/ (admin)admin-dashboard, budget-hero (SpendProgressBar), this-month-card, kpi-grid, spend-trend-card (Recharts), insights-grid, activity-timeline, jump-to-rowdashboard.htmlL1:1 mockup map. Hero→seg-bar + Doto; at-risk = the one accent (red --danger). Shared SpendProgressBar/Timeline/KpiWithMom/WhatChanged migrate as a unit.
/ ?as=viewerviewer-dashboard, identity-card (SyncCallout), my-usage-card (AreaChart), personal-kpis, my-tools-table, personal-activity, catalog-link-cardnone (derive)LNo mockup — author/sign-off. SyncCallout tinted boxes→inline status; AreaChart→line; preview banner→border/text only.
+
+ +

Tools / Users / Assignments / Requests

+
+ + + + + + + + + + + + + + + + +
RouteKey componentsMockupEffortNotes
/toolstools-table (DataTable), status/vendor faceted filters, Archive AlertDialogtools.htmlMInherits shared DataTable re-skin; status Badge→.tag--success + .led.
/tools/newnew-tool-form (RHF, useFieldArray tiers)tools.htmlMCards→.card; repeatable tier rows; ALL-CAPS field labels.
/tools/[id]tool-detail-client, Edit form, Archive AlertDialog, Access Tiers + Add/Edit Tier Dialogs (Switch), Change Historytools.htmlLDual edit/read-only; tier dialogs Nothing-styled; history→.stat-list.
/usersusers-table (8 cols, 6 faceted filters, discipline icons), batch-invite + deactivate AlertDialogs, ResetPasswordDialogusers.htmlLPending users = the single accent hero. .filter-chip promote to nothing.css.
/users/newnew-user-form (Selects), InviteLinkDialog on successusers.htmlMUser Details Card→.card + .field.
/users/[id]user-detail-client, GitHub Profile Card, Edit form, Reset/Deactivate, Assigned Tools (revoke/reactivate), Assign License Dialog, AdminCostSection (Recharts), Change Historyusers.htmlLLarge multi-section; API-key reveal/copy preserved; AdminCostSection deferred to charts.
/users/importbulk-import-form (CSV, preview table, Valid/Invalid/Update/New badges, invite links)users.htmlMNo red row tint — error to Status column only; changed cells→.cell-strong.
/assignmentsassignments-client (DataTable), Assign License Dialog (UserCombobox + date Popover+Calendar), EditAssignmentDialog (API key reveal), sync-managed Badge+Tooltip, revoke AlertDialogassignments.htmlLDate popover → Space Mono date input (documented exception).
/assignments/[id]assignment-detail-client (dual edit/read, tier Select, date Popover+Calendar, API key), Comments (Textarea + counter)assignments.htmlMBack link→.back-btn; comments→.stat-list.
/assignments/importbulk-assignment-import-form (CSV preview, masked API key, server-error notice)assignments.htmlMborder-destructive bg tint→inline [ERROR] in Status column.
/requestsrequests-table, status faceted filter, statusBadge() (rejected=destructive), REQ-### mono linksrequests.htmlMRejected stays red --danger; active-row left accent rail.
/requests/[id]request-detail-client, Requester Card, payload (FormPayloadList), Sent messages (border-l-primary + prose), action bar (Approve/Reject/Complete/Cancel), native confirm()requests.htmlMReplace native confirm() with Nothing modal; message left border→--border-visible.
+
+ +

Budget / Invoices / Reports

+
+ + + + + + + + + + + + + +
RouteKey componentsMockupEffortNotes
/budgetactive-budget landing + EmptyBudgetStatebudget.htmlSEmpty→.empty.
/budget/historybudget-table (DataTable), back-linkbudget.htmlMInherits shared DataTable.
/budget/newnew-budget-form (RHF, fiscalYear/totalAmount/periodType Selects)budget.htmlMperiodType→.segmented (Monthly/Quarterly).
/budget/[id]budget-detail-client, budget-health-hero (MultiMarkerBar), past-month-spotlight, period-allocations-table (inline inputs, sub-rows), 3 dialogsbudget.htmlLMulti-marker bar→seg-bar + legend (hardest piece). Colored row tints→value/tag only.
/invoicesinvoices-table (DataTable), Download Tooltip; ADD archived-invoices hero countinvoices.htmlSServer must surface total count for the new Doto hero.
/invoices/newinvoice-upload-form (file picker, ConfidenceInput low-confidence amber, Duplicate AlertDialog), aria-liveinvoices.htmlMLow-confidence underline→--warning; Loader2→[LOADING…]; toast→inline.
/invoices/bulkbulk-upload-form (idle/uploading/reviewing/saving/done/error machine), editable rows, outcome badges, results tableinvoices.htmlL6 states→[STATUS]; duplicate→tag--warning (confirm vs accent); skipped rows opacity 0.4.
/reports (overview)reports-nav (tabs), OverviewPanel: KpiWithMom ×4, BudgetHealthHero, WhatChanged, tool-adoption + spend-by-circle Tables, Sparklinereports.htmlLTabs NEUTRAL underline. MoM "up" delta flips from red→tag--active/neutral.
/reports/budgetbudget-report, at-a-glance (SpendProgressBar), plan-vs-actual-chart + forecast-cumulative-chart (Recharts), per-tool-breakdown, past-month-spotlightreports.htmlLTwo Recharts charts re-themed; sub-row bg tints→surface-raised + indentation.
+
+ +

Copilot / Claude consoles

+
+ + + + + + + + + + + + + +
RouteKey componentsMockupEffortNotes
/copilot (Overview)copilot-tab-bar, overview-cards (5-up KPI), usage-trend-chartcopilot.htmlMTabs NEUTRAL underline; Acceptance Rate = Doto hero.
/copilot/seatsseats-table (DataTable, avatar+login, plan/status badges)copilot.htmlMSeat-user cells; badges→.tag.
/copilot/seats/[userId]seat detail: back btn, avatar, status/plan badges, field grid, Sync History timelinecopilot.htmlMUnmatched amber→--warning on value; timeline badges→.tag.
/copilot/billing4 KPI cards, billing-trend-chart, cost-utilization-chart, Monthly Billing Details tablecopilot.htmlLBars→.hist-bars; dual-axis line re-themed.
/copilot/analyticsmetrics-freshness-card (amber), language-chart, editor-chart, activity-distribution (DONUT)copilot.htmlLDonut→seg-bars/stat-list; freshness amber bg→border-only .alert.
/claude (Workspaces)claude-tabs, sync-button, global-metrics-client (stacked BarChart + est ghost), historical-trend-card (segmented), org-credits-panel, workspace-budget-list, sync-status-pillclaude.htmlLStacked chart→CSS columns; 8-color palette→grey tiers; progress bars→seg-bars; toasts→inline.
/claude/usersusers-month-picker, sync-status-pill, user-kpi-strip, daily-by-user-chart, top-users bar, cost-distribution-histogram, top-movers-chips, bespoke users-table (cmdk filters, Sparkline, Switch)claude.htmlLMonth picker→.mini-select/period-nav; histogram bucket destructive→--danger.
/claude/users/[userId]user-detail-client (breadcrumb, badges, MonthPicker, pricing-unresolved amber, KpiStrip, daily + model breakdown + top dates, 12-month bars)claude.htmlM12-month→.hist-bars with .is-proj.
/claude/workspaces/[workspaceId]workspace-detail-client (breadcrumb, color dot, Default/Over-budget badges, inline limit edit, KpiStrip, daily chart, top users, model breakdown)claude.htmlMOver-budget badge stays warning-toned; projected KPI = the one red interrupt; displayColor kept as dot only.
+
+ +

Settings / Profile

+
+ + + + + + + + + + + +
RouteKey componentsMockupEffortNotes
/settings/appearance3-up theme grid (useThemePreference)settings.htmlS.theme-grid/.theme-opt; isSaving→[SAVED].
/settings/integrationsgithub-integration-client (PAT input, Validate, org Select, Connect/Disconnect AlertDialog), claude-code-status-cardsettings.htmlMDisconnect = --danger; scopes in .code chips; toast→inline.
/settings/syncsync-dashboard (polling 30s/5s), scheduled-jobs-table, manual-jobs-table, github-member-sync-sheet, OutcomeBadge, ErrorPopoversettings.htmlLSync hero Doto fraction + seg-bar; Suspense skeleton→[LOADING…]; live .poll LED.
/settings/ingestioningestion-filters-section (rules table, Switch, create/edit Dialog, native confirm), ingestion-history-table (DataTable, faceted filters)settings.htmlLBlacklist Badge→neutral tag--tech (not red); confirm()→Nothing modal.
/settings/license-templatestemplates-client (per-tool cards, TemplateRow), template-editor-dialog (3-col)settings.htmlMkind Badges→neutral tags; wide editor exception.
/settings/api-previewapi-preview-client (email/month form, Send, response Badge, Copy JSON, JsonViewer), not-configured Alertsettings.htmlMJSON viewer multi-color→monochrome; Loader2/toast→inline.
/profileprofile-client, profile-header (avatar, badges), profile-assignments, cost-tracking-section (MonthPicker, CostChart, destructive Alert)none (settings vocab)MAvatar→bordered .avatar; CostChart Nothing-themed; unresolved-pricing→--warning value.
+
+ +

Auth

+
+ + + + + + + +
RouteKey componentsMockupEffortNotes
/login(auth)/layout (2-col), login page Card, login-form (RHF, signIn, Alert destructive, Loader2)login.htmlMFully mocked; copy scoped .auth-split/.auth-pane; per-route theme toggle; brand panel neutral (not an accent fill). Submit stays white-on-black, NOT the accent.
/setup-password/[token]setup-password page (3 error branches: expired/consumed/invalid), setup-password-form (New/Confirm, success toast)none (login vocab)MToast→inline [PASSWORD SET]; expired/invalid = --danger, consumed = neutral; tinted chips→monoline glyphs.
AuthGuard (Access Denied)server gate + admin "Access Denied" Cardshell emptyS.empty (informational, no red).
+
+
+ + +
+
+ 08 +

Phasing & sequencing

+
+

Hard-sequence foundation → primitives → shell → shared-data → + pages → QA so shared surfaces stabilize before per-page work. shadcn semantic var names stay + aliased throughout so primitives never lose color mid-migration.

+ +
+
P0Foundation: Decisions, Tokens & FontsL
+

Ratify the brand-accent decision, port the Nothing token layer into globals.css (re-keyed onto next-themes .dark, NOT data-theme), load the three-family font stack via next/font.

+
+
Includes
Design tokens, theming & dark/light · Typography & font loading (Inter → Doto/Space Grotesk/Space Mono)
+
Depends on
+
Exit
Full Nothing token set on :root(light)/.dark(dark); --accent=#D71921 single interrupt (errors share it, no decoupling; red verified ≥4.5:1 text / ≥3:1 graphical both modes); radii 4/8/14/pill; --ease. shadcn names aliased (no primitive loses color). @theme inline registers font tokens; next/font loads all three (Space Grotesk preloaded, Doto preload:false), Inter removed. Utility classes available. typecheck + build pass; one route smoke-tested both modes, no FOUC.
+
+
+ +
+
P1Primitives & Overlays Re-skinXL
+

Re-skin the 16 control primitives + 6 overlay primitives in place; build shared cross-cutting helpers (inline status-text, [LOADING], segmented spinner, Nothing dropdown/modal/sheet chrome).

+
+
Includes
shadcn primitives · Overlays · Anti-pattern resolution (toasts/skeletons/banner/date-popovers)
+
Depends on
P0
+
Exit
All primitives Nothing-styled (pill/underline/flat/no-shadow, Space Mono caps, square greyscale checkbox/switch, border-only tags). Overlays use surface/surface-raised, 1px border-visible, 16/8px, 0.8 backdrop, NO shadow, fade-only, [X] ghost close, left 2px accent on selected. Destructive primitives point at --danger (outline). Shared <StatusText>/useInlineStatus + [LOADING]/spinner documented. Variant collapse reviewed vs call sites. Verified vs tools.html/settings.html both modes; typecheck/lint/build green.
+
+
+ +
+
P2App Shell, Sidebar, Topbar & Global LayoutXL
+

Migrate the shell so global chrome is correct and owns the one accent moment. Establish the Topbar contract. Remove global Toaster + AlertBanner from layout.tsx.

+
+
Includes
App shell, sidebar, topbar, breadcrumbs & alert banner
+
Depends on
P0, P1
+
Exit
CSS-grid .app with re-skinned shadcn sidebar machinery preserved (Cmd+B, cookie, mobile Sheet/useIsMobile); ALL-CAPS nav with 2px red active bar; AI·HUB wordmark + dot; user chip + Sign Out; segmented toggle wired to next-themes (System retained). Topbar exposes eyebrow+title+actions + mobile SidebarTrigger (net-new, approved). Global Toaster removed; AlertBanner→flat bordered status box (red critical / amber approaching) keeping aria-live + dismiss. Breadcrumb folded into eyebrow. Verified vs every shared shell both modes + mobile; build green.
+
+
+ +
+
P3Shared Data Layer: Tables & ChartsXL
+

Re-skin the shared DataTable + ui/table + column header + faceted filters ONCE (cascades to ~9 tables); migrate the shared Recharts wrapper + shared viz (SpendProgressBar→seg-bar, Sparkline consolidation, Timeline, KpiWithMom, WhatChanged, budget-health-hero). Resolve re-theme-vs-rebuild.

+
+
Includes
Data tables (TanStack) & filters · Charts (Recharts) & data viz
+
Depends on
P0, P1, P2
+
Exit
Tables: label headers, --border dividers (no zebra), .num mono right, .cell-strong, hover=surface, active row=surface-raised + 2px left accent; search=.search; pager=segmented+period-nav. OutcomeBadge/status→.tag/.led (failed=red). Chart wrapper: horizontal-only grid, mono axes, shadowless tooltip, no legend boxes, monochrome ramp (opacity>pattern>color), red for one over/active series. SpendProgressBar→seg-bar; one canonical Sparkline; donut + area replacements signed off. Re-theme-vs-rebuild decided per family. Verified both modes (red strokes pass in light).
+
+
+ +
+
P4Page Migration by SectionXL
+

Migrate route content for all feature areas. Sequence by risk: Auth first (most-referenced, mocked), then Dashboards, then CRUD list/detail, then the chart-heavy financial + console screens.

+
+
Includes
Auth · Dashboards (admin+viewer) · Tools/Users/Assignments/Requests · Budget/Invoices/Reports · Copilot/Claude · Settings/Profile
+
Depends on
P0, P1, P2, P3
+
Exit
All ~40 routes Nothing-correct in both modes vs mockups; viewer + setup-password composed/signed off. Every former toast is inline [SAVED]/[ERROR] with aria-live; every skeleton/loading.tsx is [LOADING]; no red row/tinted-bg statuses (value text only); exactly one accent interrupt per screen audited; native confirm() replaced; JSON viewer monochrome. Per-section typecheck/lint/build green.
+
+
+ +
+
P5Anti-pattern Sweep, A11y & Visual QA GateL
+

Final cross-cutting verification: zero orphaned toasts/skeletons/shadows/zebra/calendar-popovers; automated contrast/a11y (the single red accent in both modes); mockup-fidelity visual regression across all routes in both themes + mobile; no stale oklch leaks in edge states. Gate before merge.

+
+
Includes
Anti-pattern resolution · All subsystems (verification pass)
+
Depends on
P4
+
Exit
Lint/grep gate: zero toast.*/sonner imports, zero <Skeleton>/animate-pulse, zero shadow-* on chrome, zero calendar popovers except sanctioned exceptions. axe + Lighthouse pass; WCAG check confirms the red accent and amber/green status pass ≥4.5:1 text / ≥3:1 graphical in both modes, and no screen shows more than one red region. Visual regression matches mockups across 40 routes in dark+light+mobile. "One accent interrupt per screen" audited. Full typecheck/lint/build/test green; ready to merge as one coordinated replacement.
+
+
+
+ + +
+
+ 09 +

Risks & mitigations

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RiskSeverityMitigation
One-red-per-screen at scale — accent and error/destructive share #D71921, so a screen with BOTH an active/selected state AND an error or destructive surface can show two reds, breaking the "single interrupt" rule. Most acute in the error-heavy console/sync/dialog flows.MediumMake it a hard checklist item in P4/P5: exactly one red region per screen. When an error is present, IT is the red moment — render the active-nav / active-row / tab indicator in neutral --text-display on that view. The consistency sweep that cleared the mockups (neutral in-content tabs) is the template; the P5 manual audit asserts the per-route count in both modes.
Recharts theming effort — ~20 Recharts components + 2 sparklines must go monochrome (square bars, no area fill, no legend boxes, direct labels, mono axes, shadowless tooltips). Mockups render many as pure CSS/inline-SVG (no 1:1 map); pies/donut + area are bans needing redesign.HighP3 gate per chart family: re-theme the shared wrapper once; keep Recharts for line/bar; rebuild only donut (→segmented/stacked-proportion) and area (→line). Shared getCategoryStyle(rank) (opacity>pattern>color); delete duplicated 8-color palettes. Verify both modes; the red accent strokes pass in light (no darkened variant).
Removing Sonner is app-wide behavioral — ~183 toast.* sites / 39 files are the ONLY success/error feedback (incl. multi-event "sent 12 of 15", bulk created/updated/skipped/failed). Naive removal = silent failures + lost screen-reader announcements.HighBuild ONE shared inline-status primitive (StatusText/useInlineStatus, aria-live=polite) in P1 BEFORE touching call sites; remove the global Toaster only in P2 once it exists. Per-form placement convention + inline summary-box for bulk outcomes. P5 lint/grep gate asserts zero remaining sonner imports/toast.* calls.
Font FOUT/CLS & loading — three families via the mockup's render-blocking @import would reintroduce FOUT/CLS + payload; Inter→Space Grotesk/Mono glyph-width changes can overflow tight UI; Doto variable axis may be unreliable in next/font.MediumUse next/font/google per family (self-hosted, auto fallback metrics), NOT @import; display:'swap', preload Space Grotesk only, preload:false on Doto/Space Mono. Pin Doto weights ['400','500','700'] if the axis misbehaves; validate hero rendering. Define both --font-sans and --font-ui (aliased). Screenshot-diff key routes for width overflow.
Reconciling theme strategy — app authors LIGHT as :root default + .dark via class; nothing.css authors DARK as :root + data-theme=light. Mis-mapping inverts colors. Also --primary==--ring==--chart-1==--sidebar-primary, so the accent change recolors buttons/rings/sidebar-active/primary-chart at once. Toggle is 2-state in mockup vs 3-state in app.MediumRe-key nothing.css value sets onto the existing :root(light)/.dark(dark) selectors (don't copy data-theme). Keep attribute=class + System. Change primary/ring/chart-1/sidebar-primary in one coordinated edit and verify all four surfaces together. Wire the segmented toggle to next-themes (not theme.js); suppressHydrationWarning guards FOUC.
Scope/regression across ~40 routes — shared blast radius: tokens (every screen), 16 primitives (every screen), shared DataTable (~9 tables), shared viz (SpendProgressBar/Timeline/KpiWithMom/WhatChanged used by dashboard AND reports AND claude). One bad shared rule regresses the whole app; cannot ship incrementally per page.HighHard-sequence P0→P4 so shared surfaces stabilize before page work. Keep shadcn var names aliased so primitives never lose color mid-migration. Migrate shared viz as a single unit and regression-test ALL consumers. Mockup-based visual regression from P1 onward + final P5 cross-route gate in both themes + mobile before the single-shot merge.
+
+
+ + +
+
+ 10 +

Testing & verification

+
+
    +
  • Per-phase build gates: run pnpm typecheck, pnpm lint (zero warnings) and pnpm build at the end of every phase and before merging any P4 page-section batch. A red gate is blocking; no phase advances until its predecessor is green.
  • +
  • Mockup-fidelity visual regression: use the chrome-devtools / playwright-skill to render each migrated route at desktop + mobile widths in BOTH themes and diff against the matching mockups/*.html. Build the harness in P1 (primitives vs tools.html/settings.html); run cumulatively through P4/P5.
  • +
  • Auth-gated coverage: use the agent-browser-session skill to mint a NextAuth session cookie for the seeded agent user so the browser reaches every non-public route without the /login flow. Respect the agent deny-list (no DELETE /api/users, invite, reset-password, etc.). Test login.html / setup-password directly via the public routes.
  • +
  • Automated a11y + contrast: run Lighthouse (lighthouse_audit) and axe (chrome-devtools a11y-debugging skill) on representative routes in BOTH themes, with a SPECIFIC assertion that the red accent #D71921 and the amber/green status colors meet ≥4.5:1 text / ≥3:1 graphical in both modes, and that no screen shows more than one red region. Verify focus rings/borders, tap targets (44px vs sanctioned 32px compact icon-btn), keyboard nav.
  • +
  • Anti-pattern grep/lint gate (P5): assert zero remaining from "sonner" / toast. sites, zero <Skeleton>/animate-pulse, zero shadow-* on chrome, zero zebra striping, zero calendar popovers except documented exceptions, zero stale oklch leaks in edge states (rails/disabled/focus/skeleton). Confirm exactly one accent interrupt per screen via a manual checklist.
  • +
  • Shared-component regression sweeps: after P1 and P3, re-test EVERY consumer route of each shared component (not just one) — e.g. verify the DataTable re-skin against tools/users/assignments/copilot-seats/ingestion/invoices simultaneously, and seg-bar/timeline/KPI against dashboard+reports+claude.
  • +
  • Functional behavior preservation: confirm API-key reveal/copy stays masked-by-default, RHF/Zod still fires (.field.is-error/.error-msg), TanStack sort/filter/pagination works, sync polling (30s/5s) updates rows with the new live LED, theme toggle persists to DB without FOUC, and bulk import/invite multi-event outcomes surface via the inline summary-box pattern.
  • +
+
+ + +
+
+ 11 +

Rollout & effort

+
+ +

Rollout strategy

+

Execute the entire migration on a single long-lived feature branch (off main) as a + coordinated full replacement — the goal is a complete swap, and the shared-surface blast radius + (tokens, 16 primitives, shared DataTable, shared viz) makes true page-by-page production rollout + impossible without leaving the app half-styled. Within the branch, sequence strictly P0→P5 so + foundation stabilizes before consumers.

+
    +
  • No broken middle: because next-themes is class-based and shadcn var names are aliased to Nothing tokens, author the Nothing token VALUES so the app compiles and renders at every commit. Keep the old oklch values reachable behind a build flag / scoped wrapper class until P5 to A/B against mockups and bail per-surface if a shared edit regresses.
  • +
  • No permanent dual theme: do NOT ship a user-facing "classic vs nothing" toggle (doubles maintenance, contradicts full-replacement). The parallel theme is a dev-time scaffold only, removed before merge.
  • +
  • Preview review: land the branch behind a Vercel preview so reviewers click through all ~40 routes (agent-browser-session cookie) in both themes before merge.
  • +
  • Single merge: merge to main as one squashed/coordinated PR only after the P5 gate (visual regression + a11y/contrast + grep gate + green build) passes; no interim commits to main. Per project memory, if any gh-aw workflow files are touched, recompile lock.yml in the same commit (not expected here).
  • +
+ +

Sequencing notes

+

P0 (tokens+fonts) is foundational for EVERYTHING and lands first; the accent decision is + settled (Nothing red #D71921, errors share it), so token values can be written without further sign-off. + P1 (primitives+overlays) precedes shell/pages because ~30 components import the primitives; the + shared inline-status + [LOADING] helpers are built here. P2 (shell) precedes pages + because it removes the global Toaster/AlertBanner, defines the Topbar contract, and owns the single + red active-nav accent so in-content tabs stay NEUTRAL. P3 (shared tables + chart wrapper + shared + viz) precedes pages because ~9 tables and 6 dashboard/reports/console screens inherit from them. P4 + (pages) is ordered Auth → Dashboards → CRUD → financial/console (which stress P3 + hardest). P5 is the cross-cutting cleanup + QA gate.

+

Cross-cutting decisions to lock at P0/P1 so they don't churn: accent = + Nothing red #D71921 as the single per-screen interrupt, shared by destructive/error (no separate danger + color, no darkened variant — red passes both modes); keep next-themes class strategy + + System; full toast removal via inline status; full skeleton removal via [LOADING]; + period-nav vs Space-Mono date input for single-date pickers; wide-modal exception + (max-w-3xl/4xl) for data-dense editors; sanctioned 32px compact icon-btn in + dense tables. The pleasant surprise: the app ALREADY ships red as --destructive, + so the Nothing red accent reuses an existing token while the green --primary collapses to greyscale.

+ +

Total effort

+
+
+
P0 · FoundationL
+
P1 · Primitives + OverlaysXL
+
P2 · ShellXL
+
P3 · Shared Tables + ChartsXL
+
P4 · Page Migration (largest by volume)XL
+
P5 · QA gateL
+
TOTALXL · multi-week, focused team
+
+
+

A full-app redesign across ~40 routes and 14 + subsystems. Dominant cost drivers: ~183 toast call sites (app-wide behavioral refactor), ~20 Recharts + components (re-theme/rebuild), and the page-by-page Card→.card / + status→.tag / form→.field conversion across every route. + De-risked substantially by (a) complete on-contract mockups for 12 screens + nothing.css shipping + nearly every needed class, and (b) the app already shipping red as --destructive, so the accent reuses an existing + token while the green --primary collapses to greyscale. + ~60% of the leverage lands from a handful of shared-surface edits (P0–P3).

+
+ +
+

AI Developer Hub · Spec 028 — Nothing Design Redesign · Implementation Plan · + Styled with the Nothing design system (tokens only). Dark default; flip the toggle top-right.

+
+
+ + diff --git a/specs/028-nothing-design-redesign/mockups/_CONTRACT.md b/specs/028-nothing-design-redesign/mockups/_CONTRACT.md new file mode 100644 index 00000000..3be6459f --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/_CONTRACT.md @@ -0,0 +1,175 @@ +# Mockup Authoring Contract — Nothing redesign + +Every screen mockup is a standalone `.html` file in this `mockups/` folder. It MUST link the shared +`./styles/nothing.css` and `./styles/theme.js` and reuse the shell + component classes below verbatim. +This file is the single source of consistency. Do not invent new color/spacing values, new fonts, or +ad-hoc component styles. If a screen needs something truly bespoke, add a small scoped ` + + +
+ + +
+
+
+
LICENSES
+

License Assignments

+
+
+ + + + Bulk Import + + +
+ + +
+
+
+ + + + +
+
+ ACTIVE LICENSE ASSIGNMENTS + 142assignments + Track user-to-tool license assignments +
+
+
MONTHLY SNAPSHOT$2,684.00
+
MANAGED BY SYNC38
+
REVOKED · 30D6
+
+
+ +
+ + +
+ + + + + + + + + + + + + +
+ +
+ + +
+
+ SHOWING 1–5 OF 142 · SORTED BY ASSIGNED ↓ + + 136 Active + 6 Revoked + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
UserToolTierMonthly CostStatusSourceWorkspace + Assigned + + + + Actions
+ + TS + + Tobias Studer + tobias.studer@unic.com + + + Anthropic APIBoost · usage-billed$0.00ActiveManualdefault-workspaceMay 19, 2026 + + + + + + + + +
+ + AU + + Admin User + admin@unic.com + + + CursorTeam$40.00ActiveManualteam-alphaMar 01, 2026 + + + + + + + +
+ + MR + + Marco Rossi + marco.rossi@unic.com + + + ClaudePro$20.00ActiveManualFeb 02, 2026 + + + + + + + +
+ + SC + + Sarah Chen + sarah.chen@unic.com + + + GitHub CopilotBusiness$19.00 + + Active + Sync + + Copilot Syncunic-engineeringJan 14, 2026 + + + + + Managed by sync + +
+ + LF + + Lena Fischer + lena.fischer@unic.com + + + GitHub CopilotBusiness$19.00 + + Revoked + Sync + + Copilot Syncunic-engineeringNov 08, 2025 + + + + + Managed by sync + +
+
+ + +
+ + ROWS PER PAGE + + + + + + + + + PAGE 1 OF 15 + + + + + + + + + +
+
+ + + +
+ +
+
+ + diff --git a/specs/028-nothing-design-redesign/mockups/budget.html b/specs/028-nothing-design-redesign/mockups/budget.html new file mode 100644 index 00000000..137da8e4 --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/budget.html @@ -0,0 +1,436 @@ + + + + + + AI Developer Hub — Budget · Nothing + + + + +
+ + +
+
+
+
SPEND TRACKING
+

Budget

+
+
+ All budgets + + + New Budget + +
+ + +
+
+
+ + + + + + + +
+
+ ANNUAL AI TOOL BUDGET +
+ FY 2026 Budget + Active +
+
+ Monthly allocation +
+ + +
+ +
+
+
+ At risk + Variance +$3,200.00 through May 2026 +
+

+ Tracking $3,200.00 over the closed-period plan — + actual $52,300.00 vs $49,100.00 expected through May. + Projected to land under ceiling. +

+
+
+ ANNUAL CEILING + 240,000USD + $18,500.00 unallocated +
+
+ + +
+ SPEND vs CEILING · FY 2026 + Ceiling $240,000.00 +
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + +
+ Actual YTD · $100,520.00 + Allocated remaining + Planned YTD · $100,000.00 + Expected through May 2026 · $49,100.00 +
+ +
+ + +
+
+ BILLED YTD +
+ 96,400USD + + $4,120.00 running API +
+
+
+ ACTUAL YTD +
+ 100,520USD + $8,300.00 in Jun 2026 so far +
+
+
+ PROJECTED YEAR-END +
+ 231,800USD + $8,200.00 under ceiling +
+
+
+ VARIANCE THRU MAY 2026 +
+ +3,200USD + $52,300.00 vs $49,100.00 expected +
+
+
+
+ + +
+
+ PAST MONTH SPOTLIGHT +
+ May 2026 — $19,800.00 actual vs $20,000.00 planned + On track · within plan +
+
+
+ PLANNED + $20,000.00 + Allocated +
+
+ EXPECTED + $18,500.00 + From license assignments +
+
+ ACTUAL + $19,800.00 + $18,940.00 billed + $860.00 API +
+
+ VARIANCE % + +7.0% + +$1,300.00 vs expected +
+
+
+
+ + +
+
+ PERIOD ALLOCATIONS & BILLED COSTS + 12 monthly periods · click to expand line items +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PeriodPlannedExpectedActualVariance% Diff
+ + Jan 2026$18,500.00$18,940.00-$420.00-2% + + + +
+
+
+ + GitHub Copilot Business — March invoice + Ref: INV-2026-001 · 2026-03-31 + + + $4,200.00 + + + +
+ +
+ + + Anthropic API + [LIVE] + + Workspace: Default · Updated Jun 2, 2026 14:05 + + $4,120.00 (API) +
+
+
+ + May 2026$18,500.00$21,300.00+$2,800.00+15% + +
+ + + + Jun 2026 + Current + + $18,500.00$8,300.00 (API) + +
Total through May 2026$238,000.00$92,500.00$96,640.00+$4,140.00
+ + +
+ Inline-edit any Planned cell, then save. Archived budgets are read-only. +
+ Allocations exceed budget by $4,000.00 + +
+
+
+
+ + +
+
+ DIALOGS & STATES + Add / edit billed cost · delete · empty +
+ +
+ +
+
+ Add Billed Cost + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ Add Cost / Save Changes + +
+
+
+ +
+ +
+ CONFIRM · ALERTDIALOG +

Delete billed cost?

+

+ This will permanently remove the billed cost entry + “GitHub Copilot Business - March invoice” + ($4,200.00). This action cannot be undone. +

+
+ + +
+
+ + +
+ EMPTY · NO ACTIVE BUDGET +
+
No active budget for the current fiscal year.
+
Annual AI tool budget planning.
+ +
+
+
+
+
+ +
+
+ + diff --git a/specs/028-nothing-design-redesign/mockups/claude.html b/specs/028-nothing-design-redesign/mockups/claude.html new file mode 100644 index 00000000..bb3cc785 --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/claude.html @@ -0,0 +1,593 @@ + + + + + + AI Developer Hub — Claude Console · Nothing + + + + + +
+ + +
+
+
+
ANTHROPIC API
+

Claude Console

+

Org-wide usage, budgets, and Anthropic sync status.

+
+
+ + + Synced 12m ago · next in 48m + + + +
+ + +
+
+
+ + + + + + + +
+ CLAUDE CONSOLE · WORKSPACES +
+ + +
+
+ + +
+
+
+ CLAUDE API SPENDING · TOTAL · JUN 2026 + $18,420.55USD · MTD billed +
+ + + + $612.40 est. today + + + + + +14% vs prior month + +
+
+ Today is an estimate — shown as a dashed ghost bar, never merged into billed actuals. +
+
+ + +
ORG KPI STRIP
+
+ +
+ TOTAL · JUN 2026 +
+ $18,420.55 + incl. +$612.40 est. today +
+
+ +
+ MOM DELTA +
+ +$2,310.00 + + + +14% vs prior month + +
+
+ + +
+ PROJECTED MONTH-END +
+ $31,800.00 + + + + 106% of $30,000.00 · incl. est. today + +
+
+ +
+ WORKSPACES OVER 80% +
+ 2/ 9 + Platform Eng · 112% +
+
+ +
+ + +
+
+
+
Daily spend by workspace
+
Stacked · top 8 workspaces + Other · $18,420.55 this period
+
+
+ USE WORKSPACE COLORS + +
+
+ +
+
+ $600$300$0 +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ Jun 124681012141618202224262728293031Jun 1Today +
+ + +
+ Platform Eng + Data Science + Default Workspace · Other + Today (est.) ≈ $612 +
+
+ + +
+ + +
+
+
+
Historical trend
+
Last 12 months · monthly totals, pacing vs prior months
+
+
+
+
+ + + +
+
+ Platform Eng +96% + QA Automation +143% +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ JulSepNovJanMarJun +
+
+ Jan $12.4k → Jun $18.4k · dashed = projected month-end overlay +
+
+ + +
+
+
+
Organization Billing
+
Monthly billing budget · thresholds at 80% / 100%
+
+ +
+ +
+
+ CURRENT SPEND + $18,420.55 · 61% of $30,000.00 +
+
+ PROJECTED MONTH-END + $31,800.00 · 106% of budget +
+
+ MONTHLY BUDGET + $30,000.00 +
+
+ +
+
+ UTILIZATION · +$612.40 EST. TODAY + 61% +
+ +
+ + + + + +
+
+ Credit balance is not exposed by the Anthropic API — view in console. +
+
+
+ +
+ + +
+
+
+
Workspace Budgets
+
Spend / limit · utilization · 6-month trend · per-workspace limits
+
+
+ HIDE $0 + NO LIMIT + +
+
+ +
+ + +
+
+
+ + Platform Eng +
+
+ Over budget + 112% + Synced +
+
+
$7,240.00 of $6,500.00
+
+ 112% + + + + + on pace $9,800 (151%) +
+
+ + + + +96% + +
+
+
+ + +
+
+
+ + Data Science +
+
+ 82% + Synced +
+
+
$4,110.00 of $5,000.00
+
+ 82% + + + + + on pace $5,560 (111%) +
+
+ + + + +181% + +
+
+
+ + +
+
+ +
+ Default + Synced +
+
+
$980.00 of no limit
+
+ no limit + + + + + no projection +
+
+ + flat +
+
+
+ + +
+
+ +
+ new + Stale +
+
+
$1,250.00 of $2,000.00
+
+ 63% + + + + + on pace $1,690 (85%) +
+
+ + + + +143% + +
+
+
+ +
+ + + +
+ + + + +
+
+ + diff --git a/specs/028-nothing-design-redesign/mockups/copilot.html b/specs/028-nothing-design-redesign/mockups/copilot.html new file mode 100644 index 00000000..03466e24 --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/copilot.html @@ -0,0 +1,425 @@ + + + + + + AI Developer Hub — GitHub Copilot · Nothing + + + + + +
+ + +
+
+
+
INTEGRATIONS / GITHUB
+

GitHub Copilot

+

GitHub Copilot usage analytics, seat management, and billing insights.

+
+
+ +
+ + +
+
+
+ + + + + + + +
+ +
+
Copilot metrics are stale — last data 9 days ago (2026-05-24)
+
GitHub finalizes Copilot usage with a ~3 day lag, so a small gap is expected. A nine-day gap usually means the sync stalled or the token is missing the read:org scope. Check integration status or run a backfill.
+
+
+ + +
+
+ ACCEPTANCE RATE · LAST 28 DAYS + 31% + + + ↑ 2.4 pts vs prior 28 days + +
+
+

Suggestions accepted across the last 28 days

+

The headline efficiency signal that sits beside Total Seats and Active Seats. Acceptance rate is the share of inline Copilot suggestions developers chose to keep — the clearest measure of whether the investment is translating into shipped code.

+
+
SUGGESTIONS · 28D1,284,902
+
ACCEPTANCES · 28D398,320
+
+
+
+ + +

OVERVIEW

+
+
+ TOTAL SEATS + +
142
+
+
+ ACTIVE SEATS + +
118
+ 83% utilization +
+
+ ACCEPTANCE RATE + +
31%
+
+
+ SUGGESTIONS + +
1,284,902
+
+
+ ACCEPTANCES + +
398,320
+
+
+ + +
+
+
+

Usage Trends

+

Daily Copilot suggestions, acceptances, and active users

+
+ 28D +
+ +
+ + + + + + + + + + +
+ May 5May 12May 19May 26 +
+
+ +
+ Suggestions + Acceptances + Active Users +
+ +

May 19 — 46,210 suggestions / 14,890 acceptances / 96 users

+
+ + +

SEATS · TAB

+
+
+ 142 synced license assignments + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
UserMatched UserPlanStatusAssigned
+ + OC + + + Alex RiveraBusinessActive3/14/2026
+ + MD + + + UnmatchedBusinessPending4/02/2026
+ + HJ + + + Jane ParkEnterpriseActive1/21/2026
+
+ + +

BILLING OVERVIEW · TAB

+
+
+ CURRENT MONTH COST + +
$2,820.00
+
+
+ CUMULATIVE COST + +
$19,740.00
+
+
+ COST / ACTIVE USER + +
$23.90
+
+
+ PLAN + +
Business
+ 142 total / 118 active seats +
+
+ + +
+

Monthly Billing Details

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MonthCostSeatsActiveCost / User
Mar 2026$2,820.00142118$23.90
Feb 2026$2,760.00138115$24.00
Jan 2026$2,640.00132109$24.22
+
+ + +

ANALYTICS · TAB

+
+ +
+ BY LANGUAGE +
+
+
TYPESCRIPT42%
+
+ +
+
+
+
PYTHON27%
+
+ +
+
+
+
JAVASCRIPT19%
+
+ +
+
+
+
GO12%
+
+ +
+
+
+
+ + +
+ BY EDITOR +
+
+
VS CODE71%
+
+ +
+
+
+
JETBRAINS21%
+
+ +
+
+
+
NEOVIM8%
+
+ +
+
+
+
+ ACTIVITY DISTRIBUTION +
+
POWER USERS38
+
REGULAR52
+
OCCASIONAL28
+
INACTIVE24
+
+
+
+ + +
+
+ UTILIZATION TREND · ACTIVE USERS VS TOTAL SEATS + 83% +
+
+ + + + + +
May 5May 12May 19May 26
+
+
+ Active Users + Total Seats +
+
+ +
+
+ + diff --git a/specs/028-nothing-design-redesign/mockups/dashboard.html b/specs/028-nothing-design-redesign/mockups/dashboard.html new file mode 100644 index 00000000..60b2f5fa --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/dashboard.html @@ -0,0 +1,568 @@ + + + + + + AI Developer Hub — Dashboard · Nothing + + + + + +
+ + +
+
+
+
OVERVIEW
+

Dashboard

+
+
+ + + + + Preview as viewer + + + + All sources synced · 12m ago + + + + Open Budget report + + + + +
+ + +
+
+
+ + + + +
+ + +
+
+
+ + + Budget health · on track +
+ Within budget +
+ +
+ YTD BILLED + $184,250 + 61.4% of $300,000 ceiling +
+ + +
+ Actual YTD · projected year-end · ceiling + $291,400 proj. +
+
+ + + + + + + + +
+
+ $0 + Projected $291.4K + Ceiling $300K +
+ +
+ +

+ Linear trend lands at $291,400 by year-end — within the + $300,000 ceiling. +

+
+ Sep 2025 was 4.2% under plan + + Open Budget report + + + +
+
+ + +
+
+ This month · October 2025 + +5.9% over +
+ +
+
+ Actual so far + $26,480 +
+
+ Billed + $22,150 +
+
+ Anthropic API + $4,330 +
+
+ Planned + $25,000 +
+
+ Variance + +$1,480 +
+
+ +
+ 28 of 31 days elapsed · pace projects $29.3K full month +
+
+ + +
+
+ Key indicators + Month over month · vs September 2025 +
+ +
+
+ Active users +
+ 142 + across 6 teams +
+
+ +
+ Active tools +
+ 9 + all sources synced +
+
+ +
+ Active licenses +
+ 318 + + + + +12 · vs 306 last month + +
+
+ +
+ Expected monthly +
+ $27,940 + + + + +8.2% · vs $25,820 last month + +
+
+ Copilot acceptance 31% · 87 devs +
+
+
+ + +
+
+
+ +
+
+
+ Stacked monthly cost · last 12 months + Licenses (billed) + Anthropic API (running) +
+ avg $17,420/mo +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NovDecJanFebMarApr + MayJunJulAugSepOct +
+ Fading bars represent future periods with no billed data yet +
+ + +
+
+ YTD billed + $184,250 +
+ +
+
+
+ Licenses (billed) + $159,300 +
+
+ + + + +
+ 86% of YTD +
+ +
+
+ Anthropic API (running) + $24,950 +
+
+ + + +
+ 14% of YTD +
+
+
+ +
+
+
+ + +
+ What changed this month +
+ +
+
+ + +
+
Budget on track
+

Projected $291.4K for the year — 97% of the $300.0K ceiling, with headroom to spare.

+
+
+
+ +
+
+ + +
+
GitHub Copilot seats grew 72 → 87
+

+15 seats added this month — a 21% month-over-month increase in active assignments.

+
+
+
+ +
+
+ + +
+
Expected monthly spend ↑ 8.2%
+

Up from $25.8K to $27.9K vs last month, driven mostly by new Copilot seats. Sep 2025 was 4.2% under plan.

+
+
+
+ + +
+
+ + +
+
Anthropic workspace approaching limit
+

Engineering is at 92% of its monthly cap with 3 days left in the period. Review allocation before it blocks usage.

+
+
+
+ +
+
+ + +
+ + +
+
+ Top tools by monthly cost + View all 9 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ToolVendorActive usersTotal monthly cost
GitHub CopilotGitHub87$1,653.00
Anthropic APIAnthropic34$4,330.00
CursorAnysphere52$1,040.00
ChatGPT EnterpriseOpenAI61$1,525.00
Perplexity ProPerplexity28$560.00
+
+ + +
+
+ Recent activity + History +
+ +
+
+ + + + Invoice from Anthropic — Added · $4,330 + + + 2h ago +
+ +
+ + + + Jordan Lee assigned GitHub Copilot + + + 5h ago +
+ +
+ + + + Invoice from GitHub — Added · $1,272 + + + Yesterday +
+ +
+ + + + Priya Nair revoked from Cursor + + + 2d ago +
+ +
+ + + + Invoice acme-oct.pdfFiltered (duplicate) + + + 3d ago +
+
+
+
+ + +
+ Jump to + +
+ +
+
+ + diff --git a/specs/028-nothing-design-redesign/mockups/index.html b/specs/028-nothing-design-redesign/mockups/index.html new file mode 100644 index 00000000..b9c628dc --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/index.html @@ -0,0 +1,236 @@ + + + + + + AI Developer Hub — Nothing Redesign · Gallery + + + + + + + + diff --git a/specs/028-nothing-design-redesign/mockups/invoices.html b/specs/028-nothing-design-redesign/mockups/invoices.html new file mode 100644 index 00000000..722818d9 --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/invoices.html @@ -0,0 +1,269 @@ + + + + + + AI Developer Hub — Invoices · Nothing + + + + +
+ + +
+
+
+
BILLING / INVOICE ARCHIVE
+

Invoices

+
+ +
+ + + + +
+ ARCHIVED INVOICES + 42invoices + Total PDF invoices uploaded and archived · admin-only view, sorted newest first +
+ + +
+
+
+ INVOICE ARCHIVE TABLE + 42 archived · showing 1–5 of 42 +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Invoice Number + + + + Date + + + + Amount + + + + Vendor + + + Budget PeriodUploaded ByDownload
INV-1042Mar 3, 2026$12,480.00Anthropic2026-Q1Tobias Studer + + + +
INV-2087Feb 28, 2026$3,250.50GitHub2026-02Tobias Studer + + + +
INV-9921Feb 14, 2026$899.00Cursor2026-Q1Admin + + + +
INV-1043Jan 31, 2026$12,480.00Anthropic2026-01Tobias Studer + + + +
INV-7755Jan 9, 2026$540.00 + + + +
+ + +
+
+ ROWS PER PAGE +
+ + + + +
+
+
+ + + + PAGE 1 / 5 + + + +
+
+
+ +
+ + +
+ + +
+ ROW ACTION · DOWNLOAD +
+
+ CONTROL + + + + Ghost icon button + + +
+
+ TOOLTIP + Download +
+
+ ARIA-LABEL + Download invoice INV-1042 +
+
+ TARGET + /api/invoices/{id}/pdf +
+
+
+ + +
+ EMPTY STATE · NO INVOICES +
+
No invoices archived yet.
+
Shown only when the archive is empty
+ + + Upload your first invoice + +
+
+
+ + +
+ UPLOAD & SYNC STATUS · FROM /invoices/new & /invoices/bulk +
+ Synced + Linked + Unlinked · — + Duplicate + Low confidence + High confidence +
+

+ Single upload runs AI field extraction with confidence highlighting on Invoice Number / Date / Amount / Vendor; a duplicate triggers a "Duplicate Invoice Detected" dialog (Skip vs Overwrite). Bulk upload accepts a ZIP of PDFs and returns a sync-results review. +

+
+ +
+
+ + diff --git a/specs/028-nothing-design-redesign/mockups/login.html b/specs/028-nothing-design-redesign/mockups/login.html new file mode 100644 index 00000000..79cb313b --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/login.html @@ -0,0 +1,175 @@ + + + + + + AI Developer Hub — Sign In · Nothing + + + + + +
+
+ +
+ + + + + +
+ + +
+ +
+ +
+ AI DEVELOPER HUB +

Welcome back

+

Sign in to your AI Developer Hub account

+
+ +
+ + + + +
+ + +
+ +
+ + +
+ + +
+ +
+ Access is invite-only. New here? Check your email for an invite link. +
+ +
+
+ + +
+ SECURE SESSION · NEXTAUTH +
+ + +
+
+ +
+
+ + diff --git a/specs/028-nothing-design-redesign/mockups/reports.html b/specs/028-nothing-design-redesign/mockups/reports.html new file mode 100644 index 00000000..466cdcf7 --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/reports.html @@ -0,0 +1,633 @@ + + + + + + AI Developer Hub — Reports · Nothing + + + + + +
+ + +
+
+
+
ANALYTICS
+

Reports

+

Tool adoption, license utilization, and spending insights

+
+
+ Overview + Budget report +
+ + +
+
+
+ + + + + + + + + +
+
+ ACTIVE USERS +
+ 142 +
+
+ +
+ ACTIVE TOOLS +
+ 6 +
+
+ +
+ ACTIVE LICENSES +
+ 318 + + 12 + + +
+

vs 306 last month

+
+ +
+ EXPECTED MONTHLY +
+ $48,920 + + 6.4% + + +
+ + + + + +
+ JanFebMarAprMayNow +
+

Licenses only — API usage not included

+
+
+ + + + +
+
+
+ + + + +
+
+ BUDGET HEALTH + Within budget +
+ + $184,200USD YTD SPEND +

+ YTD spend is $184,200 — 61.4% of the $300,000 ceiling. + Linear trend lands at $278,400 by year-end, within the ceiling. + Apr 2026 was 4.2% under plan. +

+
+
+ + Open Budget report + + +
+
+ + + + +
+
+ What changed this month + Auto-generated from license + spend data · static rules (v1) +
+
+ +
+
+ + + +
+ Budget on track + Projected $278.4k (93% of $300.0k). +
+
+
+ +
+
+ + + +
+ GitHub Copilot grew 88 → 96 seats + ▲ 8 seats vs last month (9% MoM). +
+
+
+ +
+
+ + + +
+ Expected monthly spend rose + ▲ 6.4%$46.0k$48.9k vs last month. +
+
+
+ +
+
+ + + +
+ Apr 2026 was 4.2% under plan + ▼ 4.2% vs the planned monthly amount. +
+
+
+
+
+ + + + +
+
+ Tool adoption + Active assignments and expected monthly cost · MoM license delta +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ToolVendorActive usersMoMExpected monthly
GitHub CopilotGitHub96+8$1,824.00
CursorAnysphere74+5$2,960.00
ClaudeAnthropic58−2$1,160.00
ChatGPT EnterpriseOpenAI42±0$2,520.00
GeminiGoogle26+3$520.00
+
+ + + + +
+
+ Spend by circle + License distribution and expected cost by team · sorted by monthly cost +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CircleUsersLicensesExpected cost$ / user
Engineering64142$21,840.00$341.25
Product2861$9,120.00$325.71
Data & AI1948$7,640.00$402.11
Design1427$3,980.00$284.29
Unassigned1740$6,340.00$372.94
+
+ + + + + +
+
+ BUDGET TAB + /reports/budget +
+ + +
+
+
+ FY 2026 budget at a glance + 5 of 12 periods complete · Actual = billed + running API +
+ On track +
+ + +
+ UTILIZATION · 61.4% OF CEILING CONSUMED YTD + $184,200 / $300,000 +
+
+ + + + + + + +
+
+ 0Projected $278.4kCeiling $300k +
+ + +
+
+ ANNUAL CEILING + $300,000 +

12 × $25,000 planned

+
+
+ ACTUAL YTD + $184,200 +

$176,400 billed + $7,800 API

+
+
+ AVG MONTHLY RUN-RATE + $36,840 +

vs $25,000 planned (+47.4%)

+
+
+ PROJECTED YEAR-END + $278,400 +

within $300,000 ceiling

+
+
+
+ + +
+
+ PAST MONTH SPOTLIGHT + May 2026 — $23,960 actual vs $25,000 planned +
+ +
+
+ PLANNED + $25,000 + Allocated monthly +
+
+ ACTUAL + $23,960 + $21,200 billed + $2,760 API +
+
+ VARIANCE + −4.2% + −$1,040 vs plan +
+
+ + +
+
PLANNED$25,000
+
+ + + + + + + +
+
BILLED + API$23,960
+
+ + + + + + + +
+
+ Planned + Billed + API +
+
+ + + TOP PER-TOOL CHANGES VS LAST MONTH +
+
+ GITHUB COPILOT + $1,680 → $1,824 ↑ +$144 (+8.6%) +
+
+ CLAUDE + $1,240 → $1,160 ↓ −$80 (−6.5%) +
+
+
+ + +
+
+ Plan vs actual — FY 2026 + + Planned + Billed + API (running) + Forecast + +
+ + +
+ + +
+ + + + + + + + + + + + + + +
+
+
+ JanFebMar + AprMayJun +
+

+ Y-axis: $25k / $50k… · dashed line = planned monthly average. + Actual bars turn red when over plan — none over plan this fiscal year. +

+
+ + +
+
+ Forecast — cumulative spend + + Actual cumulative + Forecast cumulative + +
+ + + + + + + + + + + + +
+ Ceiling $300,000 + Actual May $184,200 · Forecast Dec $278,400 +
+
+ JanMarMayJulSepDec +
+

+ Solid = actual, dashed = linear-regression forecast. Below 3 completed months this shows: + “The forecast requires at least 3 months of completed spending.” +

+
+ + +
+
+ Per-tool breakdown + Sorted by YTD spent · license-derived + running-API attribution +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ToolYTD spentCurrent monthlyProjected EOYShare
Cursor$26,400$2,960$44,16028.4%
GitHub Copilot$19,920$1,824$31,10421.4%
ChatGPT Enterprise$17,640$2,520$32,76019.0%
+ + + + Anthropic API + Running API + + $7,800$2,760$26,6408.4%
↳ Workspace · Production$1,940
↳ Workspace · Research$820
+

+ License-derived rows reflect active seat assignments. The Anthropic API row is metered from running + usage and expands to a workspace-level breakdown. +

+
+ + +
+
+
No active budget
+
When no budget exists for the fiscal year, the Budget tab shows this state.
+ + + Create a budget + +
+
+
+ +
+
+ + diff --git a/specs/028-nothing-design-redesign/mockups/requests.html b/specs/028-nothing-design-redesign/mockups/requests.html new file mode 100644 index 00000000..4295572f --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/requests.html @@ -0,0 +1,351 @@ + + + + + + AI Developer Hub — Access Requests · Nothing + + + + +
+ + +
+
+
+
INBOX / APPROVALS
+

Access Requests

+
+
+ + +
+ + +
+
+
+ + + + +
+
+ PENDING REVIEW + 7requests +
+

+ Routed from Microsoft Forms via Power Automate. Any admin can claim any request — first to act wins. +

+
+ +
+ + +
+
+ + + + + + +
+
+ ROWS +
+ + + + +
+
+
+ + +
+
+ LICENSE REQUESTS · NEWEST FIRST + Showing 1–10 of 42 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDRequesterTool / tierStatusDecided byAge
REQ-042 + + Mara Holzer + mara.holzer@unic.com + + + + GitHub Copilot + Business + + Pending review3 hours agoOpen
REQ-041 + + Tobias Studer + tobias.studer@unic.com + + + + Claude + Team + + ApprovedLena Brunner1 day agoOpen
REQ-039 + + David Kümin + david.kuemin@unic.com + + + + Cursor + Pro + + CompletedLena Brunner4 days agoOpen
REQ-036 + + Anita Frei + anita.frei@unic.com + + + + ChatGPT Enterprise + Standard + + RejectedTobias Studer6 days agoOpen
REQ-031 + + Pascal Meier + pascal.meier@unic.com + + GitHub CopilotCancelled2 weeks agoOpen
REQ-029 + + Sofia Bianchi + sofia.bianchi@unic.com + + + + Claude + Pro + + ApprovedTobias Studer2 weeks agoOpen
REQ-027 + + Jonas Widmer + jonas.widmer@unic.com + + + + GitHub Copilot + Business + + CompletedLena Brunner3 weeks agoOpen
REQ-024 + + Elena Vogt + elena.vogt@unic.com + + + + Cursor + Pro + + CompletedTobias Studer1 month agoOpen
REQ-022 + + Marco Steiner + marco.steiner@unic.com + + + + ChatGPT Enterprise + Standard + + RejectedLena Brunner1 month agoOpen
REQ-019 + + Nadia Keller + nadia.keller@unic.com + + GitHub CopilotCancelled2 months agoOpen
+ + +
+ Page 1 of 5 +
+ + +
+
+
+ + +
+
+ ROW → DETAIL DRILL-IN + /requests/{id} +
+
+
+ WHAT THE DETAIL PAGE SHOWS +
+
STATUS BADGEPending review
+
REQUESTER CARDName · Email · Hub match · Assignment #
+
REQUEST PAYLOADTool · Tier · Raw Forms fields
+
SENT MESSAGESApproval · Completion · Rejection (Teams)
+
+
+
+ ACTION BAR · FIRST-WRITE-WINS +

+ Any admin can act on this request. First-write-wins — the first decision is recorded and the + request leaves the pending queue. Admin-only route (AuthGuard requiredRole = "admin"). +

+
+ Approve + Complete + Cancel request + Reject +
+
+
+
+ +
+
+ + diff --git a/specs/028-nothing-design-redesign/mockups/settings.html b/specs/028-nothing-design-redesign/mockups/settings.html new file mode 100644 index 00000000..f40edd9d --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/settings.html @@ -0,0 +1,531 @@ + + + + + + AI Developer Hub — Settings · Nothing + + + + + +
+ + +
+
+
+
SETTINGS · SYNC STATUS
+

Settings

+
+
+
+ + LIVE · 30s POLL +
+ + +
+ + +
+
+
+ + + + + + + +
+
+
+ SYNC SOURCES HEALTHY + 5 / 6sources + One source partial after last run · last successful sync 12 minutes ago +
+ +
+
+ SOURCE OUTCOMES · LAST RUN + 6 SOURCES +
+ +
+ 4 success + 1 partial + 1 never synced +
+
+
+
+ +
+ + +
+
+ Scheduled Jobs + Auto-refresh · 5s fast poll while any source is running +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SourceScheduleLast RunStatusCreatedUpdatedSkippedActions
GitHub Copilot BillingDaily at 6:00 AM UTC12 minutes agoSuccess0843
Anthropic API UsageEvery 6 hours1 hour agoSuccess142180
Anthropic API CostsDaily at 7:00 AM UTC1 hour agoPartial3052
GitHub MembersManual only2 days agoSuccess4110
Invoice-Period MatchingManual only3 hours agoSuccess6212 + +
Claude Team InvoicesManual onlyNever syncedNeverVia upload
+
+
+ + +
+
+ Sync History + 20 most recent manual sync events +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SourceTriggered ByRun TimeStatusCreatedUpdatedSkipped
GitHub Copilot BillingTobias StuderJun 2, 2026, 9:14 AMSuccess0843
Anthropic API CostsTobias StuderJun 2, 2026, 7:02 AMPartial3052
Invoice-Period MatchingTobias StuderJun 2, 2026, 6:30 AMSuccess6212
GitHub MembersTobias StuderMay 31, 2026, 4:48 PMSuccess4110
Anthropic API UsageTobias StuderMay 30, 2026, 11:05 AM[ FAILED ]000
+
+
+ +
+ + + + +
+
+ Appearance · Theme + [ SAVED ] +
+
+
+ Theme + Select your preferred color scheme. +
+
+ + + +
+
+
+ + +
+
+ Integrations +
+
+ + +
+
+
+ GITHUB ORGANIZATION + unic-com +
+ Connected +
+ +
+
CONNECTEDMar 12, 2026
+
LAST SYNCEDJun 2, 2026
+
SCOPES REQUIREDread:org · read:user · manage_billing:copilot
+
+ +
+ + +
+ +
+ + +
+
+ + +
+
+
+ CLAUDE CODE + Anthropic API +
+ Connected +
+ +
+
WORKSPACEUnic AI Platform
+
LAST CHECKEDJun 2, 2026, 8:55 AM
+
ADMIN KEYANTHROPIC_ADMIN_API_KEY · set
+
+ +

+ Status is read-only. When unconfigured, set the + ANTHROPIC_ADMIN_API_KEY environment variable + to enable Anthropic API cost tracking. +

+
+
+
+ + +
+
+ GitHub Member Sync + Overlay · opened from GitHub Members · Sync Now +
+ +
+ +
+
+
GITHUB MEMBERS56 total
+
SYSTEM USERS50 total
+
LAST MEMBER SYNCMay 31, 2026, 4:48 PM
+
STATUSConnected
+
+
+ + + +
+
+ +
+
+ + diff --git a/specs/028-nothing-design-redesign/mockups/styles/nothing.css b/specs/028-nothing-design-redesign/mockups/styles/nothing.css new file mode 100644 index 00000000..9598fd07 --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/styles/nothing.css @@ -0,0 +1,463 @@ +/* ============================================================================= + NOTHING DESIGN SYSTEM — AI Developer Hub redesign + Single source of truth for all mockups. Dark + light, monochrome + one red. + Fonts: Doto (hero/display), Space Grotesk (UI/body), Space Mono (data/labels). + ============================================================================= */ + +@import url('https://fonts.googleapis.com/css2?family=Doto:wght@400;500;700&family=Space+Grotesk:wght@300;400;500;700&family=Space+Mono:wght@400;700&display=swap'); + +/* --------------------------------------------------------------------------- + 1. TOKENS — dark is the default canvas; light is a first-class override + --------------------------------------------------------------------------- */ +:root { + /* greyscale (dark) */ + --black: #000000; + --surface: #111111; + --surface-raised: #1a1a1a; + --border: #222222; + --border-visible: #333333; + --text-disabled: #666666; + --text-secondary: #999999; + --text-primary: #e8e8e8; + --text-display: #ffffff; + + /* accent + status (identical across modes) */ + --accent: #d71921; + --accent-subtle: rgba(215, 25, 33, 0.15); + --success: #4a9e5c; + --warning: #d4a843; + --error: #d71921; + --info: #999999; + --interactive: #5b9bf6; + + /* segmented-bar empty track */ + --seg-empty: #222222; + + /* fonts */ + --font-display: "Doto", "Space Mono", monospace; + --font-ui: "Space Grotesk", "DM Sans", system-ui, sans-serif; + --font-mono: "Space Mono", "JetBrains Mono", "SF Mono", monospace; + + /* type scale */ + --display-xl: 72px; + --display-lg: 48px; + --display-md: 36px; + --heading: 24px; + --subheading: 18px; + --body: 16px; + --body-sm: 14px; + --caption: 12px; + --label: 11px; + + /* spacing (8px base) */ + --space-2xs: 2px; + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + --space-xl: 32px; + --space-2xl: 48px; + --space-3xl: 64px; + --space-4xl: 96px; + + /* radii */ + --radius-tech: 4px; + --radius-compact: 8px; + --radius-card: 14px; + --radius-pill: 999px; + + /* motion */ + --ease: cubic-bezier(0.25, 0.1, 0.25, 1); + --dur-micro: 180ms; + --dur-trans: 320ms; + + --sidebar-w: 248px; +} + +:root[data-theme="light"] { + --black: #f5f5f5; + --surface: #ffffff; + --surface-raised: #f0f0f0; + --border: #e8e8e8; + --border-visible: #cccccc; + --text-disabled: #999999; + --text-secondary: #666666; + --text-primary: #1a1a1a; + --text-display: #000000; + --interactive: #007aff; + --seg-empty: #e0e0e0; + --accent-subtle: rgba(215, 25, 33, 0.10); +} + +/* --------------------------------------------------------------------------- + 2. RESET + BASE + --------------------------------------------------------------------------- */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { font-size: 16px; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } + +body { + background: var(--black); + color: var(--text-primary); + font-family: var(--font-ui); + font-weight: 400; + line-height: 1.5; + letter-spacing: 0; + transition: background var(--dur-trans) var(--ease), color var(--dur-trans) var(--ease); +} + +a { color: inherit; text-decoration: none; } +button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; } +ul { list-style: none; } +svg { display: block; } + +::selection { background: var(--accent); color: #fff; } + +/* --------------------------------------------------------------------------- + 3. TYPOGRAPHY UTILITIES + --------------------------------------------------------------------------- */ +.doto { font-family: var(--font-display); font-weight: 500; letter-spacing: -0.02em; } +.mono { font-family: var(--font-mono); } +.ui { font-family: var(--font-ui); } + +/* instrument-panel label: Space Mono, ALL CAPS, tracked out */ +.label { + font-family: var(--font-mono); + font-size: var(--label); + line-height: 1.2; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-secondary); +} +.caption { + font-family: var(--font-mono); + font-size: var(--caption); + letter-spacing: 0.04em; + color: var(--text-secondary); +} + +/* display hero numbers */ +.display-xl { font-family: var(--font-display); font-size: var(--display-xl); line-height: 1.0; letter-spacing: -0.03em; color: var(--text-display); } +.display-lg { font-family: var(--font-display); font-size: var(--display-lg); line-height: 1.05; letter-spacing: -0.02em; color: var(--text-display); } +.display-md { font-family: var(--font-ui); font-size: var(--display-md); line-height: 1.1; letter-spacing: -0.02em; font-weight: 500; color: var(--text-display); } +.heading { font-family: var(--font-ui); font-size: var(--heading); line-height: 1.2; letter-spacing: -0.01em; font-weight: 500; color: var(--text-display); } +.subheading { font-family: var(--font-ui); font-size: var(--subheading); line-height: 1.3; font-weight: 400; color: var(--text-primary); } + +/* text-color helpers */ +.t-display { color: var(--text-display); } +.t-primary { color: var(--text-primary); } +.t-secondary { color: var(--text-secondary); } +.t-disabled { color: var(--text-disabled); } +.t-accent { color: var(--accent); } +.t-success { color: var(--success); } +.t-warning { color: var(--warning); } + +/* --------------------------------------------------------------------------- + 4. APP SHELL — sidebar + main + --------------------------------------------------------------------------- */ +.app { display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 100vh; } + +.sidebar { + position: sticky; top: 0; align-self: start; + height: 100vh; + display: flex; flex-direction: column; + border-right: 1px solid var(--border); + padding: var(--space-xl) var(--space-lg); + background: var(--black); +} + +.brand { + display: flex; align-items: center; gap: var(--space-sm); + margin-bottom: var(--space-2xl); +} +.brand .dot { width: 9px; height: 9px; border-radius: var(--radius-pill); background: var(--accent); } +.brand .wordmark { + font-family: var(--font-mono); + font-size: var(--body-sm); + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-display); +} + +.nav { display: flex; flex-direction: column; gap: var(--space-2xs); } +.nav-section { margin-top: var(--space-lg); margin-bottom: var(--space-xs); padding-left: var(--space-sm); } +.nav-item { + position: relative; + display: flex; align-items: center; gap: var(--space-sm); + padding: 10px var(--space-sm); + font-family: var(--font-mono); + font-size: var(--label); + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-disabled); + border-radius: var(--radius-tech); + transition: color var(--dur-micro) var(--ease), background var(--dur-micro) var(--ease); +} +.nav-item svg { width: 16px; height: 16px; stroke-width: 1.5; opacity: 0.7; } +.nav-item:hover { color: var(--text-primary); } +.nav-item.active { color: var(--text-display); } +.nav-item.active::before { + content: ""; position: absolute; left: -1px; top: 50%; transform: translateY(-50%); + width: 2px; height: 18px; background: var(--accent); +} +.nav-item.active svg { opacity: 1; } + +.sidebar-footer { margin-top: auto; padding-top: var(--space-lg); border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: var(--space-xs); } +.user-chip { display: flex; align-items: center; gap: var(--space-sm); padding: var(--space-sm); } +.avatar { width: 32px; height: 32px; border-radius: var(--radius-pill); border: 1px solid var(--border-visible); display: grid; place-items: center; font-family: var(--font-mono); font-size: var(--caption); color: var(--text-primary); } + +/* main scroll area */ +.main { min-width: 0; padding: var(--space-2xl) var(--space-2xl) var(--space-4xl); max-width: 1280px; } + +/* topbar: page title + actions */ +.topbar { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-lg); margin-bottom: var(--space-2xl); } +.topbar .eyebrow { margin-bottom: var(--space-sm); } +.topbar-actions { display: flex; align-items: center; gap: var(--space-md); flex-shrink: 0; } + +/* --------------------------------------------------------------------------- + 5. THEME TOGGLE (segmented [ DARK | LIGHT ]) + --------------------------------------------------------------------------- */ +.theme-toggle { display: inline-flex; border: 1px solid var(--border-visible); border-radius: var(--radius-pill); overflow: hidden; } +.theme-toggle button { + padding: 8px 16px; + font-family: var(--font-mono); font-size: var(--label); letter-spacing: 0.1em; text-transform: uppercase; + color: var(--text-secondary); + transition: color var(--dur-micro) var(--ease), background var(--dur-micro) var(--ease); +} +.theme-toggle button.active { background: var(--text-display); color: var(--black); } + +/* --------------------------------------------------------------------------- + 6. CARDS / SURFACES + --------------------------------------------------------------------------- */ +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-card); + padding: var(--space-lg); +} +.card--raised { background: var(--surface-raised); } +.card--bare { background: transparent; border: none; padding: 0; } +.card--pad-lg { padding: var(--space-xl); } +.card-label { display: block; margin-bottom: var(--space-md); } + +.grid { display: grid; gap: var(--space-md); } +.grid--2 { grid-template-columns: repeat(2, 1fr); } +.grid--3 { grid-template-columns: repeat(3, 1fr); } +.grid--4 { grid-template-columns: repeat(4, 1fr); } +.grid--kpi { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); } +@media (max-width: 1080px) { .grid--3, .grid--4 { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 720px) { .grid--2, .grid--3, .grid--4 { grid-template-columns: 1fr; } } + +/* --------------------------------------------------------------------------- + 7. METRIC / HERO NUMBER + --------------------------------------------------------------------------- */ +.metric { display: flex; flex-direction: column; gap: var(--space-sm); } +.metric-value { font-family: var(--font-mono); font-size: var(--display-lg); line-height: 1; letter-spacing: -0.02em; color: var(--text-display); display: flex; align-items: baseline; gap: var(--space-xs); } +.metric-value.is-doto { font-family: var(--font-display); } +.metric-unit { font-family: var(--font-mono); font-size: var(--label); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-secondary); } +.metric-trend { font-family: var(--font-mono); font-size: var(--caption); display: inline-flex; align-items: center; gap: 4px; } +.metric-hero .metric-value { font-size: var(--display-xl); } + +/* --------------------------------------------------------------------------- + 8. STAT ROWS / LISTS + --------------------------------------------------------------------------- */ +.stat-list { display: flex; flex-direction: column; } +.stat-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); padding: 14px 0; border-bottom: 1px solid var(--border); } +.stat-row:last-child { border-bottom: none; } +.stat-row .label { color: var(--text-secondary); } +.stat-row .value { font-family: var(--font-mono); font-size: var(--body-sm); color: var(--text-primary); display: inline-flex; align-items: baseline; gap: 4px; } +.value--success { color: var(--success); } +.value--warning { color: var(--warning); } +.value--accent { color: var(--accent); } +.value--display { color: var(--text-display); } +.indent { padding-left: var(--space-lg); } + +/* --------------------------------------------------------------------------- + 9. SEGMENTED PROGRESS BAR (signature viz) + --------------------------------------------------------------------------- */ +.seg-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: var(--space-sm); } +.seg-bar { display: flex; gap: 2px; height: 10px; width: 100%; } +.seg-bar--hero { height: 18px; } +.seg-bar--compact { height: 5px; } +.seg { flex: 1; background: var(--seg-empty); } +.seg.is-filled { background: var(--text-display); } +.seg.is-good { background: var(--success); } +.seg.is-warn { background: var(--warning); } +.seg.is-over { background: var(--accent); } + +/* --------------------------------------------------------------------------- + 10. BUTTONS + --------------------------------------------------------------------------- */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: var(--space-sm); + min-height: 44px; padding: 12px 24px; + font-family: var(--font-mono); font-size: 13px; letter-spacing: 0.06em; text-transform: uppercase; + border-radius: var(--radius-pill); + transition: background var(--dur-micro) var(--ease), border-color var(--dur-micro) var(--ease), color var(--dur-micro) var(--ease); +} +.btn svg { width: 16px; height: 16px; stroke-width: 1.5; } +.btn--primary { background: var(--text-display); color: var(--black); } +.btn--primary:hover { background: var(--text-primary); } +.btn--secondary { border: 1px solid var(--border-visible); color: var(--text-primary); } +.btn--secondary:hover { border-color: var(--text-primary); } +.btn--ghost { color: var(--text-secondary); border-radius: 0; padding: 12px 0; } +.btn--ghost:hover { color: var(--text-primary); } +.btn--destructive { border: 1px solid var(--accent); color: var(--accent); } +.btn--destructive:hover { background: var(--accent-subtle); } +.btn--sm { min-height: 36px; padding: 8px 16px; font-size: var(--label); } +.btn--icon { padding: 0; width: 44px; height: 44px; } + +/* circular back button */ +.back-btn { width: 44px; height: 44px; border-radius: var(--radius-pill); border: 1px solid var(--border-visible); display: grid; place-items: center; color: var(--text-primary); transition: border-color var(--dur-micro) var(--ease); } +.back-btn:hover { border-color: var(--text-primary); } + +/* --------------------------------------------------------------------------- + 11. TAGS / CHIPS / STATUS + --------------------------------------------------------------------------- */ +.tag { + display: inline-flex; align-items: center; gap: 6px; + padding: 4px 12px; + border: 1px solid var(--border-visible); border-radius: var(--radius-pill); + font-family: var(--font-mono); font-size: var(--caption); letter-spacing: 0.06em; text-transform: uppercase; + color: var(--text-secondary); +} +.tag--tech { border-radius: var(--radius-tech); } +.tag--active { border-color: var(--text-display); color: var(--text-display); } +.tag--success { border-color: var(--success); color: var(--success); } +.tag--warning { border-color: var(--warning); color: var(--warning); } +.tag--accent { border-color: var(--accent); color: var(--accent); } +.tag .led { width: 6px; height: 6px; border-radius: var(--radius-pill); background: currentColor; } + +/* inline status text [SAVED] */ +.status-text { font-family: var(--font-mono); font-size: var(--caption); letter-spacing: 0.06em; color: var(--text-secondary); } +.status-text--ok { color: var(--success); } +.status-text--err { color: var(--accent); } + +/* --------------------------------------------------------------------------- + 12. TABLES + --------------------------------------------------------------------------- */ +.table { width: 100%; border-collapse: collapse; } +.table thead th { + text-align: left; padding: 0 16px 12px; + font-family: var(--font-mono); font-size: var(--label); letter-spacing: 0.1em; text-transform: uppercase; + color: var(--text-secondary); font-weight: 400; + border-bottom: 1px solid var(--border-visible); +} +.table tbody td { padding: 14px 16px; border-bottom: 1px solid var(--border); font-size: var(--body-sm); color: var(--text-primary); vertical-align: middle; } +.table tbody tr:last-child td { border-bottom: none; } +.table .num { text-align: right; font-family: var(--font-mono); } +.table th.num { text-align: right; } +.table tr--active td, .table tr.is-active td { background: var(--surface-raised); } +.table tr.is-active td:first-child { box-shadow: inset 2px 0 0 var(--accent); } +.table tbody tr { transition: background var(--dur-micro) var(--ease); } +.table tbody tr:hover td { background: var(--surface); } +[data-theme="light"] .table tbody tr:hover td { background: var(--surface-raised); } +.cell-strong { color: var(--text-display); font-family: var(--font-ui); } + +/* --------------------------------------------------------------------------- + 13. SEGMENTED CONTROL / TABS + --------------------------------------------------------------------------- */ +.segmented { display: inline-flex; border: 1px solid var(--border-visible); border-radius: var(--radius-compact); overflow: hidden; } +.segmented button { padding: 9px 18px; font-family: var(--font-mono); font-size: var(--label); letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-secondary); transition: background var(--dur-micro) var(--ease), color var(--dur-micro) var(--ease); } +.segmented button.active { background: var(--text-display); color: var(--black); } + +/* text tab bar */ +.tabs { display: flex; gap: var(--space-xl); border-bottom: 1px solid var(--border); } +.tab { position: relative; padding: 0 0 14px; font-family: var(--font-mono); font-size: var(--label); letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-disabled); transition: color var(--dur-micro) var(--ease); } +.tab:hover { color: var(--text-primary); } +.tab.active { color: var(--text-display); } +.tab.active::after { content: ""; position: absolute; left: 0; bottom: -1px; width: 100%; height: 2px; background: var(--accent); } + +/* period nav < LABEL > */ +.period-nav { display: inline-flex; align-items: center; gap: var(--space-md); } +.period-nav .nav-arrow { width: 32px; height: 32px; display: grid; place-items: center; color: var(--text-secondary); } +.period-nav .nav-arrow:hover { color: var(--text-primary); } +.period-nav .period-label { font-family: var(--font-mono); font-size: var(--body-sm); letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-display); min-width: 140px; text-align: center; } + +/* --------------------------------------------------------------------------- + 14. TOGGLES / SWITCHES + --------------------------------------------------------------------------- */ +.switch { position: relative; width: 46px; height: 26px; border-radius: var(--radius-pill); background: var(--border-visible); transition: background var(--dur-micro) var(--ease); flex-shrink: 0; } +.switch::after { content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: var(--radius-pill); background: var(--text-disabled); transition: transform var(--dur-micro) var(--ease), background var(--dur-micro) var(--ease); } +.switch.on { background: var(--text-display); } +.switch.on::after { transform: translateX(20px); background: var(--black); } + +/* --------------------------------------------------------------------------- + 15. INPUTS / FIELDS + --------------------------------------------------------------------------- */ +.field { display: flex; flex-direction: column; gap: var(--space-sm); } +.field > label { color: var(--text-secondary); } +.field .control { + width: 100%; padding: 10px 0; background: transparent; + border: none; border-bottom: 1px solid var(--border-visible); + color: var(--text-display); font-family: var(--font-mono); font-size: var(--body); + transition: border-color var(--dur-micro) var(--ease); +} +.field .control::placeholder { color: var(--text-disabled); } +.field .control:focus { outline: none; border-bottom-color: var(--text-primary); } +.field.is-error .control { border-bottom-color: var(--accent); } +.field .error-msg { font-family: var(--font-mono); font-size: var(--caption); color: var(--accent); } +.field--boxed .control { border: 1px solid var(--border-visible); border-radius: var(--radius-compact); padding: 12px 14px; } + +/* select / search */ +.search { display: inline-flex; align-items: center; gap: var(--space-sm); border: 1px solid var(--border-visible); border-radius: var(--radius-pill); padding: 8px 16px; color: var(--text-secondary); } +.search input { background: none; border: none; outline: none; color: var(--text-primary); font-family: var(--font-mono); font-size: var(--body-sm); min-width: 180px; } +.search svg { width: 16px; height: 16px; stroke-width: 1.5; } + +/* --------------------------------------------------------------------------- + 16. DIVIDERS / MISC LAYOUT + --------------------------------------------------------------------------- */ +.divider { height: 1px; background: var(--border); border: none; margin: var(--space-xl) 0; } +.section-gap { margin-top: var(--space-2xl); } +.row { display: flex; align-items: center; gap: var(--space-md); } +.row--between { justify-content: space-between; } +.row--wrap { flex-wrap: wrap; } +.stack { display: flex; flex-direction: column; } +.stack--sm { gap: var(--space-sm); } +.stack--md { gap: var(--space-md); } +.stack--lg { gap: var(--space-lg); } +.spacer { flex: 1; } + +/* --------------------------------------------------------------------------- + 17. DOT-MATRIX MOTIF + --------------------------------------------------------------------------- */ +.dot-grid { background-image: radial-gradient(circle, var(--border-visible) 1px, transparent 1px); background-size: 16px 16px; } +.dot-grid-subtle { background-image: radial-gradient(circle, var(--border) 0.5px, transparent 0.5px); background-size: 12px 12px; } + +/* --------------------------------------------------------------------------- + 18. CHART HELPERS (mockup-scale; designers use inline SVG for lines) + --------------------------------------------------------------------------- */ +.bars { display: flex; align-items: flex-end; gap: 6px; height: 140px; } +.bars .bar { flex: 1; background: var(--text-display); min-height: 2px; } +.bars .bar.is-muted { background: var(--border-visible); } +.bars .bar.is-accent { background: var(--accent); } +.axis { display: flex; justify-content: space-between; margin-top: var(--space-sm); } +.axis span { font-family: var(--font-mono); font-size: var(--label); letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-disabled); } + +.gauge-num { font-family: var(--font-mono); color: var(--text-display); } + +/* concentric ring legend dot */ +.legend { display: flex; flex-wrap: wrap; gap: var(--space-md) var(--space-lg); } +.legend-item { display: inline-flex; align-items: center; gap: var(--space-sm); font-family: var(--font-mono); font-size: var(--caption); letter-spacing: 0.04em; color: var(--text-secondary); } +.legend-item .swatch { width: 10px; height: 10px; } + +/* --------------------------------------------------------------------------- + 19. EMPTY / LOADING + --------------------------------------------------------------------------- */ +.empty { text-align: center; padding: var(--space-4xl) var(--space-xl); } +.empty .empty-title { font-family: var(--font-ui); font-size: var(--subheading); color: var(--text-secondary); margin-bottom: var(--space-sm); } +.empty .empty-sub { font-family: var(--font-mono); font-size: var(--caption); color: var(--text-disabled); } +.loading-text { font-family: var(--font-mono); font-size: var(--caption); letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-secondary); } + +/* --------------------------------------------------------------------------- + 20. AUTH (centered, chrome-less) + --------------------------------------------------------------------------- */ +.auth-screen { min-height: 100vh; display: grid; place-items: center; padding: var(--space-xl); } +.auth-card { width: 100%; max-width: 400px; } + +/* --------------------------------------------------------------------------- + 21. REDUCED MOTION + --------------------------------------------------------------------------- */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { transition-duration: 1ms !important; animation-duration: 1ms !important; } +} diff --git a/specs/028-nothing-design-redesign/mockups/styles/theme.js b/specs/028-nothing-design-redesign/mockups/styles/theme.js new file mode 100644 index 00000000..088cf5df --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/styles/theme.js @@ -0,0 +1,31 @@ +/* Nothing redesign mockups — dark/light theme toggle. + Dark is the default canvas. Choice persists across mockup pages via localStorage. */ +(function () { + var KEY = "nd-mock-theme"; + var root = document.documentElement; + + function apply(theme) { + if (theme === "light") root.setAttribute("data-theme", "light"); + else root.removeAttribute("data-theme"); // dark = absence of attribute + try { localStorage.setItem(KEY, theme); } catch (e) {} + document.querySelectorAll(".theme-toggle button").forEach(function (b) { + b.classList.toggle("active", b.dataset.theme === theme); + }); + } + + // restore before paint + var saved = "dark"; + try { saved = localStorage.getItem(KEY) || "dark"; } catch (e) {} + apply(saved); + + document.addEventListener("click", function (e) { + var btn = e.target.closest(".theme-toggle button"); + if (btn) apply(btn.dataset.theme); + }); + + // re-sync toggle button state once DOM is ready (script may load in ) + document.addEventListener("DOMContentLoaded", function () { + var current = root.getAttribute("data-theme") === "light" ? "light" : "dark"; + apply(current); + }); +})(); diff --git a/specs/028-nothing-design-redesign/mockups/tools.html b/specs/028-nothing-design-redesign/mockups/tools.html new file mode 100644 index 00000000..ac063ae0 --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/tools.html @@ -0,0 +1,370 @@ + + + + + + AI Developer Hub — AI Tools · Nothing + + + + +
+ + +
+
+
+
AI TOOL REGISTRY
+

AI Tools

+

Manage your AI tool registry

+
+
+ + + Add Tool + +
+ + +
+
+
+ + + + +
+
+ TOOLS IN REGISTRY + 6tools +
+
+ ACTIVE LICENSES IN USE + 122 +
+
+ + +
+ +
+ STATUS +
+ + + +
+ VENDOR + +
+
+ + +
+
+ AI TOOLS TABLE + 6 of 6 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameVendorActive LicensesStatusActions
GitHub CopilotGitHub42Active +
+ + + + + + + +
+
Claude APIAnthropic30Active +
+ + + + + + + +
+
CursorAnysphere25Active +
+ + + + + + + +
+
ChatGPT EnterpriseOpenAI18Active +
+ + + + + + + +
+
Gemini Code AssistGoogle7Active +
+ + + + + + + +
+
TabnineTabnine0Archived +
+ + + + + + + +
+
+ + +
+
+ ROWS PER PAGE + +
+
+ Page 1 of 1 + + +
+
+
+ + +
+ ROW ACTION — ARCHIVE CONFIRMATION +
+ +
+
+ Archive GitHub Copilot? +

+ This tool has 42 active license(s). Revoke them before archiving. +

+
+
+ + +
+
+ +
+
+ Archive Tabnine? +

+ This will archive the tool and make it unavailable for new assignments. +

+
+
+ [ TOOL ARCHIVED ] +
+ + +
+
+
+
+
+ + +
+
+ ADD NEW TOOL · /TOOLS/NEW + Admin only +
+ +
+ +
+ TOOL DETAILS +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ ACCESS TIERS + +
+
+
+ TIER 1 + at least one required +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+
+ Cancel + +
+
+
+
+ + +
+ NO-RESULTS STATE +
+
+
No results.
+
No tools match the active search and filters.
+
+
+
+ +
+
+ + diff --git a/specs/028-nothing-design-redesign/mockups/users.html b/specs/028-nothing-design-redesign/mockups/users.html new file mode 100644 index 00000000..904535d6 --- /dev/null +++ b/specs/028-nothing-design-redesign/mockups/users.html @@ -0,0 +1,290 @@ + + + + + + AI Developer Hub — Users · Nothing + + + + + +
+ + +
+
+
+
DIRECTORY
+

Users

+

Manage company user directory

+
+ +
+ + + + +
+
+ USERS PENDING SETUP + 4 users + Have not set a password yet · invite links valid 72h +
+
+ + + Send Invites to All Pending (4) + + +
+
+ +
+ + +
+ + + + + + + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameEmailCircleDisciplineRoleProfileSetup StatusStatusActions
Anna Kelleranna.keller@unic.comZurich DigitalDeveloperviewerboostPendingActive + + + + + +
Marco Brunnermarco.brunner@unic.comBern PlatformConceptionadminmaxedPendingActive + + + + + +
Sophie Vogelsophie.vogel@unic.comLausanne CloudBusinessviewerindiePendingActive + + + + + +
Tobias Studertobias.studer@unic.comZurich DigitalDeveloperadminPendingActive + + + + + +
Lukas Meierlukas.meier@unic.comDeveloperviewerActiveInactive + + + +
+ +
+ 5 of 5 users +
+ ROWS +
+ + + + +
+
+ + Page 1 / 1 + +
+
+
+
+
+ + +
+
+ CONFIRMATION DIALOGS + admin-only · destructive actions require confirm +
+
+
+ SEND INVITES TO ALL PENDING +
+ Send invites to all pending users? + This will send invite emails to 4 user(s) who have not yet set up their password. Each user receives a unique invite link valid for 72 hours. +
+ + +
+
+
+
+ DEACTIVATE USER +
+ Deactivate this user? + Deactivating revokes all active license assignments. Reset Password sends a fresh setup link. Both actions open a confirmation dialog first. +
+ + +
+
+
+
+
+ +
+
+ + diff --git a/src/app/globals.css b/src/app/globals.css index 9ed3eef4..9249d6b7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -4,6 +4,17 @@ @custom-variant dark (&:where(.dark, .dark *)); +/* ============================================================================= + NOTHING DESIGN SYSTEM — token layer for AI Developer Hub (spec 028). + shadcn semantic var names are kept and re-pointed onto Nothing values so every + primitive keeps resolving. Light is the :root default (printed-manual canvas), + dark is the .dark override (instrument panel) — next-themes attribute="class". + + Collision note: shadcn's --accent is a NEUTRAL hover background, NOT the loud + Nothing red. The single red interrupt + all error/destructive states route + through --destructive (#d71921). Status greens/ambers are value-text-only. + ============================================================================= */ + @theme inline { --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); @@ -12,6 +23,13 @@ --radius-2xl: calc(var(--radius) + 8px); --radius-3xl: calc(var(--radius) + 12px); --radius-4xl: calc(var(--radius) + 16px); + + /* Three-family Nothing stack, chained to the next/font-injected variables + (font-specific names dodge a circular --font-sans: var(--font-sans)). */ + --font-sans: var(--font-space-grotesk), "DM Sans", system-ui, sans-serif; + --font-mono: var(--font-space-mono), "JetBrains Mono", "SF Mono", ui-monospace, monospace; + --font-display: var(--font-doto), var(--font-space-mono), monospace; + --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); @@ -27,6 +45,7 @@ --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); + --color-destructive-subtle: var(--destructive-subtle); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); @@ -43,75 +62,113 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + + /* Nothing extras shadcn lacks: 100% ink, 40% faint, status, segmented track. */ + --color-ink: var(--ink); + --color-faint: var(--faint); + --color-success: var(--success); + --color-warning: var(--warning); + --color-seg-empty: var(--seg-empty); + --color-border-strong: var(--input); + --color-interactive: var(--interactive); } +/* --------------------------------------------------------------------------- + LIGHT — :root default. Nothing "printed manual". + --------------------------------------------------------------------------- */ :root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.78 0.19 120); - --primary-foreground: oklch(0.18 0 0); - --secondary: oklch(0.96 0 0); - --secondary-foreground: oklch(0.20 0 0); - --muted: oklch(0.96 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.95 0.03 120); - --accent-foreground: oklch(0.20 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.91 0 0); - --input: oklch(0.91 0 0); - --ring: oklch(0.78 0.19 120); - --chart-1: oklch(0.78 0.19 120); - --chart-2: oklch(0.55 0.15 250); - --chart-3: oklch(0.65 0.18 45); - --chart-4: oklch(0.50 0.15 300); - --chart-5: oklch(0.60 0.12 180); - --sidebar: oklch(0.98 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.78 0.19 120); - --sidebar-primary-foreground: oklch(0.18 0 0); - --sidebar-accent: oklch(0.96 0 0); - --sidebar-accent-foreground: oklch(0.20 0 0); - --sidebar-border: oklch(0.91 0 0); - --sidebar-ring: oklch(0.78 0.19 120); + --radius: 0.5rem; /* technical scale; cards override to 14px in card.tsx */ + + --background: #f5f5f5; /* Nothing --black (light canvas) */ + --foreground: #1a1a1a; /* --text-primary */ + --card: #ffffff; /* --surface */ + --card-foreground: #1a1a1a; + --popover: #f0f0f0; /* --surface-raised (dropdowns/popovers) */ + --popover-foreground: #1a1a1a; + --primary: #000000; /* --text-display — greyscale fill, NOT a hue */ + --primary-foreground: #f5f5f5; /* --black */ + --secondary: #f0f0f0; /* --surface-raised */ + --secondary-foreground: #1a1a1a; + --muted: #ffffff; /* --surface */ + --muted-foreground: #666666; /* --text-secondary */ + --accent: #f0f0f0; /* shadcn hover bg — NEUTRAL surface-raised */ + --accent-foreground: #1a1a1a; + --destructive: #d71921; /* THE one interrupt + all errors */ + --border: #e8e8e8; /* --border (dividers) */ + --input: #cccccc; /* --border-visible (interactive edges) */ + --ring: #d71921; /* focus = red, passes both modes */ + --chart-1: #1a1a1a; + --chart-2: #555555; + --chart-3: #888888; + --chart-4: #aaaaaa; + --chart-5: #c8c8c8; + --sidebar: #f5f5f5; /* sidebar == canvas */ + --sidebar-foreground: #1a1a1a; + --sidebar-primary: #000000; + --sidebar-primary-foreground: #f5f5f5; + --sidebar-accent: #f0f0f0; + --sidebar-accent-foreground: #000000; + --sidebar-border: #e8e8e8; + --sidebar-ring: #d71921; + + /* Nothing extras */ + --ink: #000000; /* --text-display (100%) */ + --faint: #999999; /* --text-disabled (40%) */ + --success: #2e7d32; /* darkened for ≥4.5:1 value text on light (see notes) */ + --warning: #946c00; /* darkened for ≥4.5:1 value text on light (see notes) */ + --seg-empty: #e0e0e0; + --destructive-subtle: rgba(215, 25, 33, 0.10); + --accent-subtle: rgba(215, 25, 33, 0.10); + --interactive: #007aff; + + --font-ui: var(--font-sans); /* mockup-vocabulary alias */ } +/* --------------------------------------------------------------------------- + DARK — .dark override. Nothing "instrument panel" (OLED black). + --------------------------------------------------------------------------- */ .dark { - --background: oklch(0.16 0 0); - --foreground: oklch(0.97 0 0); - --card: oklch(0.21 0 0); - --card-foreground: oklch(0.98 0 0); - --popover: oklch(0.21 0 0); - --popover-foreground: oklch(0.98 0 0); - --primary: oklch(0.78 0.19 120); - --primary-foreground: oklch(0.18 0 0); - --secondary: oklch(0.25 0 0); - --secondary-foreground: oklch(0.98 0 0); - --muted: oklch(0.25 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.25 0.03 120); - --accent-foreground: oklch(0.98 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.78 0.19 120); - --chart-1: oklch(0.80 0.19 120); - --chart-2: oklch(0.65 0.17 250); - --chart-3: oklch(0.72 0.18 45); - --chart-4: oklch(0.62 0.16 300); - --chart-5: oklch(0.68 0.14 180); - --sidebar: oklch(0.21 0 0); - --sidebar-foreground: oklch(0.98 0 0); - --sidebar-primary: oklch(0.78 0.19 120); - --sidebar-primary-foreground: oklch(0.18 0 0); - --sidebar-accent: oklch(0.25 0 0); - --sidebar-accent-foreground: oklch(0.98 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.78 0.19 120); + --background: #000000; + --foreground: #e8e8e8; + --card: #111111; + --card-foreground: #e8e8e8; + --popover: #1a1a1a; + --popover-foreground: #e8e8e8; + --primary: #ffffff; + --primary-foreground: #000000; + --secondary: #1a1a1a; + --secondary-foreground: #e8e8e8; + --muted: #111111; + --muted-foreground: #999999; + --accent: #1a1a1a; /* neutral hover */ + --accent-foreground: #e8e8e8; + --destructive: #d71921; + --border: #222222; + --input: #333333; + --ring: #d71921; + --chart-1: #ffffff; + --chart-2: #cccccc; + --chart-3: #999999; + --chart-4: #666666; + --chart-5: #444444; + --sidebar: #000000; + --sidebar-foreground: #e8e8e8; + --sidebar-primary: #ffffff; + --sidebar-primary-foreground: #000000; + --sidebar-accent: #1a1a1a; + --sidebar-accent-foreground: #ffffff; + --sidebar-border: #222222; + --sidebar-ring: #d71921; + + /* Nothing extras (spec values pass on black) */ + --ink: #ffffff; + --faint: #666666; + --success: #4a9e5c; + --warning: #d4a843; + --seg-empty: #222222; + --destructive-subtle: rgba(215, 25, 33, 0.15); + --accent-subtle: rgba(215, 25, 33, 0.15); + --interactive: #5b9bf6; } @layer base { @@ -119,7 +176,7 @@ @apply border-border outline-ring/50; } body { - @apply bg-background text-foreground; + @apply bg-background text-foreground font-sans antialiased; } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1c60e9ae..20e5e226 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import { Space_Grotesk, Space_Mono, Doto } from "next/font/google"; import { headers } from "next/headers"; import { auth } from "@/lib/auth"; import { isPublicPath } from "@/lib/routes"; @@ -16,7 +16,30 @@ import { getActiveAlerts } from "@/actions/alerts"; import { AlertBanner } from "@/components/alert-banner"; import "./globals.css"; -const inter = Inter({ subsets: ["latin"] }); +// Nothing three-family stack — self-hosted via next/font (auto fallback metrics), +// NOT the mockup's render-blocking Google @import. Space Grotesk is the UI/body +// default and the only preloaded family; Space Mono (data/labels) and Doto +// (36px+ hero only) are loaded but not preloaded. +const spaceGrotesk = Space_Grotesk({ + subsets: ["latin"], + variable: "--font-space-grotesk", + display: "swap", + preload: true, +}); +const spaceMono = Space_Mono({ + subsets: ["latin"], + weight: ["400", "700"], + variable: "--font-space-mono", + display: "swap", + preload: false, +}); +const doto = Doto({ + subsets: ["latin"], + variable: "--font-doto", + display: "swap", + preload: false, +}); + export const metadata: Metadata = { title: "AI Developer Hub", description: "AI Tool Access & Budget Tracker", @@ -37,7 +60,9 @@ export default async function RootLayout({ return ( - + Date: Tue, 2 Jun 2026 12:24:32 +0200 Subject: [PATCH 02/19] feat(design): P1 primitives + P2 app shell (spec 028 Nothing) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1 — re-skin all shadcn primitives/overlays in place to the Nothing look: pill Space-Mono buttons (greyscale fill), border-only tags w/ success/warning, boxed mono inputs, square greyscale checkbox/switch, flat shadowless overlays with fade-only animation, neutral in-content tab underline, mono-caps table headers (no zebra), border-only alert (no fill), select/command selected = left 2px red bar. New shared helpers: /useInlineStatus (inline [SAVED]/[ERROR] w/ aria-live) and /segmented spinner ([LOADING…]). P2 — replace SidebarProvider shell with a CSS-grid .app: fixed Nothing text-nav rail (AI·HUB wordmark + red dot, ALL-CAPS items, 2px red active bar) + mobile drawer; segmented theme toggle (light/dark/system) wired to next-themes; AlertBanner -> flat bordered status box (no red fill); neutral auth brand panel; Sonner flattened to a transitional Nothing toast (call-site removal in P4). Typecheck + build + lint all green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/(auth)/layout.tsx | 26 +-- src/app/globals.css | 40 ++++ src/app/layout.tsx | 20 +- src/components/alert-banner.tsx | 81 +++++--- src/components/app-sidebar.tsx | 307 +++++++++++++++++----------- src/components/theme-toggle.tsx | 82 ++++---- src/components/ui/alert-dialog.tsx | 6 +- src/components/ui/alert.tsx | 9 +- src/components/ui/badge.tsx | 25 ++- src/components/ui/button.tsx | 32 +-- src/components/ui/calendar.tsx | 14 +- src/components/ui/card.tsx | 4 +- src/components/ui/checkbox.tsx | 2 +- src/components/ui/command.tsx | 16 +- src/components/ui/dialog.tsx | 8 +- src/components/ui/dropdown-menu.tsx | 14 +- src/components/ui/form.tsx | 2 +- src/components/ui/input.tsx | 6 +- src/components/ui/label.tsx | 2 +- src/components/ui/loading-state.tsx | 51 +++++ src/components/ui/popover.tsx | 2 +- src/components/ui/select.tsx | 24 +-- src/components/ui/sheet.tsx | 14 +- src/components/ui/sonner.tsx | 24 ++- src/components/ui/status-text.tsx | 128 ++++++++++++ src/components/ui/switch.tsx | 4 +- src/components/ui/table.tsx | 8 +- src/components/ui/tabs.tsx | 12 +- src/components/ui/textarea.tsx | 2 +- src/components/ui/tooltip.tsx | 4 +- 30 files changed, 645 insertions(+), 324 deletions(-) create mode 100644 src/components/ui/loading-state.tsx create mode 100644 src/components/ui/status-text.tsx diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index 69e9f292..98f0afc2 100644 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -1,5 +1,3 @@ -import { Bot } from "lucide-react"; - export default function AuthLayout({ children, }: { @@ -7,23 +5,25 @@ export default function AuthLayout({ }) { return (
- {/* Decorative side panel — visible on large screens */} -
-
- - AI Developer Hub + {/* Decorative side panel — neutral surface + dot-matrix, NOT an accent fill */} +
+
+
-
-

+

+

“Centralise your AI tool budgets, licences, and usage data in one place — so your team can focus on building.”

-
- Budget Tracker · Licence Manager · Usage Reports +
+ Budget Tracker · Licence Manager · Usage Reports
-

- © {new Date().getFullYear()} AI Developer Hub +

+ © {new Date().getFullYear()} AI Developer Hub

diff --git a/src/app/globals.css b/src/app/globals.css index 9249d6b7..7ca9dd53 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -180,6 +180,46 @@ } } +/* --------------------------------------------------------------------------- + Namespaced Nothing helpers (nd-*) — collision-safe globals for idioms that + are awkward as pure Tailwind utilities. Component classes live as React + components; only these few mechanical bits are global. + --------------------------------------------------------------------------- */ +@layer components { + /* Dot-matrix motif — subtle background texture for empty/auth/hero surfaces. */ + .nd-dot-grid { + background-image: radial-gradient( + circle, + var(--input) 1px, + transparent 1px + ); + background-size: 16px 16px; + } + + /* Hardware-style segmented spinner — replaces skeleton shimmer / animate-pulse. */ + .nd-seg-spinner { + display: inline-flex; + gap: 3px; + align-items: center; + } + .nd-seg-spinner > i { + width: 4px; + height: 12px; + background: var(--ink); + opacity: 0.2; + animation: nd-seg-pulse 1.1s cubic-bezier(0.25, 0.1, 0.25, 1) infinite; + } + .nd-seg-spinner > i:nth-child(2) { animation-delay: 0.12s; } + .nd-seg-spinner > i:nth-child(3) { animation-delay: 0.24s; } + .nd-seg-spinner > i:nth-child(4) { animation-delay: 0.36s; } + .nd-seg-spinner > i:nth-child(5) { animation-delay: 0.48s; } +} + +@keyframes nd-seg-pulse { + 0%, 100% { opacity: 0.18; } + 45% { opacity: 1; } +} + /* ── Reduced Motion Kill-Switch ── */ @media (prefers-reduced-motion: reduce) { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 20e5e226..bb2f67d3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,11 +3,6 @@ import { Space_Grotesk, Space_Mono, Doto } from "next/font/google"; import { headers } from "next/headers"; import { auth } from "@/lib/auth"; import { isPublicPath } from "@/lib/routes"; -import { - SidebarProvider, - SidebarInset, - SidebarTrigger, -} from "@/components/ui/sidebar"; import { AppSidebar } from "@/components/app-sidebar"; import { ThemeProvider } from "@/components/theme-provider"; import { SessionProvider } from "@/components/session-provider"; @@ -71,19 +66,18 @@ export default async function RootLayout({ > {showSidebar ? ( - +
- -
- -
+
-
{children}
- - +
+ {children} +
+
+
) : ( children )} diff --git a/src/components/alert-banner.tsx b/src/components/alert-banner.tsx index dc74c6b1..2e3fd2a7 100644 --- a/src/components/alert-banner.tsx +++ b/src/components/alert-banner.tsx @@ -1,9 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Button } from "@/components/ui/button"; -import { AlertTriangle, X } from "lucide-react"; +import { cn } from "@/lib/utils"; import type { ActiveAlertsData } from "@/types"; const STORAGE_KEY = "alert-banner-dismissed"; @@ -15,6 +13,9 @@ function computeFingerprint(alerts: ActiveAlertsData): string { .join("|"); } +// Nothing alert: a FLAT bordered status box — no red background fill. The border +// carries severity (destructive critical / warning approaching); status color is +// on the value text only. Keeps the localStorage fingerprint dismiss + aria-live. export function AlertBanner({ alerts }: { alerts: ActiveAlertsData | null }) { const [dismissed, setDismissed] = useState(null); const [announced, setAnnounced] = useState(false); @@ -31,42 +32,66 @@ export function AlertBanner({ alerts }: { alerts: ActiveAlertsData | null }) { const fingerprint = computeFingerprint(alerts); if (dismissed === fingerprint) return null; + const hasCritical = alerts.workspaceAlerts.some( + (a) => a.severity === "critical" + ); + const handleDismiss = () => { localStorage.setItem(STORAGE_KEY, fingerprint); setDismissed(fingerprint); }; return ( -
+
- {announced && `${alerts.workspaceAlerts.length} budget alert${alerts.workspaceAlerts.length > 1 ? "s" : ""}`} + {announced && + `${alerts.workspaceAlerts.length} budget alert${ + alerts.workspaceAlerts.length > 1 ? "s" : "" + }`}
- - - Budget Alert - -
    - {alerts.workspaceAlerts.map((a, i) => ( -
  • - - {a.name} - - {" "}is at {a.utilizationPct}% of monthly budget - {a.severity === "critical" ? " — limit exceeded!" : " — approaching limit"} -
  • - ))} -
-
- -
+ [ X ] + +
); } diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index eb75e771..0c6efc9b 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -1,67 +1,136 @@ "use client"; +import { useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { - LayoutDashboard, - Wrench, - Users, - KeyRound, - DollarSign, - BarChart3, - Bot, - FileText, - Inbox, - Settings, - LogOut, - LogIn, - UserCircle, -} from "lucide-react"; +import { Menu, X } from "lucide-react"; import { signOut } from "next-auth/react"; -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; + import { ThemeToggle } from "@/components/theme-toggle"; +import { cn } from "@/lib/utils"; import type { UserRole } from "@/types"; type NavItem = { title: string; href: string; - icon: React.ComponentType<{ className?: string }>; roles: UserRole[]; }; const navItems: NavItem[] = [ - { title: "Dashboard", href: "/", icon: LayoutDashboard, roles: ["admin", "viewer"] }, - { title: "Tools", href: "/tools", icon: Wrench, roles: ["admin", "viewer"] }, - { title: "Users", href: "/users", icon: Users, roles: ["admin"] }, - { title: "Assignments", href: "/assignments", icon: KeyRound, roles: ["admin", "viewer"] }, - { title: "Requests", href: "/requests", icon: Inbox, roles: ["admin"] }, - { title: "Budget", href: "/budget", icon: DollarSign, roles: ["admin"] }, - { title: "Reports", href: "/reports", icon: BarChart3, roles: ["admin"] }, - { title: "Copilot", href: "/copilot", icon: Bot, roles: ["admin"] }, - { title: "Claude Console", href: "/claude", icon: Bot, roles: ["admin"] }, - { title: "Invoices", href: "/invoices", icon: FileText, roles: ["admin"] }, - { title: "Settings", href: "/settings/appearance", icon: Settings, roles: ["admin", "viewer"] }, + { title: "Dashboard", href: "/", roles: ["admin", "viewer"] }, + { title: "Tools", href: "/tools", roles: ["admin", "viewer"] }, + { title: "Users", href: "/users", roles: ["admin"] }, + { title: "Assignments", href: "/assignments", roles: ["admin", "viewer"] }, + { title: "Requests", href: "/requests", roles: ["admin"] }, + { title: "Budget", href: "/budget", roles: ["admin"] }, + { title: "Reports", href: "/reports", roles: ["admin"] }, + { title: "Copilot", href: "/copilot", roles: ["admin"] }, + { title: "Claude Console", href: "/claude", roles: ["admin"] }, + { title: "Invoices", href: "/invoices", roles: ["admin"] }, + { title: "Settings", href: "/settings/appearance", roles: ["admin", "viewer"] }, ]; +function initialsOf(name: string | null): string { + if (!name) return "?"; + return name + .split(/\s+/) + .filter(Boolean) + .slice(0, 2) + .map((p) => p[0]?.toUpperCase() ?? "") + .join(""); +} + +function isItemActive(pathname: string, href: string): boolean { + if (href === "/") return pathname === "/"; + // Settings nav points at /settings/appearance; treat any /settings/* as active. + if (href.startsWith("/settings")) return pathname.startsWith("/settings"); + return pathname === href || pathname.startsWith(href + "/") || pathname === href; +} + +function Brand() { + return ( +
+
+ ); +} + +function NavLinks({ + items, + pathname, + onNavigate, +}: { + items: NavItem[]; + pathname: string; + onNavigate?: () => void; +}) { + return ( + + ); +} + +function Footer({ + userName, + userRole, +}: { + userName: string | null; + userRole: string | null; +}) { + return ( +
+ + + {initialsOf(userName)} + + + {userName} + + {userRole} + + + + + +
+ ); +} + export function AppSidebar({ userName, userRole, @@ -70,89 +139,81 @@ export function AppSidebar({ userRole: string | null; }) { const pathname = usePathname(); + const [mobileOpen, setMobileOpen] = useState(false); const isAuthenticated = userName !== null && userRole !== null; - const filteredNavItems = isAuthenticated + const items = isAuthenticated ? navItems.filter((item) => item.roles.includes(userRole as UserRole)) : []; return ( - - -

AI Developer Hub

-
- + <> + {/* Desktop fixed rail */} + + + {/* Mobile top bar */} +
+ + +
+ + {/* Mobile drawer */} + {mobileOpen ? ( +
+ +
+ {isAuthenticated ? ( + <> + setMobileOpen(false)} + /> +
+ + ) : null} + +
+ ) : null} + ); } diff --git a/src/components/theme-toggle.tsx b/src/components/theme-toggle.tsx index 5ca58909..f164948a 100644 --- a/src/components/theme-toggle.tsx +++ b/src/components/theme-toggle.tsx @@ -1,53 +1,51 @@ "use client"; import { useEffect, useState } from "react"; -import { Moon, Sun } from "lucide-react"; import { useThemePreference } from "@/hooks/use-theme-preference"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; +import { cn } from "@/lib/utils"; -export function ThemeToggle() { - const [mounted, setMounted] = useState(false); - const { setTheme } = useThemePreference(); +const OPTIONS = [ + { value: "light", label: "Light" }, + { value: "dark", label: "Dark" }, + { value: "system", label: "Auto" }, +] as const; - useEffect(() => { - setMounted(true); - }, []); +// Nothing segmented theme toggle wired to next-themes (via useThemePreference, +// which persists to the DB). Three states retained: light / dark / system. +export function ThemeToggle({ className }: { className?: string }) { + const [mounted, setMounted] = useState(false); + const { theme, setTheme } = useThemePreference(); - if (!mounted) { - return ( - - ); - } + useEffect(() => setMounted(true), []); return ( - - - - - - setTheme("light")}> - Light - - setTheme("dark")}> - Dark - - setTheme("system")}> - System - - - +
+ {OPTIONS.map((opt) => { + const active = mounted && theme === opt.value; + return ( + + ); + })} +
); } diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx index dad32734..d3a35a26 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/components/ui/alert-dialog.tsx @@ -36,7 +36,7 @@ function AlertDialogOverlay({ svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border bg-transparent px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5", { variants: { variant: { - default: "bg-card text-card-foreground", + default: "border-input text-foreground [&>svg]:text-muted-foreground", destructive: - "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current", + "border-destructive text-destructive *:data-[slot=alert-description]:text-destructive [&>svg]:text-destructive", + warning: "border-warning text-warning [&>svg]:text-warning", }, }, defaultVariants: { @@ -39,7 +40,7 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
`-style dot +// in consumers when a status LED is wanted. const badgeVariants = cva( - "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", + "inline-flex w-fit shrink-0 items-center justify-center gap-1.5 overflow-hidden rounded-full border bg-transparent px-3 py-0.5 font-mono text-[11px] tracking-[0.06em] uppercase whitespace-nowrap transition-colors focus-visible:ring-2 focus-visible:ring-ring [&>svg]:pointer-events-none [&>svg]:size-3", { variants: { variant: { - default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", - outline: - "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - link: "text-primary underline-offset-4 [a&]:hover:underline", + default: "border-ink text-ink", + secondary: "border-input text-muted-foreground", + destructive: "border-destructive text-destructive", + outline: "border-input text-foreground", + ghost: "border-transparent text-muted-foreground", + link: "border-transparent text-foreground underline-offset-4 [a&]:hover:underline", + success: "border-success text-success", + warning: "border-warning text-warning", + active: "border-ink text-ink", }, }, defaultVariants: { diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 4d38506c..8f741dce 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -4,31 +4,33 @@ import { Slot } from "radix-ui" import { cn } from "@/lib/utils" +// Nothing buttons: pill, Space Mono, ALL CAPS, flat (no shadow). Primary is a +// GREYSCALE fill (white/black), never the red. Destructive is an outline in the +// shared --destructive red. Secondary/outline collapse onto one bordered variant. const buttonVariants = cva( - "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "inline-flex shrink-0 items-center justify-center gap-2 rounded-full font-mono text-[13px] font-normal tracking-[0.06em] uppercase whitespace-nowrap transition-all outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-40 aria-invalid:border-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: - "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + "border border-destructive bg-transparent text-destructive hover:bg-destructive-subtle", outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + "border border-input bg-transparent text-foreground hover:border-foreground", secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", + "border border-input bg-transparent text-foreground hover:border-foreground", + ghost: "text-muted-foreground hover:text-foreground", + link: "font-sans text-[14px] tracking-normal normal-case text-foreground underline-offset-4 hover:underline", }, size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-8", - "icon-lg": "size-10", + default: "h-11 px-6 has-[>svg]:px-5", + xs: "h-8 gap-1.5 px-4 text-[11px] has-[>svg]:px-3 [&_svg:not([class*='size-'])]:size-3.5", + sm: "h-9 gap-1.5 px-4 text-[11px] has-[>svg]:px-3", + lg: "h-11 px-8 has-[>svg]:px-6", + icon: "size-11", + "icon-xs": "size-8 [&_svg:not([class*='size-'])]:size-3.5", + "icon-sm": "size-9", + "icon-lg": "size-11", }, }, defaultVariants: { diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx index 47551459..28f2df92 100644 --- a/src/components/ui/calendar.tsx +++ b/src/components/ui/calendar.tsx @@ -74,7 +74,7 @@ function Calendar({ defaultClassNames.dropdowns ), dropdown_root: cn( - "relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50", + "relative rounded-md border border-input has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring", defaultClassNames.dropdown_root ), dropdown: cn( @@ -91,7 +91,7 @@ function Calendar({ table: "w-full border-collapse", weekdays: cn("flex", defaultClassNames.weekdays), weekday: cn( - "flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none", + "flex-1 rounded-md font-mono text-[11px] uppercase tracking-[0.1em] font-normal text-muted-foreground select-none", defaultClassNames.weekday ), week: cn("mt-2 flex w-full", defaultClassNames.week), @@ -100,7 +100,7 @@ function Calendar({ defaultClassNames.week_number_header ), week_number: cn( - "text-[0.8rem] text-muted-foreground select-none", + "font-mono text-[0.8rem] text-muted-foreground select-none", defaultClassNames.week_number ), day: cn( @@ -117,15 +117,15 @@ function Calendar({ range_middle: cn("rounded-none", defaultClassNames.range_middle), range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), today: cn( - "rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none", + "rounded-md border border-input data-[selected=true]:rounded-none", defaultClassNames.today ), outside: cn( - "text-muted-foreground aria-selected:text-muted-foreground", + "text-faint aria-selected:text-faint", defaultClassNames.outside ), disabled: cn( - "text-muted-foreground opacity-50", + "text-faint opacity-50", defaultClassNames.disabled ), hidden: cn("invisible", defaultClassNames.hidden), @@ -208,7 +208,7 @@ function CalendarDayButton({ data-range-end={modifiers.range_end} data-range-middle={modifiers.range_middle} className={cn( - "flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70", + "flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 font-mono leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70", defaultClassNames.day, className )} diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index acf57dc5..48f5a03b 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
) { return (
) diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index f5a7e433..95cd99f1 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -14,7 +14,7 @@ function Checkbox({ - + {children} @@ -67,13 +67,13 @@ function CommandInput({ return (
- + ) @@ -118,7 +118,7 @@ function CommandGroup({ Close @@ -125,7 +125,7 @@ function DialogTitle({ return ( ) diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index ae1fcf62..271207dc 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -42,7 +42,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} className={cn( - "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", + "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg border border-input bg-popover p-1 text-popover-foreground data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", className )} {...props} @@ -74,7 +74,7 @@ function DropdownMenuItem({ data-inset={inset} data-variant={variant} className={cn( - "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", + "relative flex cursor-default items-center gap-2 rounded-md px-2 py-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive-subtle data-[variant=destructive]:focus:text-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", className )} {...props} @@ -92,7 +92,7 @@ function DropdownMenuCheckboxItem({ ) {

{body} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index f1124aea..8055bc1a 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -8,9 +8,9 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { type={type} data-slot="input" className={cn( - "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30", - "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", - "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", + "h-11 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 font-mono text-base text-foreground transition-[color,box-shadow] outline-none selection:bg-destructive selection:text-white file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-mono file:text-sm file:text-foreground placeholder:text-faint placeholder:tracking-normal disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-40 md:text-sm", + "focus-visible:border-foreground focus-visible:ring-[3px] focus-visible:ring-foreground/15", + "aria-invalid:border-destructive aria-invalid:ring-destructive/20", className )} {...props} diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx index 1ac80f70..224c0b81 100644 --- a/src/components/ui/label.tsx +++ b/src/components/ui/label.tsx @@ -13,7 +13,7 @@ function Label({ + + + [{label}…] + +

+ ); +} + +/** Compact inline spinner without text, for buttons / tight rows. */ +export function InlineSpinner({ className }: { className?: string }) { + return ( + + ); +} diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx index 188f67f3..d40a25f7 100644 --- a/src/components/ui/popover.tsx +++ b/src/components/ui/popover.tsx @@ -30,7 +30,7 @@ function PopoverContent({ align={align} sideOffset={sideOffset} className={cn( - "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + "z-50 w-72 rounded-lg border border-input bg-popover p-4 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", className )} {...props} diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index c0dc7120..a95d64a5 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react" import { Select as SelectPrimitive } from "radix-ui" import { cn } from "@/lib/utils" @@ -37,7 +37,7 @@ function SelectTrigger({ data-slot="select-trigger" data-size={size} className={cn( - "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", + "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 font-mono text-sm text-foreground whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:border-foreground focus-visible:ring-[3px] focus-visible:ring-foreground/15 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-faint data-[size=default]:h-11 data-[size=sm]:h-9 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", className )} {...props} @@ -62,7 +62,7 @@ function SelectContent({ ) @@ -109,19 +112,14 @@ function SelectItem({ - - - - - + + + {children} ) diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index cb53bb28..f3dee702 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -60,22 +60,22 @@ function SheetContent({ {children} {showCloseButton && ( - + Close @@ -112,7 +112,7 @@ function SheetTitle({ return ( ) diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index 9b20afe2..fff747e1 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -24,12 +24,32 @@ const Toaster = ({ ...props }: ToasterProps) => { error: , loading: , }} + toastOptions={{ + classNames: { + toast: + "!rounded-lg !border !border-input !shadow-none !font-mono !text-xs", + title: "!font-mono !text-xs !tracking-wide", + description: "!text-muted-foreground", + }, + }} style={ { + // Flat Nothing toast — transitional fallback while toast.* call sites + // migrate to inline . Monochrome surface, status color on + // text only, no shadow. "--normal-bg": "var(--popover)", "--normal-text": "var(--popover-foreground)", - "--normal-border": "var(--border)", - "--border-radius": "var(--radius)", + "--normal-border": "var(--input)", + "--success-bg": "var(--popover)", + "--success-text": "var(--success)", + "--success-border": "var(--input)", + "--error-bg": "var(--popover)", + "--error-text": "var(--destructive)", + "--error-border": "var(--destructive)", + "--warning-bg": "var(--popover)", + "--warning-text": "var(--warning)", + "--warning-border": "var(--input)", + "--border-radius": "8px", } as React.CSSProperties } {...props} diff --git a/src/components/ui/status-text.tsx b/src/components/ui/status-text.tsx new file mode 100644 index 00000000..9a33bd71 --- /dev/null +++ b/src/components/ui/status-text.tsx @@ -0,0 +1,128 @@ +"use client"; + +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +/** + * Inline status text — the Nothing replacement for toast popups. + * + * Renders bracketed Space Mono status near the trigger: `[SAVED]`, + * `[ERROR: …]`, `[SAVING…]`. Announces via aria-live so screen-reader users + * still get feedback (toasts previously carried this). Pair with + * `useInlineStatus` for transient set + auto-clear. + */ + +export type InlineStatusKind = "idle" | "ok" | "error" | "info" | "pending"; + +export interface InlineStatusState { + kind: InlineStatusKind; + message: string; +} + +const KIND_CLASS: Record = { + idle: "text-muted-foreground", + ok: "text-success", + error: "text-destructive", + info: "text-muted-foreground", + pending: "text-muted-foreground", +}; + +export function StatusText({ + status, + className, +}: { + status: InlineStatusState; + className?: string; +}) { + const { kind, message } = status; + // Reserve a live region even when idle so the announcement fires on change. + return ( + + {kind === "pending" ? ( + + ) : null} + {kind !== "idle" && message ? [{message}] : null} + + ); +} + +export interface UseInlineStatusReturn { + status: InlineStatusState; + set: ( + kind: InlineStatusKind, + message: string, + opts?: { autoClearMs?: number } + ) => void; + ok: (message?: string) => void; + error: (message?: string) => void; + info: (message: string) => void; + pending: (message?: string) => void; + clear: () => void; +} + +/** + * Transient inline status with auto-clear. Success/info clear after + * `autoClearMs`; errors linger longer; `pending` never auto-clears (caller + * resolves it). Cleans its timer on unmount. + */ +export function useInlineStatus(autoClearMs = 4000): UseInlineStatusReturn { + const [status, setStatus] = React.useState({ + kind: "idle", + message: "", + }); + const timer = React.useRef | null>(null); + + const clearTimer = React.useCallback(() => { + if (timer.current) { + clearTimeout(timer.current); + timer.current = null; + } + }, []); + + const set = React.useCallback( + (kind: InlineStatusKind, message: string, opts?: { autoClearMs?: number }) => { + clearTimer(); + setStatus({ kind, message }); + const ms = opts?.autoClearMs ?? autoClearMs; + if (kind !== "pending" && kind !== "idle" && ms > 0) { + timer.current = setTimeout( + () => setStatus({ kind: "idle", message: "" }), + ms + ); + } + }, + [autoClearMs, clearTimer] + ); + + const ok = React.useCallback((message = "SAVED") => set("ok", message), [set]); + const error = React.useCallback( + (message = "ERROR") => set("error", message, { autoClearMs: 8000 }), + [set] + ); + const info = React.useCallback((message: string) => set("info", message), [set]); + const pending = React.useCallback( + (message = "WORKING…") => set("pending", message), + [set] + ); + const clear = React.useCallback(() => { + clearTimer(); + setStatus({ kind: "idle", message: "" }); + }, [clearTimer]); + + React.useEffect(() => clearTimer, [clearTimer]); + + return { status, set, ok, error, info, pending, clear }; +} diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx index 8baa844f..97a4cacb 100644 --- a/src/components/ui/switch.tsx +++ b/src/components/ui/switch.tsx @@ -17,7 +17,7 @@ function Switch({ data-slot="switch" data-size={size} className={cn( - "peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80", + "peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-6 data-[size=default]:w-11 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", className )} {...props} @@ -25,7 +25,7 @@ function Switch({ diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx index e27f1831..5e57c80c 100644 --- a/src/components/ui/table.tsx +++ b/src/components/ui/table.tsx @@ -44,7 +44,7 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { tr]:last:border-b-0", + "border-t border-border bg-muted font-medium [&>tr]:last:border-b-0", className )} {...props} @@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) { ) { [role=checkbox]]:translate-y-[2px]", + "h-auto px-4 pb-3 text-left align-middle font-mono text-[11px] uppercase tracking-[0.1em] font-normal text-muted-foreground border-b border-input whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className )} {...props} @@ -83,7 +83,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) { [role=checkbox]]:translate-y-[2px]", + "px-4 py-3.5 align-middle text-sm text-foreground whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className )} {...props} diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index b463afd1..aaeb3ffe 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -26,12 +26,12 @@ function Tabs({ } const tabsListVariants = cva( - "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none", + "group/tabs-list inline-flex items-center gap-8 border-b border-border bg-transparent p-0 text-muted-foreground group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col", { variants: { variant: { - default: "bg-muted", - line: "gap-1 bg-transparent", + default: "", + line: "", }, }, defaultVariants: { @@ -64,10 +64,8 @@ function TabsTrigger({ ) {