sahin.io

All skills and agents

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_URLS in InvestorThemeContext holds portfolio-company logos only. server/vc-theme-management.test.ts fails 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 YAML name, 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 in vc-theme-management.test.ts). Counts via summarizePortfolioStatus (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

  1. Add the entry to investorThemes (id, ...THEME_TEXT.<id>, emoji, six colors).
  2. Add name/description and its gallery group (and preview words) in content/shared/themes.yaml.
  3. For a full-page view: write content/themes/<id>.yaml, run node scripts/sync-site-copy-module.mjs, create client/src/components/views/<Name>View.tsx (accept ViewProps, import @/generated/copy/themes/<id>), register in THEME_VIEWS in Home.tsx.
  4. Add a gallery preview in ThemeSwitcher.tsx if it has a distinct layout.
  5. deploy-gate → ui-before-after (hub with ?theme=<id>, desktop and mobile) → ship-to-production.

Rebuild from scratch

  1. Palette registry + provider (InvestorThemeContext.tsx), themeVars.ts.
  2. 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).
  3. Gallery (ThemeSwitcher.tsx) + themes.yaml.
  4. Hub views + THEME_VIEWS, each with its content/themes/*.yaml.
  5. 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.