Appearance
Epic 0 — Design System Expansion
All UI components must live in packages/design-system. Build these before or in parallel with feature epics.
DS-001 — TalentCard component
- [x] Implemented
Files:
- Create:
packages/design-system/src/components/ui/TalentCard.tsx - Edit:
packages/design-system/src/index.ts— add export
Description: Swipeable card for the Discover Feed. Shows talent photo, name, primary category, top skills, location, and match score badge. Supports swipe-right (shortlist), swipe-left (skip), and an X button (block). Built on Card + Radix motion or CSS transforms. Uses cva for state variants (default, swiping-right, swiping-left).
Props: talent: TalentCardData, onShortlist, onSkip, onBlock, matchScore?: number, matchReasons?: string[], className?
Acceptance criteria:
- Renders photo with gradient overlay, name, category, skills chips, location
- Swipe gesture works on touch and mouse drag
- Action buttons (✓, ✗, ×) trigger respective callbacks
- Match score badge visible when
matchScoreis provided
DS-002 — JobCard component
- [x] Implemented
Files:
- Create:
packages/design-system/src/components/ui/JobCard.tsx - Edit:
packages/design-system/src/index.ts
Description: Card for job listings. Shows title, producer name, production type, location (or Remote badge), payment type, commitment type, and deadline. Tappable — navigates to job detail. Uses Card primitive.
Props: job: JobCardData, onClick, className?
DS-003 — SkillBadge / Chip component
- [x] Implemented
Files:
- Create:
packages/design-system/src/components/ui/Chip.tsx - Edit:
packages/design-system/src/index.ts
Description: Compact label chip for skills, categories, genres. Extends Badge with a removable variant (shows × icon when onRemove is provided). Used in profile editors, search filters, talent cards.
Props: label: string, onRemove?: () => void, variant?: 'default' | 'outline' | 'filled', size?: 'sm' | 'md'
DS-004 — Select and MultiSelect components
- [x] Implemented
Files:
- Create:
packages/design-system/src/components/ui/Select.tsx - Create:
packages/design-system/src/components/ui/MultiSelect.tsx - Edit:
packages/design-system/src/index.ts
Description: Select wraps Radix Select primitive with project styling. MultiSelect shows selected items as Chip components and opens a dropdown with checkboxes. Used for: primary category, skills, languages, production types, filters.
DS-005 — FileUpload component
- [x] Implemented
Files:
- Create:
packages/design-system/src/components/ui/FileUpload.tsx - Edit:
packages/design-system/src/index.ts
Description: Drag-and-drop + click-to-browse file upload zone. Shows upload progress bar. Accepts accept (MIME types), maxSize, multiple. Emits onFiles(files: File[]). Used for: profile photo, media uploads, reel builder.
DS-006 — EmptyState component
- [x] Implemented
Files:
- Create:
packages/design-system/src/components/ui/EmptyState.tsx - Edit:
packages/design-system/src/index.ts
Description: Centered empty state with icon slot, title, description, and optional CTA button. Used across all list pages when there is no data.
Props: icon?: ReactNode, title: string, description?: string, action?: { label: string; onClick: () => void }
DS-007 — Skeleton loaders
- [x] Implemented
Files:
- Create:
packages/design-system/src/components/ui/Skeleton.tsx - Edit:
packages/design-system/src/index.ts
Description: Animated shimmer skeleton with Skeleton, SkeletonText, SkeletonAvatar, SkeletonCard. Used in all React Query loading states.
DS-008 — Toast / Notification Banner
- [x] Implemented
Files:
- Create:
packages/design-system/src/components/ui/Toast.tsx - Edit:
packages/design-system/src/index.ts
Description: Wraps Radix Toast. Supports success, error, info, warning variants. Provides a useToast() hook to trigger toasts from anywhere. Used for action confirmations, API error messages.
DS-009 — BottomTabBar component
- [x] Implemented
Files:
- Create:
packages/design-system/src/components/layout/BottomTabBar.tsx - Edit:
packages/design-system/src/index.ts
Description: Fixed bottom navigation bar for the app (mobile-first). Receives an array of { label, icon, path } nav items. Active tab is highlighted with brand color. Used in apps/app as the primary navigation structure matching the Figma Jornada flows.
DS-010 — ProgressRing / ProfileCompleteness component
- [x] Implemented
Files:
- Create:
packages/design-system/src/components/ui/ProgressRing.tsx - Edit:
packages/design-system/src/index.ts
Description: SVG circular progress ring for profile completeness indicator. Shown on profile pages and in the discover feed card.
Props: value: number (0–100), size?: number, strokeWidth?: number, className?
DS-011 — SearchBar component
- [x] Implemented
Files:
- Create:
packages/design-system/src/components/ui/SearchBar.tsx - Edit:
packages/design-system/src/index.ts
Description: Styled search input with a leading search icon, optional clear (×) button, and onSearch(query) callback on submit/Enter. Used in the talent search and job board pages.
DS-012 — FilterSheet component
- [x] Implemented
Files:
- Create:
packages/design-system/src/components/ui/FilterSheet.tsx - Edit:
packages/design-system/src/index.ts
Description: Bottom sheet drawer (Radix Dialog styled as sheet) for search/filter options. Contains slots for filter sections. Shows an "Apply" and "Reset" button. Used on the Discover and Jobs pages.
DS-013 — MediaPlayer component
- [x] Implemented
Files:
- Create:
packages/design-system/src/components/ui/MediaPlayer.tsx - Edit:
packages/design-system/src/index.ts
Description: Video/audio player using native HTML5 <video>/<audio> with custom controls overlay (play/pause, scrubber, time, volume). Handles thumbnail display before play. Used on talent profile for demo reels.
DS-014 — Migrate landing page components to design system
- [x] Implemented
Files to refactor (landing → use DS imports):
apps/landing/src/components/HeroSection.tsx— extract Button CTA to<Button>from DSapps/landing/src/components/Nav.tsx— use DSButtonfor CTAapps/landing/src/components/WaitlistForm.tsx— use DSInput,Buttonapps/landing/src/components/Footer.tsx— use DSButtonwhere applicableapps/landing/src/components/FeaturesSection.tsx— use DSCardapps/landing/src/components/CategoriesSection.tsx— use DSBadge/Chip
Description: Replace inline Tailwind patterns that duplicate design system components. Import from @castyou/design-system. Do not change visual appearance — only replace duplicated component patterns. Keep Next.js / next-intl specific logic in place.
DS-015 — Light / Dark mode system + ThemeToggle
- [x] Implemented
Files:
- Create:
packages/design-system/src/tokens/theme.css— CSS custom property definitions for both modes - Edit:
packages/design-system/src/tokens/index.ts— add semantic token names as JS constants - Create:
packages/design-system/src/providers/ThemeProvider.tsx— React context, persistence,prefers-color-schemedetection - Create:
packages/design-system/src/hooks/useTheme.ts—useTheme()hook - Create:
packages/design-system/src/components/ui/ThemeToggle.tsx— sun/moon toggle button - Edit:
packages/design-system/src/index.ts— exportThemeProvider,useTheme,ThemeToggle - Edit:
packages/design-system/tailwind.config.ts— wire semantic tokens to CSS variables - Edit: every existing DS component — replace hardcoded
bg-neutral-900,text-white,border-white/10etc. with semantic token classes (bg-surface,text-primary,border-default, etc.) - Edit:
apps/app/src/main.tsx— wrap app in<ThemeProvider> - Edit:
packages/design-system/src/components/layout/AppHeader.tsx— add<ThemeToggle>to header
Design reference: the attached screenshots show the intended light and dark visual language.
| Element | Light mode | Dark mode |
|---|---|---|
| Page background | #f5f0fc — soft lavender | #0d0820 — near-black purple |
| Card surface | #ffffff — white | #1a0f35 — dark purple |
| Card surface alt | #faf8ff — off-white lavender | #221442 — mid purple |
| Text primary | #1a0a2e — deep purple-black | #ffffff — white |
| Text secondary | #6D10A3 — brand purple | #c4a8e8 — soft lavender |
| Text muted | #8b7aa0 | #7a6a8a |
| Border | #e8d9f5 — light purple tint | #2d1b4e — dark purple border |
| Input background | #ffffff | #150c2e |
| Input border | #d4b8f0 | #3d2460 |
Semantic CSS custom properties to define in theme.css:
css
:root {
/* surfaces */
--color-surface-page: #f5f0fc;
--color-surface-card: #ffffff;
--color-surface-card-alt: #faf8ff;
--color-surface-input: #ffffff;
--color-surface-overlay: rgba(109, 16, 163, 0.06);
/* text */
--color-text-primary: #1a0a2e;
--color-text-secondary: #6D10A3;
--color-text-muted: #8b7aa0;
--color-text-inverse: #ffffff;
/* borders */
--color-border: #e8d9f5;
--color-border-input: #d4b8f0;
--color-border-focus: #6D10A3;
/* brand (same in both modes) */
--color-brand: #6D10A3;
--color-brand-hover: #5a0d87;
--color-violet: #7367F0;
}
.dark {
--color-surface-page: #0d0820;
--color-surface-card: #1a0f35;
--color-surface-card-alt: #221442;
--color-surface-input: #150c2e;
--color-surface-overlay: rgba(115, 103, 240, 0.10);
--color-text-primary: #ffffff;
--color-text-secondary: #c4a8e8;
--color-text-muted: #7a6a8a;
--color-text-inverse: #1a0a2e;
--color-border: #2d1b4e;
--color-border-input: #3d2460;
--color-border-focus: #9b45d4;
}Tailwind semantic utility classes to add (via CSS variables):
js
// tailwind.config.ts — extend colors
colors: {
surface: {
page: 'var(--color-surface-page)',
card: 'var(--color-surface-card)',
'card-alt': 'var(--color-surface-card-alt)',
input: 'var(--color-surface-input)',
overlay: 'var(--color-surface-overlay)',
},
text: {
primary: 'var(--color-text-primary)',
secondary: 'var(--color-text-secondary)',
muted: 'var(--color-text-muted)',
inverse: 'var(--color-text-inverse)',
},
border: {
DEFAULT: 'var(--color-border)',
input: 'var(--color-border-input)',
focus: 'var(--color-border-focus)',
},
}Chip colour palette (same in both modes — vibrant, category-coded):
The skill chips in the screenshots use distinct vivid colours per category. These should become chip variant values — not generic colours but named categories:
| Variant | Background | Text | Example use |
|---|---|---|---|
dance | #FF6B35 (orange) | white | Dance |
acting | #E91E8C (hot pink) | white | Acting |
hiphop | #7367F0 (violet) | white | HipHop |
language-native | #22C55E (green) | white | Native |
language-accent | #14B8A6 (teal) gradient | white | English • British • Scouse |
default | var(--color-surface-card-alt) | var(--color-text-primary) | generic |
Add these as new variants to the Chip component (cva config).
ThemeProvider behaviour:
- On mount: read
localStorage.getItem('castyou-theme')— apply saved preference - If no saved preference: check
window.matchMedia('(prefers-color-scheme: dark)')— apply system preference - On toggle: flip
.darkclass on<html>, persist choice tolocalStorage - Export
useTheme(): { theme: 'light' | 'dark'; toggle: () => void; setTheme: (t: 'light' | 'dark') => void }
ThemeToggle component:
- Sun icon (light mode) / Moon icon (dark mode) from
lucide-react - Smooth icon crossfade transition (200ms)
- Available in two sizes:
size="sm"(icon-only, for header) andsize="md"(icon + label, for settings page) - Uses DS
Buttonwithvariant="ghost"as base
Component migration checklist — every DS component must be audited. Replace:
| Replace | With |
|---|---|
bg-neutral-900 / bg-neutral-800 | bg-surface-card / bg-surface-page |
text-white (body text) | text-text-primary |
text-neutral-400 / text-neutral-500 (muted) | text-text-muted |
border-white/10 / border-neutral-700 | border-border |
bg-white (card/input surfaces) | bg-surface-card / bg-surface-input |
text-neutral-900 (dark text) | text-text-primary |
Components to audit: Button, Input, Textarea, Card, Badge, Chip, Select, MultiSelect, Modal, Toast, FilterSheet, SearchBar, FileUpload, EmptyState, Skeleton, MediaPlayer, AppShell, AppHeader, BottomTabBar.
Acceptance criteria:
- Toggling theme switches the full app instantly with no flash
- On first load, system preference is respected
- Preference persists across page reloads (localStorage)
- All DS components look correct in both light and dark mode — no hardcoded dark-only or light-only colours remain in component files
ThemeToggleis visible inAppHeaderapps/landingalso supports light/dark (wrap inThemeProvider, add toggle toNav)- No regression in existing tests; add new tests for
useThemehook (toggle, persistence, system preference fallback)
DS-016 — Migrate local app components to design system
- [x] Implemented
Files:
- Audit:
apps/app/src/components/— identify every component not imported from@castyou/design-system - Move each to
packages/design-system/src/components/ui/orlayout/ - Edit:
packages/design-system/src/index.ts— export each migrated component - Edit: all
apps/appandapps/landingimport sites — update to@castyou/design-system - Delete the original local file after import sites are updated
- Run:
pnpm --filter @castyou/app test run+pnpm --filter @castyou/design-system build— must pass
Components to audit and migrate (non-exhaustive — run the audit first):
| Likely location | Component | Notes |
|---|---|---|
apps/app/src/components/ | AppShell, AppHeader | Already referenced in DS tickets — confirm they live in DS |
apps/app/src/components/ | CaStarsWidget | Defined in FE-CASTARS-001 as a local component — move to DS |
apps/app/src/components/ | BundleCard | FE-CASTARS-002 — move to DS |
apps/app/src/components/ | FeatureStoreCard | FE-CASTARS-003 — move to DS |
apps/app/src/components/ | LivenessChallenge | Exception — owns raw MediaPipe/camera logic, stays local |
apps/app/src/components/ | StripeCheckout | Exception — owns Stripe Elements, stays local |
apps/app/src/components/feed/ | PostCard | Epic 16 — should live in DS from the start |
apps/app/src/components/groups/ | GroupApplyModal | Epic 9 — move to DS |
apps/app/src/components/reel/ | TransitionPicker | Epic 7 v2 — move to DS |
apps/app/src/components/folders/ | SaveToFolderButton | Epic 19 — move to DS |
apps/app/src/components/ | ExportPitchModal | Epic 18 — move to DS |
Audit command to find local components not from DS:
bash
grep -rL "@castyou/design-system" apps/app/src/components/ --include="*.tsx"Rules:
- A component moves to DS if it is (or could be) used in more than one page, or is visually part of the product's design language.
- A component stays local if it owns a third-party SDK boundary (Stripe, MediaPipe, Firebase) — wrap it in a DS shell instead.
- After migration, the component must use DS tokens only (no raw Tailwind colour literals — see DS-015 migration table).