Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions packages/docs/audits/color-token-migration-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# Color Token Migration Audit

This document summarizes the color usage patterns across the codebase and provides migration guidance for adopting the semantic token system.

## Migration Progress

### Completed

- [x] `Layout.jsx` - Main app background (`bg-primary-subtle`)
- [x] `Navbar.jsx` - User dropdown menu
- [x] **All UI components** (`components/ui/`):
- button.tsx, checkbox.tsx, switch.tsx, tabs.tsx
- dialog.tsx, alert-dialog.tsx, select.tsx, progress.tsx
- file-upload.tsx, steps.tsx, menu.tsx, popover.tsx

### Remaining

- [ ] Settings pages (`components/settings/`)
- [ ] Dashboard components (`components/dashboard/`)
- [ ] Project components (`components/project/`)
- [ ] Auth components (`components/auth/`)
- [ ] Sidebar components (`components/sidebar/`)
- [ ] Checklist components (`components/checklist/`)

## Summary

| Category | Occurrences | Files | Priority |
| --------------------------------------- | ----------- | ----- | -------- |
| `gray-*` colors (should be `slate-*`) | 309+ | 50+ | High |
| `bg-blue-50` (app/hover backgrounds) | 33+ | 21 | High |
| `hover:bg-blue-50` patterns | 28 | 21 | Medium |
| `bg-blue-500/600/700` (primary buttons) | 143 | 88 | Medium |
| Status colors (`emerald/amber/red-50`) | 70+ | 30+ | Low |
| Focus ring patterns | 183 | 90 | Low |

## New Tokens Added

The following tokens were added to `global.css` to support common patterns:

```css
/* Light mode */
--color-primary-subtle: oklch(0.97 0.014 254.604); /* blue-50 */
--color-destructive-subtle: oklch(0.971 0.013 17.38); /* red-50 */
--color-success-subtle: oklch(0.979 0.021 166.113); /* emerald-50 */
--color-warning-subtle: oklch(0.987 0.022 95.277); /* amber-50 */

/* Dark mode */
--color-primary-subtle: oklch(0.282 0.091 267.935); /* blue-950 */
--color-destructive-subtle: oklch(0.258 0.092 26.042); /* red-950 */
--color-success-subtle: oklch(0.262 0.051 172.552); /* emerald-950 */
--color-warning-subtle: oklch(0.344 0.075 66.288); /* amber-950 */
```

## Migration Mappings

### High Priority: Gray to Slate

The codebase mixes `gray-*` and `slate-*` colors inconsistently. Slate has a subtle blue undertone that matches the primary brand color better.

| Current | Token Replacement | Direct Replacement |
| ------------------- | --------------------------- | -------------------- |
| `bg-gray-50` | `bg-background` | `bg-slate-50` |
| `bg-gray-100` | `bg-muted` | `bg-slate-100` |
| `bg-gray-200` | `bg-secondary` | `bg-slate-200` |
| `text-gray-500` | `text-muted-foreground` | `text-slate-500` |
| `text-gray-600` | - | `text-slate-600` |
| `text-gray-700` | `text-secondary-foreground` | `text-slate-700` |
| `text-gray-900` | `text-foreground` | `text-slate-900` |
| `border-gray-200` | `border-border` | `border-slate-200` |
| `hover:bg-gray-50` | `hover:bg-muted` | `hover:bg-slate-50` |
| `hover:bg-gray-100` | `hover:bg-muted` | `hover:bg-slate-100` |

### High Priority: Blue-50 Backgrounds

`bg-blue-50` is used extensively for:

- Main app background (Layout.jsx)
- Selected/active states
- Hover states
- Icon container backgrounds
- Info banners

| Current | Token Replacement |
| ------------------------ | ------------------------------- |
| `bg-blue-50` | `bg-primary-subtle` |
| `hover:bg-blue-50` | `hover:bg-primary-subtle` |
| `group-hover:bg-blue-50` | `group-hover:bg-primary-subtle` |

**Key file:** `Layout.jsx:80` uses `bg-blue-50` for the main app background.

### Medium Priority: Primary Button Colors

| Current | Token Replacement |
| ----------------------- | ----------------------------- |
| `bg-blue-600` | `bg-primary` |
| `bg-blue-700` | - (use `hover:bg-primary/90`) |
| `hover:bg-blue-700` | `hover:bg-primary/90` |
| `text-blue-600` | `text-primary` |
| `text-blue-700` | `text-primary` |
| `border-blue-600` | `border-primary` |
| `ring-blue-600` | `ring-ring` |
| `focus:ring-blue-500` | `focus:ring-ring` |
| `focus:border-blue-500` | `focus:border-primary` |

### Low Priority: Status Colors

For status indicators, banners, and badges:

| Current | Token Replacement |
| ------------------ | ----------------------- |
| `bg-emerald-50` | `bg-success-subtle` |
| `bg-green-50` | `bg-success-subtle` |
| `bg-emerald-600` | `bg-success` |
| `text-emerald-600` | `text-success` |
| `bg-amber-50` | `bg-warning-subtle` |
| `bg-yellow-50` | `bg-warning-subtle` |
| `bg-amber-500` | `bg-warning` |
| `text-amber-600` | `text-warning` |
| `bg-red-50` | `bg-destructive-subtle` |
| `bg-red-600` | `bg-destructive` |
| `text-red-600` | `text-destructive` |

## Files by Category

### Layout and Core (migrate first)

- `Layout.jsx` - Main app background (`bg-blue-50`)
- `Navbar.jsx` - Navigation styling
- `components/sidebar/*` - Sidebar components

### Settings Pages (already partially migrated)

- `ProfileSettings.jsx` - Uses tokens
- `PersonaSection.jsx` - Uses some tokens
- `AcademicInfoSection.jsx` - Uses some tokens
- Other settings pages - Need migration

### UI Components

- `components/ui/*.tsx` - Base UI components (buttons, inputs, etc.)
- These should use tokens for maximum theme flexibility

### Project Views

- `components/project/*` - Heavy use of blue-50 and gray colors
- `components/dashboard/*` - Mixed gray/slate usage

### Mocks (low priority)

- `components/mocks/*` - Demo/prototype components
- Can be migrated last or left as-is

## Suggested Migration Order

1. **Layout.jsx** - Change `bg-blue-50` to `bg-primary-subtle`
2. **UI components** (`components/ui/`) - Ensure base components use tokens
3. **Settings pages** - Complete token migration
4. **Dashboard components** - Migrate gray to slate/tokens
5. **Project components** - Largest surface area
6. **Remaining files** - Mocks and edge cases

## Considerations

### Gray vs Slate Decision

The codebase should standardize on either:

- **Option A:** Use `slate-*` everywhere (has blue undertone, matches brand)
- **Option B:** Use `gray-*` everywhere (pure neutral)

Recommendation: Use `slate-*` since it complements the blue primary color.

### When NOT to Use Tokens

Some cases may warrant keeping direct color references:

- One-off decorative elements
- Complex gradients
- Third-party component overrides
- Status badge colors that need to remain consistent regardless of theme

### Opacity Variants

For hover/focus states, prefer opacity variants over separate colors:

```jsx
// Preferred
className = 'bg-primary hover:bg-primary/90';

// Avoid
className = 'bg-blue-600 hover:bg-blue-700';
```

## Testing Dark Mode

After migration, test dark mode by adding `class="dark"` to the `<html>` element:

```js
document.documentElement.classList.add('dark');
```

All token-based colors should automatically switch to their dark variants.
4 changes: 2 additions & 2 deletions packages/web/src/Layout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default function Layout(props) {

return (
<div
class={`flex h-screen flex-col overflow-hidden bg-blue-50 ${isImpersonating() ? 'pt-10' : ''}`}
class={`bg-primary-subtle flex h-screen flex-col overflow-hidden ${isImpersonating() ? 'pt-10' : ''}`}
>
<Show when={isImpersonating()}>
<Suspense>
Expand All @@ -99,7 +99,7 @@ export default function Layout(props) {
onWidthChange={handleWidthChange}
/>
</Show>
<main class='flex-1 overflow-auto text-gray-900'>{props.children}</main>
<main class='text-foreground flex-1 overflow-auto'>{props.children}</main>
</div>
<ToasterContainer />
{/* Dev Panel - global, context-aware */}
Expand Down
14 changes: 7 additions & 7 deletions packages/web/src/components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,21 +164,21 @@ export default function Navbar(props) {
</button>

<Show when={showUserMenu()}>
<div class='absolute right-0 z-50 mt-2 w-48 rounded-md border border-gray-200 bg-white py-1 text-gray-700 shadow-lg'>
<div class='border-b border-gray-200 px-4 py-2 text-sm'>
<div class='font-medium text-gray-900'>{user()?.name || 'User'}</div>
<div class='truncate text-xs text-gray-500'>{user()?.email}</div>
<div class='border-border bg-popover text-popover-foreground absolute right-0 z-50 mt-2 w-48 rounded-md border py-1 shadow-lg'>
<div class='border-border border-b px-4 py-2 text-sm'>
<div class='text-foreground font-medium'>{user()?.name || 'User'}</div>
<div class='text-muted-foreground truncate text-xs'>{user()?.email}</div>
</div>
<A
href='/settings/profile'
class='block px-4 py-2 text-sm hover:bg-gray-100'
class='hover:bg-muted block px-4 py-2 text-sm'
onClick={() => setShowUserMenu(false)}
>
Profile
</A>
<A
href='/settings'
class='block px-4 py-2 text-sm hover:bg-gray-100'
class='hover:bg-muted block px-4 py-2 text-sm'
onClick={() => setShowUserMenu(false)}
>
Settings
Expand All @@ -188,7 +188,7 @@ export default function Navbar(props) {
setShowUserMenu(false);
handleSignOut();
}}
class='block w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-gray-100'
class='text-destructive hover:bg-muted block w-full px-4 py-2 text-left text-sm'
>
Sign Out
</button>
Expand Down
14 changes: 7 additions & 7 deletions packages/web/src/components/admin/AdminDashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export default function AdminDashboard() {
<Show
when={isAdmin()}
fallback={
<div class='flex min-h-100 flex-col items-center justify-center text-gray-500'>
<div class='text-muted-foreground flex min-h-100 flex-col items-center justify-center'>
<FiAlertCircle class='mb-4 h-12 w-12' />
<p class='text-lg font-medium'>Access Denied</p>
<p class='text-sm'>You do not have admin privileges.</p>
Expand Down Expand Up @@ -130,7 +130,7 @@ export default function AdminDashboard() {
description='Manage system users and their access'
cta={
<div class='relative'>
<FiSearch class='absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400' />
<FiSearch class='text-muted-foreground/70 absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2' />
<input
type='text'
placeholder='Search by name or email...'
Expand All @@ -156,8 +156,8 @@ export default function AdminDashboard() {

{/* Pagination */}
<Show when={usersData()?.pagination}>
<div class='flex items-center justify-between border-t border-gray-200 px-6 py-4'>
<p class='text-sm text-gray-500'>
<div class='border-border flex items-center justify-between border-t px-6 py-4'>
<p class='text-muted-foreground text-sm'>
Showing {(page() - 1) * (usersData()?.pagination?.limit || 20) + 1} to{' '}
{Math.min(
page() * (usersData()?.pagination?.limit || 20),
Expand All @@ -169,19 +169,19 @@ export default function AdminDashboard() {
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page() === 1}
class='rounded-lg border border-gray-200 p-2 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50'
class='border-border hover:bg-muted rounded-lg border p-2 transition-colors disabled:cursor-not-allowed disabled:opacity-50'
>
<FiChevronLeft class='h-4 w-4' />
</button>
<span class='text-sm text-gray-500'>
<span class='text-muted-foreground text-sm'>
Page {page()} of {usersData()?.pagination?.totalPages || 1}
</span>
<button
onClick={() =>
setPage(p => Math.min(usersData()?.pagination?.totalPages || 1, p + 1))
}
disabled={page() >= (usersData()?.pagination?.totalPages || 1)}
class='rounded-lg border border-gray-200 p-2 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50'
class='border-border hover:bg-muted rounded-lg border p-2 transition-colors disabled:cursor-not-allowed disabled:opacity-50'
>
<FiChevronRight class='h-4 w-4' />
</button>
Expand Down
8 changes: 4 additions & 4 deletions packages/web/src/components/admin/AdminLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,16 @@ export default function AdminLayout(props) {
<Show
when={isAdmin()}
fallback={
<div class='flex min-h-100 flex-col items-center justify-center text-gray-500'>
<div class='text-muted-foreground flex min-h-100 flex-col items-center justify-center'>
<FiAlertCircle class='mb-4 h-12 w-12' />
<p class='text-lg font-medium'>Access Denied</p>
<p class='text-sm'>You do not have admin privileges.</p>
</div>
}
>
<div class='mx-auto min-h-full bg-gray-50'>
<div class='bg-muted mx-auto min-h-full'>
{/* Navbar */}
<div class='border-b border-gray-200 bg-white'>
<div class='border-border bg-card border-b'>
<div class='px-6'>
<nav class='flex space-x-1' role='navigation' aria-label='Admin navigation'>
<For each={navItems}>
Expand All @@ -86,7 +86,7 @@ export default function AdminLayout(props) {
class={`flex items-center space-x-2 rounded-t-lg border-b-2 px-4 py-3 text-sm font-medium transition-colors ${
active() ?
'border-blue-600 bg-blue-50 text-blue-700'
: 'border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-900'
: 'text-muted-foreground hover:border-border hover:bg-muted hover:text-foreground border-transparent'
}`}
>
<Icon class='h-4 w-4' />
Expand Down
Loading