| Finding | +User impact | +
|---|---|
| 60 invisible icons in dark mode | +Can't see navigation, actions, status indicators | +
| Pipeline status broken in dark mode | ++ Feature is unusable — white circles/grey lines on dark background + | +
| 4 charts unreadable in dark mode | +Analytics and usage data is useless | +
| Button wrong hover states in dark mode | +Danger looks like primary, tertiary is unreadable | +
| Toast notifications unstyled in dark mode | +Success/error feedback looks broken | +
| Checkbox/switch missing dark states | +Can't tell if controls are focused or disabled | +
| Read-only inputs invisible in dark mode | +#777 on dark background — low contrast | +
| Textarea border invisible in dark mode | +Can't see input boundaries | +
| 3 parallel theming mechanisms that don't compose | +
+ .dark SCSS selectors (48 rules, 29 files),{' '}
+ getDarkMode() runtime calls (13 components), and{' '}
+ data-bs-theme attribute (set but underused). Semantic
+ tokens unify all three into one mechanism.
+ |
+
| Finding | +User impact | +
|---|---|
| 13 different confirmation modals | ++ "Are you sure?" is a different experience everywhere + | +
| 4 dropdown implementations | +Different click behaviour, positioning, animation | +
| 5 different purple shades for the same brand colour | ++ Brand colour shifts subtly — product doesn't feel cohesive + | +
| 280+ hardcoded hex values in TSX files | ++ Not tied to any token or variable. Each one is a manual decision + that can't be updated system-wide. + | +
| Off-grid spacing (5px, 6px, 10px mixed) | +Layout feels slightly "off" | +
| Finding | +User impact | +
|---|---|
+ project-components.js globals
+ |
+ + All components bundled upfront, no tree-shaking, no code splitting + | +
+ Icon.tsx 87KB single file
+ |
+ All 70 icons parsed on every page, even if only 3 are used | +
+ @material-ui/core for 1 component
+ |
+ Heavy dependency for a single collapsible card | +
+ @ionic/react IonIcon for 3 icons
+ |
+ Entire icon library for 3 icons | +
| Finding | +User impact | +
|---|---|
| Secondary text (#656D7B) at 4.48:1 contrast ratio | ++ Just below WCAG AA threshold (4.5:1) for normal text. Fails for + some users. + | +
| No automated accessibility testing in CI | +Contrast regressions can ship undetected. No safety net. | +
| When | +What | +
|---|---|
| Feb 2026 | ++ Audit started — scanned full codebase, catalogued 85 findings + | +
| Feb–Mar 2026 | ++ 4 quick wins shipped: Icon.tsx currentColor, SuccessMessage + fontWeight, SidebarLink dark mode, Button Redux decouple + | +
| Mar 2026 | ++ Semantic tokens PR opened (#6883) — the foundation for all dark + mode work + | +
| Now | ++ Storybook + Chromatic + MCP, Banner component, SettingRow pattern, + Skeleton docs, Typography proposal + | +
+ The approach works. We're asking for support to continue, not + permission to begin. +
+| Area | +LaunchDarkly | +PostHog | +Unleash | +Flagsmith | +
|---|---|---|---|---|
| + Dialogs + | +1 Dialog.tsx | +1 LemonDialog | +1 Dialogue | +13 files → consolidating | +
| + Alerts + | +1 Alert.tsx | +1 LemonBanner | +— | +Banner component added | +
| + Dropdowns + | +1 Popover + 1 Menu | +1 LemonDropdown | +1 DropdownMenu | +4 files → planned | +
| + Icons + | +Per-file, tree-shakable | +Per-file | +Mixed | +Split planned (#7019) | +
| + Typography + | +Heading + Text | +— | +— | +Proposed (see Typography docs) | +
| + Tokens + | +Dedicated package | +Theme system | +CSS modules | +PR in progress (#6883) | +
| + Storybook + | +Full + Chromatic | +Full | +No | +Set up + Chromatic | +
+ We're early but already building the same way. The foundation is in + place — now we scale it. +
+ ++ We're NOT proposing to build a monorepo component library. + We're proposing to reach the same consolidation goals within our + existing codebase. +
++ Free tier covers 5,000 snapshots per month. That's enough for our + current scope. To keep costs minimal: +
+--only-changed flag
+ {' '}
+ — Chromatic skips snapshots for stories that haven't changed
+ between builds. Fewer snapshots consumed per run.
+ When Claude asks the MCP for our Button component, it gets:
++ No hallucinated props. No guessed variant names. Real metadata from our + actual codebase. +
+| Before | +After | +
|---|---|
| Developer searches codebase, finds 13 modal implementations | +AI reads MCP, uses the one documented component | +
| AI guesses prop names and colour values | +AI reads real props and semantic tokens from Storybook | +
| Backend engineer avoids frontend work | ++ Backend engineer describes what they need, AI generates correct + code + | +
| New component = new colour choices to make | +Tokens handle colours automatically, dark mode included | +
| PR review catches visual inconsistencies manually | +Chromatic catches visual regressions automatically | +
One command to connect Claude Code to our component library:
+
+ Already configured in our .mcp.json. New team members get
+ it automatically.
+
| Work | +Effort | +Risk | +
|---|---|---|
| + Semantic colour tokens (PR #6883) + | +In PR | +Low — additive, no visual change | +
| + Dark mode: buttons (#6892) + | +Small | +Low — CSS overrides only | +
| + Dark mode: toasts (#6893) + | +Small | +Low — CSS overrides only | +
| + Dark mode: charts (#6889) + | +Small | +Low — swap colour constants | +
| + Dark mode: checkbox/switch (#6894) + | +Small | +Low — CSS overrides only | +
| + Dark mode: account dropdown (#6902) + | +Small | +Low — CSS overrides only | +
| + Dark mode: pipeline status + | +Small | +Low — CSS overrides only | +
| + Icon.tsx refactor — split into individual files + | +Medium | +Low — same API, same visuals | +
| + project-components.js ES6 conversion (#6310) + | +Medium | +Low — replace window.* with imports | +
| + Delete legacy duplicates (#6891) + | +Small | +Low — redirect imports | +
| + Remove @material-ui/core + | +Small | +Low — replace with CSS transition | +
| + Remove @ionic/react IonIcon + | +Small | +Low — inline the 3 SVGs | +
+ Total: ~2-3 focused weeks of work, shippable incrementally. +
+ ++ Every item has a clear "done" state, can be reviewed in + isolation, doesn't change user-facing behaviour (except fixing dark + mode), and can be measured (screenshots before/after, bundle size + before/after). +
+| Direction | +What it means | +Why it matters | +
|---|---|---|
| + Consolidate modals (13 → 1) + | +Single ConfirmModal with variant prop | +Every new feature gets consistency for free | +
| + Unified Alert (6 → 1) + | +Single Alert with variant prop | +Fix dark mode once, fixed everywhere | +
| + Shared dropdown (4 → 1) + | +One composable dropdown/popover | +Same positioning, behaviour everywhere | +
| + Modal API migration + | +Replace window.openModal with React context | +Enables testing, Storybook, React 18 upgrade | +
| + Typography components + | +
+ {' |
+ Replaces 64 raw h5 tags and 49 inline font sizes | +
| + Button API cleanup + | +Rename theme → variant, text → ghost, extract IconButton | ++ Industry standard naming, cleaner API (170 usages, 113 files) + | +
| Work | +Effort | +Risk | +
|---|---|---|
| + Semantic colour tokens (PR #6883) + | +In PR | +Low — additive, no visual change | +
| + Dark mode: buttons (#6892) + | +Small | +Low — CSS overrides only | +
| + Dark mode: toasts (#6893) + | +Small | +Low — CSS overrides only | +
| + Dark mode: charts (#6889) + | +Small | +Low — swap colour constants | +
| + Dark mode: checkbox/switch (#6894) + | +Small | +Low — CSS overrides only | +
| + Dark mode: account dropdown (#6902) + | +Small | +Low — CSS overrides only | +
| + Dark mode: pipeline status + | +Small | +Low — CSS overrides only | +
| + Icon.tsx refactor — split into individual files + | +Medium | +Low — same API, same visuals | +
| + project-components.js ES6 conversion (#6310) + | +Medium | +Low — replace window.* with imports | +
| + Delete legacy duplicates (#6891) + | +Small | +Low — redirect imports | +
| + Remove @material-ui/core + | +Small | +Low — replace with CSS transition | +
| + Remove @ionic/react IonIcon + | +Small | +Low — inline the 3 SVGs | +
+ Total: ~2-3 focused weeks of work, shippable incrementally. +
+ ++ Every item has a clear "done" state, can be reviewed in + isolation, doesn't change user-facing behaviour (except fixing dark + mode), and can be measured (screenshots before/after, bundle size + before/after). +
+| Direction | +What it means | +Why it matters | +
|---|---|---|
| + Consolidate modals (13 → 1) + | +Single ConfirmModal with variant prop | +Every new feature gets consistency for free | +
| + Unified Alert (6 → 1) + | +Single Alert with variant prop | +Fix dark mode once, fixed everywhere | +
| + Shared dropdown (4 → 1) + | +One composable dropdown/popover | +Same positioning, behaviour everywhere | +
| + Modal API migration + | +Replace window.openModal with React context | +Enables testing, Storybook, React 18 upgrade | +
| + Typography components + | +
+ {' |
+ Replaces 64 raw h5 tags and 49 inline font sizes | +
| + Button API cleanup + | +Rename theme → variant, text → ghost, extract IconButton | ++ Industry standard naming, cleaner API (170 usages, 113 files) + | +
+ We're not asking the team to do this. We're asking for + support to continue. +
++ The biggest risk isn't doing the work — it's PRs sitting + in review for weeks and losing momentum. +
++ This is how the 13 modals became 13. Checking before copying + prevents the problem from growing. +
++ This work fits into normal sprint flow. We're not adding + overhead. +
+| Metric | +How we'll measure it | +
|---|---|
| + Dark mode + | ++ Zero P0 findings when re-running the audit. Before/after + screenshots. + | +
| + Performance + | ++ Bundle size before/after for Icon.tsx split and + project-components.js conversion. + | +
| + Consistency + | ++ Screenshot comparison — same pages, same flows, visible + improvement. + | +
+ The design system isn't a frontend vanity project. It's the + infrastructure that makes frontend development accessible to the whole + team — and makes AI tools generate correct, consistent code + automatically. +
++ Free tier covers 5,000 snapshots per month. That's enough for our + current scope. We only pay if we scale to hundreds of stories with + frequent pushes. +
+When Claude asks the MCP for our Button component, it gets:
++ No hallucinated props. No guessed variant names. Real metadata from our + actual codebase. +
+| Before | +After | +
|---|---|
| Developer searches codebase, finds 13 modal implementations | +AI reads MCP, uses the one documented component | +
| AI guesses prop names and colour values | +AI reads real props and semantic tokens from Storybook | +
| Backend engineer avoids frontend work | ++ Backend engineer describes what they need, AI generates correct + code + | +
| New component = new colour choices to make | +Tokens handle colours automatically, dark mode included | +
| PR review catches visual inconsistencies manually | +Chromatic catches visual regressions automatically | +
One command to connect Claude Code to our component library:
+
+ Already configured in our .mcp.json. New team members get
+ it automatically.
+
+ We're not asking the team to do this. We're asking for + support to continue. +
++ The biggest risk isn't doing the work — it's PRs sitting + in review for weeks and losing momentum. +
++ This is how the 13 modals became 13. Checking before copying + prevents the problem from growing. +
++ This work fits into normal sprint flow. We're not adding + overhead. +
+| Metric | +How we'll measure it | +
|---|---|
| + Dark mode + | ++ Zero P0 findings when re-running the audit. Before/after + screenshots. + | +
| + Performance + | ++ Bundle size before/after for Icon.tsx split and + project-components.js conversion. + | +
| + Consistency + | ++ Screenshot comparison — same pages, same flows, visible + improvement. + | +
+ The design system isn't a frontend vanity project. It's the + infrastructure that makes frontend development accessible to the whole + team — and makes AI tools generate correct, consistent code + automatically. +
++ One story file. Five automated quality benefits. Zero extra effort after + the initial 20 minutes. +
+| Activity | +Who | +When | +
|---|---|---|
| New shared component | +Whoever builds the feature — same PR | +When a pattern appears in 2+ places | +
| Story for a component | +Same developer — takes 10-20 minutes | +When building or modifying the component | +
| Token additions | +Frontend lead (Talisson) | +When a new colour role is identified | +
| Architecture decisions | +Frontend lead (Talisson) | +When consolidation or new patterns are proposed | +
| Chromatic visual review | +PR reviewer — automated in CI | +Every PR that touches components | +
_categorical.scss as CSS custom properties (
+ --color-tag-1 through --color-tag-20).
+ Currently in constants.ts pending migration. These are
+ NOT semantic tokens — they are categorical identifiers that need
+ to be visually distinct from each other.
+ >
+ }
+ >
+
+ Default tag colour: {DEFAULT_TAG_COLOUR}
+
_categorical.scss as --color-project-1{' '}
+ through --color-project-6. Currently in{' '}
+ constants.ts pending migration. Decorative — not
+ tied to any UI role or theme.
+ >
+ }
+ >
+ common/constants.ts as{' '}
+ Constants.featureHealth. These should migrate to semantic
+ feedback tokens: var(--color-success-default) and{' '}
+ var(--color-warning-default).
+ >
+ }
+ >
+ var(--color-success-default)
+ var(--color-warning-default)
+ web/styles/_primitives.scss. Add a
+ new variable to the SCSS file and it will appear here automatically.
+ >
+ }
+ >
+ {scales.map((scale) => (
+ _categorical.scss as a CSS custom property.
+ _primitives.scss.
+ _tokens.scss for
+ the UI role.
+ _primitives.scss.
+ | Category | +Token prefix | +Use for | +Examples | +
|---|---|---|---|
| + {category} + | +
+ {prefix}
+ |
+ {description} | +{examples} | +
_categorical.scss as CSS custom properties. Users pick
+ these when creating tags. Many don't map to any primitive family
+ (indigo, lime, cerise). Forcing them into scales would reduce visual
+ distinction.
+ _categorical.scss, assigned by index for project avatar
+ badges. Decorative, not semantic.
+ --color-chart-* variables to{' '}
+ _categorical.scss.
+
+ Follow the naming convention:{' '}
+ --color-{category}-{variant}
+
| Part | +Options | +Example | +
|---|---|---|
+ category
+ |
+
+ brand, surface, text,{' '}
+ border, danger, success,{' '}
+ warning, info
+ |
+
+ --color-brand-
+ |
+
+ variant
+ |
+
+ default (base), hover,{' '}
+ active, subtle (low opacity bg),{' '}
+ muted, emphasis, secondary,{' '}
+ tertiary, on-fill (text on filled bg),{' '}
+ strong
+ |
+
+ --color-brand-subtle
+ |
+
+ Open _primitives.scss and choose the right step from the
+ 50–950 scale. The pattern for light vs dark:
+
| Variant | +Light mode | +Dark mode | +Why | +
|---|---|---|---|
+ default
+ |
+ 500–600 | +400–500 | +Needs to read well on light/dark surfaces | +
+ hover
+ |
+ 600–700 (darker) | +400–600 (lighter) | +Light: darken on hover. Dark: lighten on hover. | +
+ active
+ |
+ 700–800 (darkest) | +300–400 (lightest) | +More intense than hover in both modes | +
+ subtle
+ |
+ 8% opacity of default | +Opaque dark tint | ++ Light: transparent tint. Dark: opaque to avoid stacking issues. + | +
_tokens.scss
+
+ Add the token in both :root (light) and .dark{' '}
+ blocks, under the matching category comment.
+
$red-500, never #e74856
+ directly. Primitives are the single source of truth.
+ .dark override
+ {' '}
+ — every token in :root must have a corresponding value in{' '}
+ .dark. Missing it means the light mode value shows in
+ dark mode.
+ --color-highlight. It must fit a category:{' '}
+ --color-brand-highlight or{' '}
+ --color-warning-highlight.
+ | Category | +Token prefix | +Use for | +Examples | +
|---|---|---|---|
| + {category} + | +
+ {prefix}
+ |
+ {description} | +{examples} | +
| Path | +Purpose | +
|---|---|
| documentation/ | +Storybook stories and documentation (self-contained, portable) | +
| web/components/ | +React components | +
| web/styles/ | +SCSS styles and design tokens | +
| common/ | +Shared state, services, types, and utilities | +
web/styles/_tokens.scss. Toggle the theme in the toolbar
+ to see computed values update.
+ >
+ }
+ >
+ {groups.map((group) => (
+ {size}
+ {lineHeight}
+
+ {usages} {usages === 1 ? 'usage' : 'usages'}
+
+ {name}
+ {size}
+
+ {usages}
+
+
+ h5 is used 64 times — more than all other
+ headings combined (h1: 1, h2: 6, h3: 15, h4: 10, h6: 12). Most usages
+ are not actual subsections — they are labels, setting titles, or card
+ headers that happen to need bold 18px text. This breaks semantic
+ hierarchy and hurts accessibility (screen readers use heading levels for
+ navigation).
+
+ Components set fontSize directly in style props:{' '}
+ {'style={{ fontSize: 13 }}'}. These bypass the type scale
+ and are invisible to the design system. Any scale change requires
+ finding and updating each one manually.
+
+ Weights are defined per-component in variables (
+ $btn-font-weight: 700, $input-font-weight: 500
+ ) but there are no shared tokens like $font-weight-medium{' '}
+ or $font-weight-bold. Developers guess values or copy from
+ nearby code.
+
+ Body sizes use inconsistent names: $font-sm,{' '}
+ $font-caption, $font-caption-sm,{' '}
+ $font-caption-xs. The relationship between them is unclear
+ — is $font-sm smaller than $font-caption? (No
+ — they are 14px and 13px respectively.)
+
+ The industry uses a base-4 scale: 12, 14, 16, 18, 20,
+ 24, 30, 36, 48. Our 11px and 13px values are off-grid — no design system
+ (shadcn, Radix, Atlassian, Carbon, MUI) ships these sizes. They were
+ likely eyeballed to "look right" without a system.{' '}
+ $font-caption-xs (11px) should be 12px.{' '}
+ $font-caption (13px) should be 12px or 14px depending on
+ context.
+
+ Aligned with the industry standard. All sizes are multiples of 2, + following the pattern used by shadcn, Radix, Atlassian, and Carbon. +
+| Token | +Size | +Use for | +Replaces | +
|---|---|---|---|
+ xs
+ |
+ 12px | +Badges, hints, helper text | +
+ $font-caption-xs (11px),{' '}
+ $font-caption-sm (12px)
+ |
+
+ sm
+ |
+ 14px | +Body text, labels, alerts | +
+ $font-size-base, $font-sm,{' '}
+ $font-caption (13px)
+ |
+
+ md
+ |
+ 16px | +Subheadings, emphasis text | +h6 | +
+ lg
+ |
+ 18px | +Section headings | +h5 | +
+ xl
+ |
+ 24px | +Page section titles | +h4 | +
+ 2xl
+ |
+ 30px | +Page titles | +h3 | +
+ Replaces raw h1–h6 tags. The size{' '}
+ prop controls the visual size. The as prop controls the
+ rendered HTML element (for semantic hierarchy).
+
+ Replaces '}fs-small, text-muted, inline{' '}
+ fontSize, and raw {'/
+ {''} tags with font styling.
+
+ These components don't need to be adopted all at once. The + approach: +
+{description}
+ {children} +{swatch.hex}
+ | Variant | +Shape | +Use for | +
|---|---|---|
text (default) |
+ Rectangular bar, 16px tall, 4px radius | +Text lines, labels, values | +
badge |
+ Pill shape, 12px radius | +Tags, status badges, chips | +
circle |
+ Fully round | +Avatars, icon placeholders, toggle switches | +
text — default, 16px height, 4px radius
+ badge — pill shape, 12px radius
+ circle — round, for avatars and icons
+ {label || colour}
+ {token.cssVar}
+ {token.computed}
+