From 344b72d3a7283c38a32dab254a9f8e302fcfa40e Mon Sep 17 00:00:00 2001 From: Thammanoon Semapru Date: Thu, 16 Jul 2026 16:21:56 +0700 Subject: [PATCH 01/11] docs: calm-corporate whole-app reskin design spec Token + shell + primitives reskin (approach A). Calm-corporate vibe, keep Carmen blue accent, Inter-only. Pages inherit; bespoke Dashboard + entry pages. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...2026-07-16-calm-corporate-reskin-design.md | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-16-calm-corporate-reskin-design.md diff --git a/docs/superpowers/specs/2026-07-16-calm-corporate-reskin-design.md b/docs/superpowers/specs/2026-07-16-calm-corporate-reskin-design.md new file mode 100644 index 0000000..53969c9 --- /dev/null +++ b/docs/superpowers/specs/2026-07-16-calm-corporate-reskin-design.md @@ -0,0 +1,239 @@ +# Calm-Corporate Whole-App Reskin — Design + +**Date:** 2026-07-16 +**Branch:** `redesign/calm-corporate-reskin` +**Status:** Approved direction, pending spec review + +## Goal + +Give the Carmen support/ops admin dashboard a fresh, cohesive **calm-corporate** +visual identity (Stripe/Notion-admin feel: light default, spacious, trustworthy, +information-clear) across the whole app — without rewriting all 30+ pages. + +The app is already a shadcn/Tailwind enterprise dashboard. The redesign is +delivered through the **design-system leverage points**: the token layer, the app +shell, and the shared primitives. Every Management/Edit page inherits the new look +for free; Dashboard and entry pages get bespoke attention. + +## Decisions (locked) + +| Decision | Choice | +|----------|--------| +| Scope | Whole-app new visual identity | +| Vibe | Calm corporate — clean, spacious, light default, trustworthy | +| Brand anchor | Keep Carmen blue (`--primary: 221 61% 48%`) as the single accent | +| Depth | Token + shell + shared primitives (pages inherit); bespoke Dashboard + entry pages | +| Typography | Inter-only, disciplined hierarchy + tabular figures (no new display face) | +| Approach | A — reskin design system, not per-page rewrite | + +## Honest framing + +The existing token system is technically sound (clean HSL vars, light+dark, +semantic `success/warning/info`, shadow + zebra tokens, Inter). The "generic SaaS" +feel comes from **cool default neutrals, flat hierarchy, a thin type scale, and a +plain sidebar/topbar** — not from broken tokens. So the transformation is roughly +**70% shell + primitives + spacing/type discipline, 30% palette refresh.** This +spec reflects that weighting. + +--- + +## 1. Token layer (`src/index.css`, `tailwind.config.js`) + +Rebuild the token *values* for a calmer, softer, more intentional neutral system. +Carmen blue is preserved exactly. Radius bumps up for a calmer feel. + +### 1.1 Neutral + accent palette (starting target, tuned against WCAG AA during impl) + +Light (`:root`): + +| Token | Value | Role | +|-------|-------|------| +| `--background` | `220 20% 98%` | app canvas (faintly tinted) | +| `--card` | `0 0% 100%` | white cards pop on canvas → calm depth | +| `--foreground` | `222 24% 12%` | near-black text | +| `--muted` | `220 16% 96%` | subtle fills | +| `--muted-foreground` | `220 9% 44%` | secondary text (AA on card) | +| `--secondary` | `220 16% 95%` | secondary surfaces/buttons | +| `--accent` | `220 16% 95%` | hover/active neutral surface | +| `--border` | `220 16% 91%` | hairline borders | +| `--input` | `220 16% 88%` | input borders (slightly darker) | +| `--primary` | `221 61% 48%` | **Carmen blue — unchanged** | +| `--ring` | `221 61% 48%` | focus ring | +| `--destructive` | `0 72% 51%` | destructive | +| `--success` | `152 42% 38%` | active/healthy | +| `--warning` | `32 90% 45%` | at-risk | +| `--info` | `221 61% 48%` | info = brand | +| `--radius` | `0.5rem` | up from 0.375 | + +Dark (`.dark`): soften the current very-saturated blue-black (`224 71% 4%`) to a +calmer slate; keep semantic tokens legible: + +| Token | Value | +|-------|-------| +| `--background` | `222 24% 8%` | +| `--card` | `222 22% 11%` | +| `--foreground` | `220 20% 92%` | +| `--muted` | `222 18% 16%` | +| `--muted-foreground` | `220 12% 62%` | +| `--border` | `222 18% 20%` | +| `--primary` | `217 70% 60%` | +| `--ring` | `217 70% 60%` | + +Semantic `success/warning/info` keep their existing dark-tuned lightness. + +### 1.2 Elevation + +Define an explicit scale and use it consistently (canvas-vs-card does most of the +depth work; shadows stay whisper-light): + +- `--shadow-xs: 0 1px 2px rgba(16,24,40,0.04)` +- `--shadow-sm` / `--shadow-md`: keep, lighten slightly. +- "Raised card" recipe: white card + `--shadow-xs` + 1px `--border`. No glass. + +### 1.3 Numerics & misc + +- Apply `font-feature-settings: "tnum" 1` (tabular numbers) to tables, counts, + IDs, dates, currency — anything columnar. Add a `.tabular-nums` utility and use + it in `DataTable` + stat tiles. +- Keep zebra/sticky mechanics; re-tune zebra alpha to the new neutrals. +- Refined focus-visible ring already present — keep, verify against new palette. + +### 1.4 Tailwind config + +No structural change — the `hsl(var(--…))` mapping already covers new tokens. +Confirm `success/warning/info` mappings present (they are). Bump `borderRadius` +comment only; values already derive from `--radius`. + +--- + +## 2. Typography + +Inter only. Standardize a **type scale as `@layer components` classes** (or a small +`Text`/`Heading` convention) so pages stop hand-rolling sizes: + +| Role | Spec | +|------|------| +| Page title | `text-2xl sm:text-3xl font-bold tracking-tight` | +| Section title | `text-lg font-semibold tracking-tight` | +| Card title | `text-base font-semibold` | +| Body | `text-sm` | +| Meta / caption | `text-xs text-muted-foreground` | +| Data / mono | `text-xs font-mono tabular-nums` | + +Line-height and `tracking-tight` on all headings. This matches (and formalizes) the +existing styling reference in `CLAUDE.md`. + +--- + +## 3. App shell — `src/components/Layout.tsx`, `src/components/Sidebar.tsx` + +Biggest visible change. Keep all existing behavior (collapse persistence, mobile +Sheet, permission/superadmin gating, groups) — restyle and restructure. + +### 3.1 Sidebar +- **Brand/workspace header** at top: Carmen mark + product name; in collapsed rail, + just the mark. +- **Grouped nav** with quiet uppercase section labels (`Organization` / `Content` / + `Platform`); ungrouped items (Dashboard) sit above, label-less. +- **Refined active state**: soft primary-tinted pill + left accent, not heavy fill. +- **Collapsed icon rail** keeps right-side tooltips (`delayDuration=200`) — exists. +- **User menu pinned to bottom**: avatar + name + role, opens menu (Profile, theme, + logout). Consolidates account affordances. +- Width tokens unchanged (`w-60` / `w-16`), transition class unchanged. + +### 3.2 Topbar (within `Layout`) +- Left: breadcrumb (section / page). +- Right: global search trigger (`⌘K`, wired to existing search shortcut pattern), + theme toggle, notifications (if present), avatar (mirrors sidebar bottom on + mobile). +- Sticky, hairline bottom border, `bg-background/80` backdrop. + +### 3.3 Content shell +- Uniform page wrapper: max-width container, consistent horizontal padding, vertical + rhythm `space-y-4 sm:space-y-6`. All pages render inside it (already close — make + it a single source). + +--- + +## 4. Shared primitives (`src/components/`, `src/components/ui/`) + +Restyle (do **not** break APIs — pages depend on them). Where a canonical component +is missing, add it and adopt on Dashboard + a couple reference pages; other pages +adopt opportunistically. + +| Primitive | Change | +|-----------|--------| +| `PageHeader` (new/consolidated) | Title + subtitle + right-aligned actions, one pattern. Used by Management + Edit headers. | +| `Card` (`ui/card`) | Raised recipe (white + `--shadow-xs` + border), calmer padding. | +| `DataTable` (`ui/data-table`) | Header: muted bg, `text-xs` uppercase, tabular body; comfortable row height; keep auto `#` col + sticky mechanics. | +| `Badge` (`ui/badge`) | Status variants: `success` (active), `secondary` (inactive), `warning`, `destructive`, `info`. No raw green. | +| `Button` (`ui/button`) | Enforce hierarchy: one `primary` per view, rest `secondary`/`ghost`/`outline`. Slightly softer radius. | +| `EmptyState` | Designed empty (icon in soft circle, title, description, one action). | +| Read-only field | `ReadOnlyText` style refresh (muted surface, aligned height) — keep the existing helper contract. | +| `TableSkeleton` | Match new row density. | + +**Constraint:** `src/components/ui/*` primitives keep their prop APIs (project rule +#2). Restyle via classes/variants only, no API breaks. + +--- + +## 5. Dashboard — `src/pages/Dashboard.tsx` (bespoke) + +Operational **status board**, not a decorative hero. Sections: + +1. **Header**: greeting + environment badge (dev/uat/prod) + last-refresh meta. +2. **Health tiles** (tabular-nums): Tenants/BUs, Users, Applications, Migration + status (healthy/at-risk count). Each tile = number + small label + subtle trend + or status dot. Intentional ops metric, sourced from existing services where data + exists; static/omitted where it doesn't (no fake numbers). +3. **Recent activity / quick actions**: shortcuts to create Cluster/BU/User, jump to + Tenant Migrations, News/Broadcast. +4. Respect permissions — hide tiles/actions the user can't access. + +Only real data. Any metric without a backing service is omitted, not faked. + +--- + +## 6. Entry pages (bespoke) + +- **Login** (`src/pages/Login.tsx`): split layout — left brand/product panel + (Carmen, one-line value prop, subtle pattern), right clean login card. Enterprise, + calm. +- **Landing** (`src/pages/Landing.tsx`): concise product intro for the internal + tool → primary CTA to Login/Dashboard. Not a marketing splash; a professional + front door. + +--- + +## 7. Testing + +- Existing 498 Vitest tests assert **behavior/roles/text, not snapshots** — token, + palette, and primitive restyles are safe. +- **Risk:** Sidebar/Layout structural changes (bottom user menu, breadcrumb, group + labels) may touch nav-related assertions. Plan updates those tests alongside the + shell change and keeps the suite green. +- Add light component tests for any new primitive (`PageHeader`, restyled + `EmptyState`) per repo test conventions (RTL, co-located, explicit `vitest` + imports). +- `bun run test` green before each phase merge; `CI=true bun run build` clean (lint + + types) — catch leftover imports. + +## 8. Out of scope + +- No backend/API changes. +- No new libraries (project rule #6) — Inter already loaded; no new font. +- No per-page layout rewrites beyond Dashboard + entry pages (pages inherit). +- No behavior/routing/permission changes. +- `src/components/ui/*` prop APIs unchanged. + +## 9. Suggested phasing (for the plan) + +1. **Tokens + type scale** (foundation) — palette, radius, elevation, tabular-nums, + type classes. Whole app shifts; verify contrast + build. +2. **Shared primitives** — Card, DataTable, Badge, Button, EmptyState, PageHeader, + read-only field. +3. **App shell** — Sidebar + Layout + topbar; update affected tests. +4. **Dashboard** — status board. +5. **Entry pages** — Login + Landing. + +Each phase: `bun run test` + `CI=true bun run build` green before moving on. From 9e48b5e8d5e539fa65020bf2bd36991379341eab Mon Sep 17 00:00:00 2001 From: Thammanoon Semapru Date: Thu, 16 Jul 2026 16:27:03 +0700 Subject: [PATCH 02/11] docs: calm-corporate reskin implementation plan 9 tasks across 4 phases: tokens, primitives, shell breadcrumb, regression. Entry/Dashboard inherit tokens (already redesigned) not rewritten. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-07-16-calm-corporate-reskin.md | 709 ++++++++++++++++++ 1 file changed, 709 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-16-calm-corporate-reskin.md diff --git a/docs/superpowers/plans/2026-07-16-calm-corporate-reskin.md b/docs/superpowers/plans/2026-07-16-calm-corporate-reskin.md new file mode 100644 index 0000000..ec15154 --- /dev/null +++ b/docs/superpowers/plans/2026-07-16-calm-corporate-reskin.md @@ -0,0 +1,709 @@ +# Calm-Corporate Whole-App Reskin — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Give the Carmen ops admin dashboard a fresh calm-corporate visual identity across the whole app by reskinning the design-system leverage points (tokens, primitives, shell), so all 30+ pages inherit the new look. + +**Architecture:** Change is delivered top-down through shared layers, not per page. Phase 1 retunes CSS tokens (palette, radius, elevation, tabular figures) — every page shifts at once. Phase 2 restyles shared primitives (Badge, Card, DataTable, EmptyState). Phase 3 adds a desktop breadcrumb bar and refines the sidebar active state. Phase 4 verifies the already-good entry/Dashboard pages under the new tokens and runs full regression. + +**Tech Stack:** React 19 + TypeScript, Tailwind 3.4 (HSL CSS custom properties), shadcn/ui (Radix + CVA), TanStack Table v8, Vitest + React Testing Library, Bun. + +## Global Constraints + +- **No new libraries** (project rule #6). Inter is already loaded; no new font. +- **Do not change `src/components/ui/*` prop APIs** (rule #2). Restyle via classes/CVA variants only. +- **No raw green Tailwind for status** — use `Badge` variants (rule #5). +- **Keep Carmen blue exactly:** `--primary: 221 61% 48%` (light) / `217 70% 60%` (dark). +- **Tests:** co-locate `*.test.tsx` beside source; explicit `import { describe, it, expect } from 'vitest'` (no globals); assert behavior/roles/text, not snapshots (rule #18). Don't touch `tsconfig.json` / `vite.config.ts` for test setup. +- **Gate per phase:** `bun run test` green AND `CI=true bun run build` clean (lint + types) before moving to the next phase. +- Package manager: **bun**. Dev server / preview is port `3304`. +- Entry pages (`Login.tsx`, `Landing.tsx`) and `Dashboard.tsx` are already redesigned and on-vibe — they INHERIT new tokens. Do not rewrite them; polish only. + +--- + +## File Structure + +**Modified:** +- `src/index.css` — token values (`:root`, `.dark`), radius, elevation tokens, `.tabular-nums` utility, zebra retune. +- `src/components/ui/badge.tsx` — add `warning` + `info` variants. +- `src/components/ui/card.tsx` — elevation + padding recipe. +- `src/components/ui/data-table.tsx` — header/row density + tabular figures on data cells. +- `src/components/EmptyState.tsx` — soft-ring icon well. +- `src/components/Layout.tsx` — desktop breadcrumb bar, content top padding. +- `src/components/Sidebar.tsx` — refined active state. + +**Created:** +- `src/components/Breadcrumbs.tsx` — route→crumb pure fn + component. +- `src/components/Breadcrumbs.test.tsx` — unit test for the pure fn. +- `src/components/ui/badge.test.tsx` already exists — extend it. + +--- + +## Phase 1 — Foundation tokens + +### Task 1: Refresh palette + radius + +Shift the neutral ramp from the generic cool `226`-hue slate to a deliberate, very-low-saturation **warm neutral** (calm corporate, "warm paper + cool ink"), bump radius, keep Carmen blue. Every page re-tints. + +**Files:** +- Modify: `src/index.css` (`:root` block ~lines 5–33, `.dark` block ~lines 40–68) + +**Interfaces:** +- Produces: token values consumed by every component via `hsl(var(--…))`. No JS surface. + +- [ ] **Step 1: Replace the `:root` color + radius tokens** + +In `src/index.css`, replace the existing `:root` token lines (keep `--shadow-*`, `--zebra-*`, `--bu-chip-*` for now — Task 2 retunes them) with: + +```css +:root { + --background: 40 9% 97.5%; + --foreground: 30 7% 12%; + --card: 0 0% 100%; + --card-foreground: 30 7% 12%; + --popover: 0 0% 100%; + --popover-foreground: 30 7% 12%; + --primary: 221 61% 48%; + --primary-foreground: 0 0% 100%; + --secondary: 40 8% 94%; + --secondary-foreground: 30 10% 20%; + --muted: 40 8% 95.5%; + --muted-foreground: 33 5% 43%; + --accent: 40 8% 94%; + --accent-foreground: 30 10% 20%; + --destructive: 0 72% 51%; + --destructive-foreground: 0 0% 100%; + --border: 40 8% 90%; + --input: 40 8% 87%; + --ring: 221 61% 48%; + --radius: 0.5rem; + --success: 152 40% 36%; + --success-foreground: 0 0% 100%; + --warning: 32 90% 44%; + --warning-foreground: 0 0% 100%; + --info: 221 61% 48%; + --info-foreground: 0 0% 100%; +``` + +(Leave the `--shadow-*`, `--zebra-*`, `--bu-chip-*` lines and the closing `}` untouched below this.) + +- [ ] **Step 2: Replace the `.dark` color tokens** + +Replace the `.dark` color token lines (keep `--shadow-*`, `--zebra-*`, `--bu-chip-*`) with a softened, faintly-warm neutral dark (avoids the current over-saturated blue-black and avoids brown): + +```css +.dark { + --background: 30 6% 9%; + --foreground: 40 8% 90%; + --card: 30 6% 12%; + --card-foreground: 40 8% 90%; + --popover: 30 6% 12%; + --popover-foreground: 40 8% 90%; + --primary: 217 70% 60%; + --primary-foreground: 0 0% 100%; + --secondary: 30 5% 17%; + --secondary-foreground: 40 8% 95%; + --muted: 30 5% 16%; + --muted-foreground: 35 6% 62%; + --accent: 30 5% 18%; + --accent-foreground: 40 8% 95%; + --destructive: 0 62% 45%; + --destructive-foreground: 0 0% 100%; + --border: 30 5% 20%; + --input: 30 5% 22%; + --ring: 217 70% 60%; + --radius: 0.5rem; + --success: 152 45% 45%; + --success-foreground: 0 0% 100%; + --warning: 38 92% 55%; + --warning-foreground: 20 14% 8%; + --info: 217 70% 60%; + --info-foreground: 0 0% 100%; +``` + +(`--radius` here is harmless duplication for clarity; `:root` already sets it. Leave `--shadow-*`, `--zebra-*`, `--bu-chip-*` and the closing `}` untouched.) + +- [ ] **Step 3: Run the test suite** + +Run: `bun run test` +Expected: PASS (498 tests). Palette values are not asserted by any test, so all stay green. + +- [ ] **Step 4: Build** + +Run: `CI=true bun run build` +Expected: build succeeds, no lint/type errors. + +- [ ] **Step 5: Visual smoke** + +Run: `bun start` (port 3304), open `/login` and `/dashboard`. Confirm: canvas reads as a warm off-white (not cool gray), cards are clean white, corners are softer (0.5rem), Carmen blue unchanged. Confirm dark mode via sidebar theme toggle looks neutral, not brown or over-blue. + +- [ ] **Step 6: Commit** + +```bash +git add src/index.css +git commit -m "feat(design): warm-neutral calm-corporate palette + 0.5rem radius" +``` + +### Task 2: Elevation tokens + tabular figures utility + +Whisper-light elevation (canvas-vs-card does the depth work) and tabular numerals for all columnar data. + +**Files:** +- Modify: `src/index.css` (`--shadow-*` lines in `:root` and `.dark`; add a utility block) + +**Interfaces:** +- Produces: CSS var `--shadow-xs`; utility class `.tabular-nums` (consumed by Task 5). + +- [ ] **Step 1: Retune shadow tokens (both themes)** + +In `:root`, replace the shadow lines with: + +```css + --shadow-xs: 0 1px 2px rgba(24, 20, 16, 0.04); + --shadow-sm: 0 1px 2px rgba(24, 20, 16, 0.05); + --shadow-md: 0 4px 12px rgba(24, 20, 16, 0.06); +``` + +In `.dark`, replace the shadow lines with: + +```css + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45); +``` + +- [ ] **Step 2: Add the tabular-nums utility** + +Append to `src/index.css` (after the `body` rule, before the zebra rules): + +```css +.tabular-nums { + font-variant-numeric: tabular-nums; + font-feature-settings: "tnum" 1; +} +``` + +- [ ] **Step 3: Retune zebra alpha to the warm neutrals** + +In `:root`, replace the `--zebra-even` / `--zebra-hover` / `--zebra-hover-accent` lines with: + +```css + --zebra-even: rgba(24, 20, 16, 0.02); + --zebra-hover: hsl(221 61% 48% / 0.045); + --zebra-hover-accent: hsl(221 61% 48% / 0.5); +``` + +(Leave `.dark` zebra lines as-is — they read fine on the new dark ground.) + +- [ ] **Step 4: Test + build** + +Run: `bun run test` → PASS. +Run: `CI=true bun run build` → clean. + +- [ ] **Step 5: Commit** + +```bash +git add src/index.css +git commit -m "feat(design): elevation scale + tabular-nums utility + zebra retune" +``` + +--- + +## Phase 2 — Shared primitives + +### Task 3: Badge — add `warning` and `info` variants + +`Badge` currently exposes only `default/secondary/destructive/outline/success`. Status usage across pages needs `warning` and `info` (rule #5: no raw green/amber classes). Additive — no API break. + +**Files:** +- Modify: `src/components/ui/badge.tsx:9-16` +- Test: `src/components/ui/badge.test.tsx` (exists — extend) + +**Interfaces:** +- Produces: `` renders with `bg-warning` / `bg-info`. + +- [ ] **Step 1: Write the failing test** + +Add to `src/components/ui/badge.test.tsx`: + +```tsx +it('renders warning and info variants with their status backgrounds', () => { + const { rerender } = render(At risk); + expect(screen.getByText('At risk')).toHaveClass('bg-warning'); + rerender(Info); + expect(screen.getByText('Info')).toHaveClass('bg-info'); +}); +``` + +(If `render`/`screen` aren't imported in the file, add `import { render, screen } from '@testing-library/react';` and `import { describe, it, expect } from 'vitest';` and `import { Badge } from './badge';` — match the file's existing imports.) + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bun run test src/components/ui/badge.test.tsx` +Expected: FAIL — `warning` variant not defined, element lacks `bg-warning`. + +- [ ] **Step 3: Add the variants** + +In `src/components/ui/badge.tsx`, extend the `variant` map (after `success`): + +```tsx + success: 'border-transparent bg-success text-success-foreground', + warning: 'border-transparent bg-warning text-warning-foreground', + info: 'border-transparent bg-info text-info-foreground', +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `bun run test src/components/ui/badge.test.tsx` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/components/ui/badge.tsx src/components/ui/badge.test.tsx +git commit -m "feat(ui): add warning + info Badge variants" +``` + +### Task 4: Card — elevation + calmer padding + +Current card is `p-3 shadow-sm` (tight). Calm corporate wants more breathing room and the whisper elevation. Default className only — callers can still override. + +**Files:** +- Modify: `src/components/ui/card.tsx:9` +- Test: `src/components/ui/card.test.tsx` (exists) + +**Interfaces:** +- Produces: `Card` default surface = white + `--shadow-xs` + border + `p-5` + `gap-4` + `rounded-lg`. + +- [ ] **Step 1: Update the Card base className** + +Replace line 9 of `src/components/ui/card.tsx`: + +```tsx + 'flex flex-col gap-4 rounded-lg border bg-card text-card-foreground p-5 shadow-[var(--shadow-xs)] overflow-hidden', +``` + +- [ ] **Step 2: Verify existing card test still passes** + +Run: `bun run test src/components/ui/card.test.tsx` +Expected: PASS (card test asserts rendering/children/className passthrough, not exact padding). + +- [ ] **Step 3: Test + build** + +Run: `bun run test` → PASS. +Run: `CI=true bun run build` → clean. + +- [ ] **Step 4: Commit** + +```bash +git add src/components/ui/card.tsx +git commit -m "feat(ui): calmer Card padding + whisper elevation" +``` + +### Task 5: DataTable — density + tabular figures + +Make table headers quieter/uppercase and body figures tabular so columns of IDs/dates/counts align. Restyle only — no prop changes. + +**Files:** +- Modify: `src/components/ui/data-table.tsx` (header cell `` classes; body cell `` classes; root table wrapper) + +**Interfaces:** +- Consumes: `.tabular-nums` (Task 2). +- Produces: no API change. + +- [ ] **Step 1: Locate the header + body cell class strings** + +Run: `grep -n "thead\| +``` + +- [ ] **Step 2: Test + build** + +Run: `bun run test` → PASS. +Run: `CI=true bun run build` → clean. + +- [ ] **Step 3: Commit** + +```bash +git add src/components/EmptyState.tsx +git commit -m "feat(ui): ringed EmptyState icon well" +``` + +--- + +## Phase 3 — App shell + +### Task 7: Breadcrumbs — pure fn + component + +Desktop admin consoles orient via breadcrumbs. Build a pure route→crumbs function (unit-tested) and a thin presentational component. + +**Files:** +- Create: `src/components/Breadcrumbs.tsx` +- Create: `src/components/Breadcrumbs.test.tsx` + +**Interfaces:** +- Produces: + - `crumbsFromPath(pathname: string): { label: string; to?: string }[]` — last crumb has no `to`. + - `` (uses `useLocation`, renders `crumbsFromPath`). + +- [ ] **Step 1: Write the failing test** + +Create `src/components/Breadcrumbs.test.tsx`: + +```tsx +import { describe, it, expect } from 'vitest'; +import { crumbsFromPath } from './Breadcrumbs'; + +describe('crumbsFromPath', () => { + it('maps a section list route to a single unlinked crumb', () => { + expect(crumbsFromPath('/clusters')).toEqual([{ label: 'Clusters' }]); + }); + + it('maps an edit route to Section > Edit with the section linked', () => { + expect(crumbsFromPath('/clusters/abc-123/edit')).toEqual([ + { label: 'Clusters', to: '/clusters' }, + { label: 'Edit' }, + ]); + }); + + it('maps a new route to Section > New', () => { + expect(crumbsFromPath('/business-units/new')).toEqual([ + { label: 'Business Units', to: '/business-units' }, + { label: 'New' }, + ]); + }); + + it('handles nested platform routes', () => { + expect(crumbsFromPath('/platform/roles')).toEqual([ + { label: 'Platform', to: '/platform' }, + { label: 'Roles' }, + ]); + }); + + it('returns an empty list for the dashboard', () => { + expect(crumbsFromPath('/dashboard')).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bun run test src/components/Breadcrumbs.test.tsx` +Expected: FAIL — module not found / `crumbsFromPath` undefined. + +- [ ] **Step 3: Implement the component** + +Create `src/components/Breadcrumbs.tsx`: + +```tsx +import { Link, useLocation } from 'react-router-dom'; +import { ChevronRight } from 'lucide-react'; + +export interface Crumb { + label: string; + to?: string; +} + +const SEGMENT_LABELS: Record = { + clusters: 'Clusters', + 'business-units': 'Business Units', + 'tenant-migrations': 'Tenant Migrations', + users: 'Users', + 'report-templates': 'Report Templates', + 'print-template-mapping': 'Print Mapping', + news: 'News', + broadcasts: 'Broadcasts', + applications: 'Applications', + platform: 'Platform', + roles: 'Roles', + 'super-admins': 'Super Admins', + 'user-platform': 'User Platform', + 'sql-workbench': 'SQL Workbench', + profile: 'Profile', + changelog: 'Changelog', + new: 'New', + edit: 'Edit', +}; + +const labelFor = (seg: string): string => + SEGMENT_LABELS[seg] ?? + seg.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); + +// Segments that are opaque record ids (uuid-ish) carry no label of their own. +const isIdSegment = (seg: string): boolean => + !SEGMENT_LABELS[seg] && /\d/.test(seg) && seg.length > 8; + +export function crumbsFromPath(pathname: string): Crumb[] { + const segs = pathname.split('/').filter(Boolean); + if (segs.length === 0 || (segs.length === 1 && segs[0] === 'dashboard')) { + return []; + } + const meaningful = segs.filter((s) => !isIdSegment(s)); + return meaningful.map((seg, i) => { + const isLast = i === meaningful.length - 1; + if (isLast) return { label: labelFor(seg) }; + return { label: labelFor(seg), to: `/${meaningful.slice(0, i + 1).join('/')}` }; + }); +} + +export function Breadcrumbs() { + const { pathname } = useLocation(); + const crumbs = crumbsFromPath(pathname); + if (crumbs.length === 0) return null; + return ( + + ); +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `bun run test src/components/Breadcrumbs.test.tsx` +Expected: PASS (5 cases). + +Note: `/clusters/abc-123/edit` → `abc-123` is dropped by `isIdSegment` (has a digit, len > 8), leaving `['clusters','edit']` → `[{Clusters,/clusters},{Edit}]`, matching the test. + +- [ ] **Step 5: Commit** + +```bash +git add src/components/Breadcrumbs.tsx src/components/Breadcrumbs.test.tsx +git commit -m "feat(shell): route breadcrumbs (pure fn + component)" +``` + +### Task 8: Wire breadcrumb bar into Layout + refine sidebar active state + +Add a slim sticky desktop top bar hosting the breadcrumb (account/theme stay in the sidebar — no duplication). Refine the sidebar active state to a soft primary-tinted pill. + +**Files:** +- Modify: `src/components/Layout.tsx` (import Breadcrumbs; add desktop bar; adjust main padding) +- Modify: `src/components/Sidebar.tsx:86-93` and `:312-319` (active state) +- Test: `src/components/Sidebar.test.tsx`, `src/components/Layout` tests if present + +**Interfaces:** +- Consumes: `Breadcrumbs` (Task 7). + +- [ ] **Step 1: Check existing shell tests** + +Run: `bun run test src/components/Sidebar.test.tsx` +Expected: PASS (baseline before edits). + +- [ ] **Step 2: Import Breadcrumbs in Layout** + +In `src/components/Layout.tsx`, add after the `Sidebar` import (line 7): + +```tsx +import { Breadcrumbs } from './Breadcrumbs'; +``` + +- [ ] **Step 3: Add the desktop breadcrumb bar** + +In `src/components/Layout.tsx`, inside the main content `
` (after the mobile `
` closes at line 158, before `
` at line 161), insert: + +```tsx + {/* Desktop breadcrumb bar */} +
+ +
+``` + +- [ ] **Step 4: Reduce main top padding (bar now provides spacing)** + +Change `
` (line 161) from `py-6 sm:py-10` to `py-6 sm:py-8`: + +```tsx +
+``` + +- [ ] **Step 5: Refine sidebar active state (desktop NavLink)** + +In `src/components/Sidebar.tsx`, in the `NavLink` component, replace the active/inactive class branch (lines 86–89) with a soft primary-tinted pill: + +```tsx + active + ? 'bg-primary/10 text-primary' + : 'text-muted-foreground hover:bg-accent hover:text-foreground' +``` + +And in the mobile `` block, replace the matching active/inactive branch (lines 312–315) with: + +```tsx + active + ? 'bg-primary/10 text-primary' + : 'text-muted-foreground hover:bg-accent hover:text-foreground' +``` + +(Keep the left accent bar `
` — it now sits under a tinted pill, reinforcing the active item.) + +- [ ] **Step 6: Update / add the breadcrumb-bar assertion** + +If a `Layout` test exists, add a case; otherwise add to `Sidebar.test.tsx` a focused render test is not applicable (breadcrumb is in Layout). Add a case to `src/components/Breadcrumbs.test.tsx` for the component render: + +```tsx +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { Breadcrumbs } from './Breadcrumbs'; + +it('renders a linked section crumb and a current-page crumb', () => { + render( + + + , + ); + expect(screen.getByRole('link', { name: 'Clusters' })).toHaveAttribute('href', '/clusters'); + expect(screen.getByText('Edit')).toBeInTheDocument(); +}); +``` + +(Add the imports at the top of the test file if not already present.) + +- [ ] **Step 7: Run shell tests** + +Run: `bun run test src/components/Sidebar.test.tsx src/components/Breadcrumbs.test.tsx` +Expected: PASS. If a Sidebar test asserted the old `bg-secondary` active class, update it to `bg-primary/10` and re-run. + +- [ ] **Step 8: Full test + build** + +Run: `bun run test` → PASS. +Run: `CI=true bun run build` → clean. + +- [ ] **Step 9: Visual smoke** + +`bun start` → navigate Dashboard → Clusters → open a cluster edit. Confirm: breadcrumb bar shows on desktop (hidden on mobile, mobile header unchanged), active nav item is a soft blue pill, no duplicate account controls. + +- [ ] **Step 10: Commit** + +```bash +git add src/components/Layout.tsx src/components/Sidebar.tsx src/components/Breadcrumbs.test.tsx +git commit -m "feat(shell): desktop breadcrumb bar + softer sidebar active state" +``` + +--- + +## Phase 4 — Inherited-page verification + regression + +### Task 9: Verify entry/Dashboard under new tokens + full regression + +Entry pages and Dashboard already inherit the new palette. Verify contrast/legibility, apply only micro-polish if something reads wrong, then run the whole suite + a clean production build. + +**Files:** +- Possibly modify (only if a contrast/spacing issue is found): `src/pages/Login.tsx`, `src/pages/Landing.tsx`, `src/pages/Dashboard.tsx` + +**Interfaces:** none. + +- [ ] **Step 1: Visual audit of inherited pages** + +`bun start`. Check `/`, `/login`, `/dashboard` in BOTH light and dark: +- Login brand panel: `text-primary-foreground/60` labels still legible on the (unchanged) primary-blue panel — yes, primary unchanged. +- Landing hero `text-primary/[0.04]` watermark still subtle on warm canvas. +- Dashboard counts rail figures — confirm they read as tabular where numeric. + +- [ ] **Step 2: Apply micro-polish only if needed** + +If any muted text fails legibility on the warm canvas, nudge the specific class (e.g. `text-muted-foreground` is already AA by token design). Do NOT restructure these pages. If nothing reads wrong, make no change and note it. + +- [ ] **Step 3: Confirm no raw-status-color regressions** + +Run: `grep -rn "bg-green-\|text-green-\|bg-amber-\|bg-yellow-" src/pages src/components | grep -v test` +Expected: no matches (rule #5). If any exist (pre-existing), leave unless trivially a status badge — out of scope otherwise. + +- [ ] **Step 4: Full regression** + +Run: `bun run test` +Expected: PASS — all suites (target ~498+ existing + new Badge/Breadcrumbs cases). + +- [ ] **Step 5: Production build** + +Run: `CI=true bun run build` +Expected: clean build to `build/`, no lint/type errors (catches any leftover unused imports). + +- [ ] **Step 6: Commit (only if Step 2 changed a file)** + +```bash +git add -A +git commit -m "chore(design): verify inherited pages under calm-corporate tokens" +``` + +- [ ] **Step 7: Push branch** + +```bash +git push -u origin redesign/calm-corporate-reskin +``` + +(Do not open a PR to DEV/UAT automatically — the user manages those branches.) + +--- + +## Self-Review + +**Spec coverage:** +- §1 tokens (palette/radius/elevation/tabular) → Tasks 1, 2. ✅ +- §2 typography (Inter, scale, tabular) → tabular in Task 2/5; type scale already formalized in `PageHeader` + CLAUDE.md styling ref, tokens carry the rest. ✅ (No new type classes needed — existing scale is already the spec's scale; adding class layer would be churn.) +- §3 shell (sidebar refine, topbar/breadcrumb) → Tasks 7, 8. ⚠️ Spec's ⌘K-search + avatar-in-topbar intentionally trimmed (sidebar already owns account/theme; avoiding duplicate controls) — documented in Task 8 and plan intro. +- §4 primitives (Badge/Card/DataTable/Button/EmptyState/PageHeader/read-only) → Tasks 3–6. Button already has correct hierarchy variants; PageHeader + ReadOnlyField already exist and are on-vibe → no task (would be churn). ✅ +- §5 Dashboard → already redesigned; inherits + verified in Task 9. ✅ (Adjusted from "bespoke rewrite" — the page is already a status/activity board.) +- §6 entry pages → already redesigned; inherit + verified in Task 9. ✅ (Adjusted from "bespoke rewrite".) +- §7 testing → gates in every task; new tests in Tasks 3, 7, 8. ✅ + +**Placeholder scan:** No TBD/TODO; every code step shows exact code or exact command. ✅ + +**Type consistency:** `crumbsFromPath`/`Crumb`/`Breadcrumbs` names consistent across Task 7 impl, Task 7 test, Task 8 wiring. Badge `warning`/`info` strings consistent between test and impl. ✅ + +**Deviations from spec (intellectual honesty):** Login/Landing/Dashboard were found already-redesigned and on-vibe, so this plan has them INHERIT tokens rather than be rewritten (avoids redoing good work / YAGNI). Topbar trimmed to breadcrumb-only to avoid duplicate account controls. Both changes reduce scope while still delivering the whole-app new look via tokens + primitives + shell. From bfbad46581fc7c352d3225305f65da855063a7af Mon Sep 17 00:00:00 2001 From: Thammanoon Semapru Date: Thu, 16 Jul 2026 16:30:59 +0700 Subject: [PATCH 03/11] feat(design): warm-neutral calm-corporate palette + 0.5rem radius --- src/index.css | 75 ++++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/src/index.css b/src/index.css index 53fe9ab..23a31de 100644 --- a/src/index.css +++ b/src/index.css @@ -3,29 +3,29 @@ @tailwind utilities; :root { - --background: 226 20% 98%; - --foreground: 226 30% 10%; + --background: 40 9% 97.5%; + --foreground: 30 7% 12%; --card: 0 0% 100%; - --card-foreground: 226 30% 10%; + --card-foreground: 30 7% 12%; --popover: 0 0% 100%; - --popover-foreground: 226 30% 10%; + --popover-foreground: 30 7% 12%; --primary: 221 61% 48%; --primary-foreground: 0 0% 100%; - --secondary: 226 15% 94%; - --secondary-foreground: 226 30% 15%; - --muted: 226 15% 95%; - --muted-foreground: 226 10% 46%; - --accent: 220 14% 96%; - --accent-foreground: 220 30% 15%; - --destructive: 0 84% 60%; + --secondary: 40 8% 94%; + --secondary-foreground: 30 10% 20%; + --muted: 40 8% 95.5%; + --muted-foreground: 33 5% 43%; + --accent: 40 8% 94%; + --accent-foreground: 30 10% 20%; + --destructive: 0 72% 51%; --destructive-foreground: 0 0% 100%; - --border: 226 15% 90%; - --input: 226 15% 90%; + --border: 40 8% 90%; + --input: 40 8% 87%; --ring: 221 61% 48%; - --radius: 0.375rem; - --success: 142 40% 40%; + --radius: 0.5rem; + --success: 152 40% 36%; --success-foreground: 0 0% 100%; - --warning: 38 92% 45%; + --warning: 32 90% 44%; --warning-foreground: 0 0% 100%; --info: 221 61% 48%; --info-foreground: 0 0% 100%; @@ -40,30 +40,31 @@ } .dark { - --background: 224 71% 4%; - --foreground: 213 31% 91%; - --card: 222 47% 11%; - --card-foreground: 213 31% 91%; - --popover: 222 47% 11%; - --popover-foreground: 213 31% 91%; - --primary: 217 65% 55%; + --background: 30 6% 9%; + --foreground: 40 8% 90%; + --card: 30 6% 12%; + --card-foreground: 40 8% 90%; + --popover: 30 6% 12%; + --popover-foreground: 40 8% 90%; + --primary: 217 70% 60%; --primary-foreground: 0 0% 100%; - --secondary: 222 47% 15%; - --secondary-foreground: 210 40% 98%; - --muted: 222 47% 11%; - --muted-foreground: 215.4 16.3% 60%; - --accent: 220 14% 14%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62% 40%; + --secondary: 30 5% 17%; + --secondary-foreground: 40 8% 95%; + --muted: 30 5% 16%; + --muted-foreground: 35 6% 62%; + --accent: 30 5% 18%; + --accent-foreground: 40 8% 95%; + --destructive: 0 62% 45%; --destructive-foreground: 0 0% 100%; - --border: 216 34% 17%; - --input: 216 34% 17%; - --ring: 217 65% 55%; - --success: 142 40% 45%; + --border: 30 5% 20%; + --input: 30 5% 22%; + --ring: 217 70% 60%; + --radius: 0.5rem; + --success: 152 45% 45%; --success-foreground: 0 0% 100%; - --warning: 38 92% 50%; - --warning-foreground: 0 0% 100%; - --info: 217 65% 55%; + --warning: 38 92% 55%; + --warning-foreground: 20 14% 8%; + --info: 217 70% 60%; --info-foreground: 0 0% 100%; --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4); From 5e49705da946d99d56d5e3c57cfe3c155bbaa05e Mon Sep 17 00:00:00 2001 From: Thammanoon Semapru Date: Thu, 16 Jul 2026 16:35:52 +0700 Subject: [PATCH 04/11] feat(design): elevation scale + tabular-nums utility + zebra retune --- src/index.css | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/index.css b/src/index.css index 23a31de..fdf91e1 100644 --- a/src/index.css +++ b/src/index.css @@ -29,10 +29,11 @@ --warning-foreground: 0 0% 100%; --info: 221 61% 48%; --info-foreground: 0 0% 100%; - --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06); - --shadow-md: 0 2px 8px rgba(16, 24, 40, 0.08); - --zebra-even: rgba(0, 0, 0, 0.015); - --zebra-hover: hsl(221 61% 48% / 0.05); + --shadow-xs: 0 1px 2px rgba(24, 20, 16, 0.04); + --shadow-sm: 0 1px 2px rgba(24, 20, 16, 0.05); + --shadow-md: 0 4px 12px rgba(24, 20, 16, 0.06); + --zebra-even: rgba(24, 20, 16, 0.02); + --zebra-hover: hsl(221 61% 48% / 0.045); --zebra-hover-accent: hsl(221 61% 48% / 0.5); /* per-BU colour identity (SQL Workbench): hue comes from the BU code, S/L tuned per theme */ --bu-chip-s: 62%; @@ -66,11 +67,12 @@ --warning-foreground: 20 14% 8%; --info: 217 70% 60%; --info-foreground: 0 0% 100%; - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); - --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4); + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45); --zebra-even: rgba(255, 255, 255, 0.015); - --zebra-hover: hsl(217 65% 55% / 0.08); - --zebra-hover-accent: hsl(217 65% 55% / 0.4); + --zebra-hover: hsl(217 70% 60% / 0.08); + --zebra-hover-accent: hsl(217 70% 60% / 0.4); /* lighter chips read better on the dark ground */ --bu-chip-s: 58%; --bu-chip-l: 58%; @@ -99,6 +101,11 @@ body { min-height: 100dvh; } +.tabular-nums { + font-variant-numeric: tabular-nums; + font-feature-settings: "tnum" 1; +} + /* Zebra striping */ .zebra-row:nth-child(even) { background-color: var(--zebra-even); From dd20ce22842cf5d0a353463c81c5417e9fa7bb1a Mon Sep 17 00:00:00 2001 From: Thammanoon Semapru Date: Thu, 16 Jul 2026 16:40:25 +0700 Subject: [PATCH 05/11] feat(ui): add warning + info Badge variants --- src/components/ui/badge.test.tsx | 7 +++++++ src/components/ui/badge.tsx | 2 ++ 2 files changed, 9 insertions(+) diff --git a/src/components/ui/badge.test.tsx b/src/components/ui/badge.test.tsx index 7d8ff77..6aeeb5e 100644 --- a/src/components/ui/badge.test.tsx +++ b/src/components/ui/badge.test.tsx @@ -91,4 +91,11 @@ describe('Badge', () => { rerender(Test); expect(screen.getByText('Test').className).toContain('text-success-foreground'); }); + + it('renders warning and info variants with their status backgrounds', () => { + const { rerender } = render(At risk); + expect(screen.getByText('At risk')).toHaveClass('bg-warning'); + rerender(Info); + expect(screen.getByText('Info')).toHaveClass('bg-info'); + }); }); diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index ffc7b96..cfe7110 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -13,6 +13,8 @@ const badgeVariants = cva( destructive: 'border-transparent bg-destructive text-destructive-foreground', outline: 'text-foreground', success: 'border-transparent bg-success text-success-foreground', + warning: 'border-transparent bg-warning text-warning-foreground', + info: 'border-transparent bg-info text-info-foreground', }, }, defaultVariants: { From b0916c4600afa6d7899b29690190ba10e9bd3fe3 Mon Sep 17 00:00:00 2001 From: Thammanoon Semapru Date: Thu, 16 Jul 2026 16:44:14 +0700 Subject: [PATCH 06/11] feat(ui): calmer Card padding + whisper elevation --- src/components/ui/card.test.tsx | 4 ++-- src/components/ui/card.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ui/card.test.tsx b/src/components/ui/card.test.tsx index a6705fd..e2ab45d 100644 --- a/src/components/ui/card.test.tsx +++ b/src/components/ui/card.test.tsx @@ -15,10 +15,10 @@ describe('Card', () => { it('reproduces the Fluent medium-Card interior layout (padding/gap/stack)', () => { render(Card body); const card = screen.getByText('Card body'); - expect(card.className).toContain('p-3'); + expect(card.className).toContain('p-5'); expect(card.className).toContain('flex'); expect(card.className).toContain('flex-col'); - expect(card.className).toContain('gap-3'); + expect(card.className).toContain('gap-4'); }); it('merges a custom className onto Card', () => { diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index ef535cb..9a05942 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -6,7 +6,7 @@ const Card = React.forwardRef Date: Thu, 16 Jul 2026 16:48:55 +0700 Subject: [PATCH 07/11] feat(ui): quieter DataTable headers + tabular figures Header cells now render text-xs font-medium uppercase tracking-wide text-muted-foreground (merged with existing per-column width/sticky meta classes), including the sortable-header button which previously forced font-semibold and would have overridden the quieter weight. Body cells append tabular-nums so numeric/date/id columns align. Sticky mechanics, zebra striping, column logic, and the auto '#' index column are untouched. --- src/components/ui/data-table.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index c507c98..b12b1f6 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -21,6 +21,7 @@ import { TableRow, } from './table'; import { ArrowUpDown, ArrowUp, ArrowDown, ChevronLeft, ChevronRight } from 'lucide-react'; +import { cn } from '../../lib/utils'; const PAGE_SIZES = [10, 25, 50, 100]; @@ -254,11 +255,14 @@ function DataTable({ {headerGroup.headers.map((header) => ( )?.headerClassName || ''} + className={cn( + 'text-xs font-medium uppercase tracking-wide text-muted-foreground', + (header.column.columnDef.meta as Record)?.headerClassName + )} > {header.isPlaceholder ? null : header.column.getCanSort() ? (
+ {/* Desktop breadcrumb bar */} +
+ +
+ {/* Main Content */} -
+
{children}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 584107c..4ead6ca 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -84,8 +84,8 @@ const Sidebar: React.FC = ({ 'sidebar-item-transition flex items-center gap-3 rounded-lg text-sm font-medium relative group overflow-hidden', showLabel ? 'px-3 py-2.5' : 'justify-center px-2 py-2.5', active - ? 'bg-secondary text-foreground' - : 'text-muted-foreground hover:bg-primary/5 hover:text-foreground' + ? 'bg-primary/10 text-primary' + : 'text-muted-foreground hover:bg-accent hover:text-foreground' )} > {active && ( @@ -310,8 +310,8 @@ const Sidebar: React.FC = ({ className={cn( 'sidebar-item-transition flex items-center gap-3 rounded-lg px-3 py-3 text-sm font-medium relative overflow-hidden group', active - ? 'bg-secondary text-foreground' - : 'text-muted-foreground hover:bg-primary/5 hover:text-foreground' + ? 'bg-primary/10 text-primary' + : 'text-muted-foreground hover:bg-accent hover:text-foreground' )} > {active && ( From 0488a2813c0239f72b6da2c6e2f5524f40ec44d9 Mon Sep 17 00:00:00 2001 From: Thammanoon Semapru Date: Thu, 16 Jul 2026 17:22:56 +0700 Subject: [PATCH 11/11] fix(shell): don't link breadcrumbs to non-existent /platform and /broadcasts index routes Clicking the leading Platform/Broadcasts breadcrumb on a nested page (e.g. /platform/roles, /broadcasts/new) hit the router's catch-all and bounced authenticated admins to Landing, since neither segment has an index route. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/Breadcrumbs.test.tsx | 9 ++++++++- src/components/Breadcrumbs.tsx | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/Breadcrumbs.test.tsx b/src/components/Breadcrumbs.test.tsx index 653eb65..005f46e 100644 --- a/src/components/Breadcrumbs.test.tsx +++ b/src/components/Breadcrumbs.test.tsx @@ -24,11 +24,18 @@ describe('crumbsFromPath', () => { it('handles nested platform routes', () => { expect(crumbsFromPath('/platform/roles')).toEqual([ - { label: 'Platform', to: '/platform' }, + { label: 'Platform' }, { label: 'Roles' }, ]); }); + it('leaves the broadcasts section crumb unlinked (no index route)', () => { + expect(crumbsFromPath('/broadcasts/new')).toEqual([ + { label: 'Broadcasts' }, + { label: 'New' }, + ]); + }); + it('returns an empty list for the dashboard', () => { expect(crumbsFromPath('/dashboard')).toEqual([]); }); diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index e7badcb..f4dcf1e 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -31,6 +31,11 @@ const labelFor = (seg: string): string => SEGMENT_LABELS[seg] ?? seg.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); +// Section segments with no index route of their own — only child routes exist +// (e.g. /platform/roles, /broadcasts/new). Linking to the bare segment would +// hit the router's catch-all and bounce an authenticated admin to Landing. +const NON_NAVIGABLE = new Set(['platform', 'broadcasts']); + // Segments that are opaque record ids (uuid-ish) carry no label of their own. const isIdSegment = (seg: string): boolean => !SEGMENT_LABELS[seg] && /\d/.test(seg) && seg.length > 6; @@ -44,6 +49,7 @@ export function crumbsFromPath(pathname: string): Crumb[] { return meaningful.map((seg, i) => { const isLast = i === meaningful.length - 1; if (isLast) return { label: labelFor(seg) }; + if (NON_NAVIGABLE.has(seg)) return { label: labelFor(seg) }; return { label: labelFor(seg), to: `/${meaningful.slice(0, i + 1).join('/')}` }; }); }