Phase 0 discovery. No code changed yet. This audit is the input to the design-system and migration work in later phases. Read the "Near-duplicate flags" and "Non-token color usage" sections before writing any implementation code.
| Concern | Finding |
|---|---|
| Framework | React 19 (react@^19.1, react-dom@^19.1) |
| Router | react-router@^7.12 (data routes in web/src/App.tsx) |
| Build tool | Vite 7 (vite@^7, @vitejs/plugin-react) |
| Language | TypeScript ~5.9, strict project refs (tsc -b) |
| Styling | Tailwind CSS v4 via @tailwindcss/vite — confirmed v4 by the @theme { … } block in web/src/index.css and the absence of any tailwind.config.{js,ts}. No CSS modules, no styled-components, no SASS. A handful of inline style={{…}} props exist only for dynamic viewport / safe-area math, not color. |
| Icons | lucide-react |
| Markdown | streamdown (assistant message rendering) |
| Tests | Vitest + Testing Library (unit), Playwright (e2e) |
Conclusion: Tailwind v4 is already installed and configured correctly. This is a re-theme of an existing, already-tokenized design system, not a from-scratch Tailwind introduction. The bulk of the work is (a) repointing the semantic token layer to the new palette, (b) adding the raw 5-scale palette as theme variables, (c) eliminating the small number of non-token color usages, and (d) consolidating the inline "pill/badge" pattern.
- Version: v4. Tokens are declared in a single
@themeblock in web/src/index.css and dark mode is handled by overriding the same custom properties underhtml[data-theme="dark"](web/src/index.css). - Theme switching is real and wired: an inline boot script in web/index.html
sets
data-themebefore paint; web/src/theme.ts + the nav in web/src/components/Layout.tsx togglelight/dark/system. - Existing semantic tokens (already consumed across the app):
primary,primary-hover,primary-dark,on-primary,bg,surface,surface-2/3/alt/muted/hover,text,text-muted,text-subtle,border,divider,success,warning,danger,danger-hover,error, plus chat tokenschat-bg,bubble-in/out/system,focus, and non-color tokens for radius/shadow/layout. - There is no raw color scale (no
--color-primary-500family); only semantic aliases exist today. The new palette ships as full 50–950 scales, so Phase 2 will add the raw scales and keep the semantic alias layer on top.
| File | Component(s) | Visual purpose | Props |
|---|---|---|---|
| Button.tsx | Button |
Text button | variant: primary|secondary|danger|ghost, size: sm|md|lg, native button attrs |
| Card.tsx | Card, CardHeader, CardContent |
Surface container w/ optional header & body | children, className, onClick |
| Input.tsx | Input |
Labeled text field w/ error | label, error, native input attrs |
| Alert.tsx | Alert |
Inline status banner w/ icon | variant: info|success|warning|error, children |
| SectionHeader.tsx | SectionHeader |
Icon + title + subtitle + action row | icon, title, subtitle, action |
| ProtectedRoute.tsx | ProtectedRoute |
Auth/role route guard (renders a loading state) | children, requiredRole |
| NotificationBanner.tsx | NotificationBanner |
Push-enable / push-blocked banner | none (reads route + hook) |
| File | Component | Visual purpose | Props |
|---|---|---|---|
| IconButton.tsx | IconButton |
Circular 44px icon-only button | children, label, native attrs |
| PageHeader.tsx | PageHeader |
Sticky page title bar + actions | title, actions |
| ListRow.tsx | ListRow |
Tappable list row (avatar/primary/secondary/trailing/unread) | avatar, primary, secondary, trailing, unread, onClick |
| BottomNav.tsx | BottomNav |
Bottom tab bar (portrait) | none (internal navItems) |
| ChatHeader.tsx | ChatHeader |
Chat top bar: back, title, connection status pill, kebab menu, debug toggle | title, avatar, backTo, hideBack, connectionStatus, menuItems, debugMode, onToggleDebug |
| MessageBubble.tsx | MessageBubble |
Chat bubble (user/assistant/system) + debug panel + condition pill | role, content, metadata, timestamp, isGroupContinuation, isStreaming, debugInfo, showDebug |
| Composer.tsx | Composer |
Auto-growing message input + send | onSend, disabled |
| File | Component | Visual purpose |
|---|---|---|
| Layout.tsx | Layout |
Top-nav chrome for public/auth pages (landing, login, register, admin, activation, onboarding) incl. theme toggle + mobile hamburger |
| MobileShell.tsx | MobileShell |
Portrait app shell: scroll area + BottomNav |
| ChatShell.tsx | ChatShell |
Portrait immersive chat shell (no nav) + NotificationBanner |
| shell/AppShell.tsx | AppShell |
Responsive switch: MobileShell (bottom) ↔ SideRailShell |
| shell/ChatAppShell.tsx | ChatAppShell |
Responsive switch: ChatShell (bottom) ↔ ChatShellSide |
| shell/SideRailShell.tsx | SideRailShell |
Desktop/landscape: NavRail + optional chat-list pane + main |
| shell/ChatShellSide.tsx | ChatShellSide |
Desktop chat: NavRail + chat-list pane + thread |
| shell/NavRail.tsx | NavRail |
Vertical 72px nav rail (desktop) |
| File | Component | Visual purpose |
|---|---|---|
| chat/AssistantMarkdown.tsx | AssistantMarkdown |
Streamdown markdown renderer w/ prose tweaks |
| chat/FeedbackPollWidget.tsx | FeedbackPollWidget |
In-bubble feedback poll |
| chats/ChatListPane.tsx | ChatListPane |
Project/thread list (used standalone on mobile + embedded in desktop rail shells) |
Landing, Login, Register, Activation, Onboarding, OnboardingNotifications,
Dashboard, ChatThread, Updates, UpdatesPage, Notifications, Settings, Admin.
All consume the primitives above; none define their own button/card/input variants except
the inline status-pill / form-control patterns noted below.
-
Badge / pill / chip — implemented inline in ≥4 places, no canonical component.
- Condition pills
A/B/C/Din MessageBubble.tsx - Connection-status pill (
online/reconnecting/offline) in ChatHeader.tsx - Membership-status pill (
active/paused/ended) + health-check pill in Admin.tsx - Unread dot in ListRow.tsx and [BottomNav]/Dashboard
- Action: introduce ONE
Badgeprimitive withvariant+ optionaldot, migrate all four. This is the single clearest consolidation win.
- Condition pills
-
Navigation item list duplicated. BottomNav.tsx and NavRail.tsx each hand-maintain their own
navItemsarray (same routes, icons, labels; NavRail adds Admin). The two renderers are legitimately different (horizontal tabs vs vertical rail), but the data should be a single shared config to prevent drift. Not a component merge — a data-extraction. -
Header-ish components ×3. PageHeader.tsx, ChatHeader.tsx and the top nav inside Layout.tsx are all sticky top bars. They serve genuinely different roles (simple page title vs interactive chat bar vs app-wide nav), so this is kept-as-is — flagged here only so it is a conscious decision, not an accident.
-
Seven shell files look duplicative but are intentional.
AppShell/ChatAppShellare responsive routers that pick between a portrait shell (MobileShell/ChatShell) and a desktop shell (SideRailShell/ChatShellSide). This is a deliberate responsive architecture, not copy-paste duplication.NotificationBanneris rendered by both the portrait (ChatShell) and desktop (ChatShellSide) chat shells — consistent, fine. No consolidation needed; documented so a later reader doesn't "clean it up" by mistake. -
No competing Button/Card/Input/Modal implementations. There is exactly one of each.
Buttonis already variant-driven (4 variants × 3 sizes). There is noPrimaryButton,ButtonV2, second card, or second modal. The codebase is in good shape on this axis.
These are the only places that bypass the semantic token layer and hardcode Tailwind default palette values or undefined tokens. Every one of these gets routed through a token in Phase 3.
| Location | Offending classes | Problem |
|---|---|---|
| Alert.tsx | bg-yellow-500/10 text-yellow-600 border-yellow-500/20 |
warning variant uses raw Tailwind yellow instead of the warning token. (Note: Alert.test.tsx asserts these exact classes — test must be updated alongside.) |
| MessageBubble.tsx | bg-blue-100 text-blue-800, bg-purple-100 …, bg-amber-100 …, bg-emerald-100 … |
Condition pills hardcode 4 raw palettes. Should map onto palette/Badge variants. |
| MessageBubble.tsx | bg-muted |
Undefined token — there is no --color-muted; renders nothing. Bug surfaced by audit. |
| MessageBubble.tsx | text-red-500 |
Debug error text hardcodes red; should be text-danger. |
| UpdatesPage.tsx | text-gray-500 |
Empty-state text hardcodes gray; should be text-text-muted. |
| manifest.json | theme_color: #f2f3f5 |
PWA theme color hardcoded to the old neutral bg; must track the new palette's surface/bg. |
| index.html | meta theme-color #f2f3f5 |
Same as above for the browser chrome color. |
Everything else in the app already uses semantic token classes (bg-surface, text-text,
text-text-muted, bg-primary, border-border, bg-success/10, bg-danger/10, …), so the
re-theme propagates automatically once the token values in index.css are repointed.
- Keep the existing split: primitives in
components/+components/ui/, shells incomponents/shell/, feature components incomponents/chat/&components/chats/. It already approximates a primitives-vs-composed convention; we will formalize it in the design-system doc rather than reshuffle folders (low risk, no call-site churn). - Add one primitive:
Badge(consolidates flag #1). - Extract one config: shared
navItemsconsumed by bothBottomNavandNavRail(flag #2). - No other new components anticipated.
This completes Phase 0. Per the task protocol, no styles or colors have been migrated yet.
Pending review of this audit, Phase 1 will produce DESIGN_SYSTEM.md (semantic token mapping
of the five scales, light/dark resolution, type scale, and a WCAG contrast pass) before any
implementation.