theme-system
The investor theme system on sahin.io/angel-investments — theme colors and ids in InvestorThemeContext, the --investor-* CSS variables (themeVars.ts, theme bootstrap script, themed public-page CSS), full-page hub views in client/src/components/views/*View.tsx, theme copy in content/themes/*.yaml, the gallery in ThemeSwitcher, and the no-VC-firm-logo rule. Use when adding, changing or removing a theme or hub view, touching theme colors, or styling chrome that must follow the active theme.
- Source file
.claude/skills/theme-system/SKILL.md
Theme system
What it is
A visitor picks a design ("Change Design" gallery). The choice is a theme id, read from
?theme={id}, then localStorage["investor-theme"], else default. A theme has two parts:
a six-color palette that recolors every page (hub, chrome, static and React pages) and,
for some ids, a full-page hub view that replaces the default hub layout.
client/src/contexts/ThemeContext.tsx is a separate light/dark provider (ThemeProvider,
useTheme, localStorage["theme"] when switchable); it is not the investor theme.
Files
| File | Role |
|---|---|
client/src/contexts/InvestorThemeContext.tsx |
investorThemes (id → { id, name, description, emoji, colors: { background, foreground, primary, accent, card, border } }), InvestorThemeId, InvestorThemeProvider (deferStoredTheme on SSR), useInvestorTheme(), getThemeLogoUrl(); sets data-theme on <html> (removed for default) |
content/shared/themes.yaml |
gallery copy, gallery groups (VC firm themes, Design Concepts, Classic), every theme's name + description by id, thumbnail previews → client/src/generated/copy/shared/themes.ts |
content/themes/<theme>.yaml |
every word inside one hub view (e.g. a16z.yaml: heading, status map Live/Public/Acquired/Failed → the firm's words, filters) → client/src/generated/copy/themes/<theme>.ts; shared.ts and view-modes.ts are also generated there |
client/src/components/views/*View.tsx |
hub views; props ViewProps (views/types.ts: companies, navigateToCompany, formatCurrency, statusColor, fg/bg/primary/card/border); shared parts in publicThemePrimitives.tsx; non-theme view modes exported from views/index.ts |
client/src/pages/Home.tsx |
THEME_VIEWS map (theme id → lazy view); THEME_VIEWS[themeId] replaces the default layout; only CardGridView ships in the hub bundle |
client/src/components/ThemeSwitcher.tsx |
gallery modal (portal to document.body, MiniPreview thumbnails) |
client/src/lib/themeVars.ts |
tv(key) = var(--investor-<key>), tvMix(key, pct) = color-mix(...); all React chrome colors use these |
server/publicReactDocument.ts |
themeBootstrapScript() (inline head script: applies stored/URL theme palette as --investor-* and data-theme, plus data-sidebar, before first paint), themedPublicPageCss() / buildThemedCssAsset() (static page CSS rescoped under :root[data-theme] .public-page, written as assets/public-page-theme-<hash>.css), themedColor() |
client/src/contexts/ViewModeContext.tsx |
human vs agent view (useViewMode().isAgent) |
Rules
- No VC firm logos (owner rule): a firm theme copies the firm's layout, type and imagery
style, never its logo or its name as a wordmark; Sahin's own mark goes in that slot.
LOGO_URLSin InvestorThemeContext holds portfolio-company logos only.server/vc-theme-management.test.tsfails if a firm view renders the firm name as text. - Copy only in
content/themes/*.yaml/themes.yaml(page-content-yaml); ids are code keys — rename in YAMLname, never the id. - Views show only public fields: no private timing, stage or deal data, no portfolio-total
widgets, no fabricated social metrics, no letter-avatar logo fallbacks (
private-data-boundary; enforced invc-theme-management.test.ts). Counts viasummarizePortfolioStatus(portfolio-counts-single-source). First-person voice ("my portfolio"). - Chrome and public-page CSS must read
--investor-*variables, never the visitor's stored theme at render time, so server HTML hydrates identically.
Add a theme
- Add the entry to
investorThemes(id,...THEME_TEXT.<id>, emoji, six colors). - Add
name/descriptionand its gallery group (and preview words) incontent/shared/themes.yaml. - For a full-page view: write
content/themes/<id>.yaml, runnode scripts/sync-site-copy-module.mjs, createclient/src/components/views/<Name>View.tsx(acceptViewProps, import@/generated/copy/themes/<id>), register inTHEME_VIEWSinHome.tsx. - Add a gallery preview in
ThemeSwitcher.tsxif it has a distinct layout. deploy-gate→ui-before-after(hub with?theme=<id>, desktop and mobile) →ship-to-production.
Rebuild from scratch
- Palette registry + provider (
InvestorThemeContext.tsx),themeVars.ts. - Bootstrap script and themed CSS asset in
server/publicReactDocument.ts; the build writes the asset and injects the script into the hub (scripts/generate-static.ts). - Gallery (
ThemeSwitcher.tsx) +themes.yaml. - Hub views +
THEME_VIEWS, each with itscontent/themes/*.yaml. - Gate, before/after screenshots, ship.
Tests
server/vc-theme-management.test.ts, server/shared-content.test.ts (themes.yaml names exactly
the defined themes), server/public-react-document.test.ts (bootstrap/themed CSS),
server/content-in-yaml.test.ts, server/logo-assets.test.ts.