Skip to content

feat: Add multi-theme support (light/dark/legacy) with Storybook integration#24

Open
claudioALEPH wants to merge 16 commits intomainfrom
feat/multi-theme-support
Open

feat: Add multi-theme support (light/dark/legacy) with Storybook integration#24
claudioALEPH wants to merge 16 commits intomainfrom
feat/multi-theme-support

Conversation

@claudioALEPH
Copy link
Copy Markdown
Member

Summary

  • Three themes: Aleph Cloud Light (production), Aleph Cloud Dark (new), Legacy Aleph (preserved for backward compatibility)
  • Runtime switching: AlephThemeProvider with useThemeSwitch() hook, prefers-color-scheme auto-detection, and localStorage persistence
  • Storybook integration: Three-theme toolbar selector with correct Storybook 7 API
  • Startup speed: ~30% faster Storybook cold starts by disabling react-docgen-typescript
  • Dark theme: Full color palette designed for readability — deep purple-black backgrounds, warm off-white text, brightened status colors

Backward Compatibility — Nothing Breaks

This PR is specifically designed to be non-breaking for front-aleph-cloud-page:

  1. themes.twentysix still works — it's a deprecated alias pointing to alephCloudLight. Any code doing import { themes } from '@aleph-front/core'; themes.twentysix continues to work unchanged.
  2. themes.aleph still works — maps to the legacy theme with identical values (file moved to themes/legacy/aleph.ts, same content).
  3. No interface changesCoreTheme type is unchanged. All theme objects conform to the same interface.
  4. ThemeProvider unchanged — The consuming app's existing <ThemeProvider theme={themes.twentysix}> pattern works exactly as before. The new AlephThemeProvider is opt-in for when the app wants runtime switching.
  5. Global styles unchanged — Effect class gating (glow/glass/noise for legacy, grain for aleph-cloud) now uses getThemeFamily() which correctly maps both aleph-cloud-light and aleph-cloud-dark to the aleph-cloud family.
  6. All exports additive — New exports (alephCloudLight, alephCloudDark, legacyAleph, AlephThemeProvider, useThemeSwitch) are purely additive. No existing exports were removed.

What Changed

New Theme Architecture

  • src/themes/aleph-cloud/base.ts — Shared values (breakpoints, fonts, typography, transitions) extracted from the original twentysix theme
  • src/themes/aleph-cloud/light.ts — Full light theme (renamed from twentysix, name: 'aleph-cloud-light')
  • src/themes/aleph-cloud/dark.ts — Full dark theme with refined color palette:
    • Deep purple-black backgrounds (#0F0E1A, #161525, #1C1B2E)
    • Warm off-white text with purple tint (#F0EBF7)
    • Brand purple lightened for dark bg visibility (#7B3FE4)
    • Brightened status colors (success #3DE64E, warning #FFC040, error #F04878)
    • Dark text on lime active states for readability
  • src/themes/legacy/aleph.ts — Original aleph theme preserved as-is

Runtime Theme Switching (opt-in)

  • src/contexts/theme.tsxAlephThemeProvider wrapping styled-components ThemeProvider + GlobalStyles
  • useThemeSwitch() hook returning { themeName, theme, setTheme, toggleDarkMode, isDark }
  • prefers-color-scheme media query listener for OS-level dark mode detection
  • localStorage persistence with 'aleph-theme-preference' key
  • SSR-safe with typeof window guards

Theme Utilities

  • src/themes/utils.tsdeepMerge, composeTheme, getThemeFamily() helpers
  • getThemeFamily() maps theme names to 'aleph' or 'aleph-cloud' families for effect gating

Storybook Fixes

  • Theme selector: Fixed for Storybook 7 — replaced deprecated defaultValue with initialGlobals export
  • Startup speed: Disabled react-docgen-typescript (~7s savings on cold starts). Controls panel still works; only auto-generated prop tables in Docs tab are removed.
  • Dark theme contrast: Fixed unreadable lime highlights — Storybook sidebar uses brand purple; nav active states use dark text on lime backgrounds

Deleted Files

  • src/themes/aleph.ts → moved to src/themes/legacy/aleph.ts
  • src/themes/twentysix.ts → replaced by src/themes/aleph-cloud/light.ts

Migration Path for the Consuming App

When ready to adopt runtime theme switching (not required by this PR):

// Before (still works):
import { themes } from '@aleph-front/core'
<ThemeProvider theme={themes.twentysix}>

// After (opt-in):
import { AlephThemeProvider } from '@aleph-front/core'
<AlephThemeProvider defaultTheme="aleph-cloud-light">

Test Plan

  • Library builds successfully (npm run build)
  • Storybook loads with all three themes
  • Theme selector switches correctly between Light, Dark, and Legacy
  • Dark theme colors render correctly (backgrounds, text, gradients, status colors)
  • Active navigation states are readable in dark theme
  • themes.twentysix alias resolves to Aleph Cloud Light
  • No TypeScript errors introduced (pre-existing TS2322 in AccountPicker unrelated)
  • Visual review of all component stories in dark theme
  • Integration test with front-aleph-cloud-page (no changes needed there)

Covers restructuring theme system into Aleph Cloud Light, Aleph Cloud Dark,
and Legacy Aleph themes with shared base, runtime switching provider, and
backward-compatible aliases.
…heme contrast

- Fix Storybook 7 theme selector: replace deprecated `defaultValue` in
  `globalTypes` with `initialGlobals` export, add fallback in decorator
- Disable react-docgen-typescript to cut cold startup from ~26s to ~18s
  (Controls panel still works, only auto-generated prop tables are lost)
- Fix dark theme readability: change Storybook sidebar highlight from
  lime (#D4FF00) to brand purple (#7B3FE4)
- Fix dark theme nav contrast: use dark text (#080811) on lime
  backgrounds for RouterLink variant 4 and sidebar nav2 active states
Auto-fix prettier formatting across theme files, storybook config,
design stories, and context files to satisfy the ESLint pre-push hook.
@claudioALEPH
Copy link
Copy Markdown
Member Author

Thought Process & Design Decisions

Why Three Themes?

The project has a naming history: aleph.imtwentysixAleph Cloud. The existing codebase had two themes (aleph and twentysix) that were really just different brands, not light/dark variants. This PR introduces proper light/dark theming for the current Aleph Cloud brand while preserving the legacy aleph theme for backward compatibility.

Why ThemeProvider, Not CSS Variables?

We considered migrating to CSS custom properties for instant runtime switching. However:

  • The entire component library is built on styled-components + twin.macro with theme values accessed via theme.color.xxx at definition time
  • Migrating 400+ source files to CSS variables would be a massive refactoring effort with high regression risk
  • The current ThemeProvider approach works well and is already understood by the codebase
  • CSS variables can be considered for a future major version

Why Shared Base + Variant Overrides?

Each theme file is ~1000 lines. We considered deepMerge(base, variant) to avoid duplication, but component/form tokens reference colors at definition time (e.g., button.background: color.main0), so they can't live in a shared base. The base file only contains truly color-independent values: breakpoints, fonts, typography, transitions.

Dark Theme Color Approach

Rather than simply inverting the light theme, we designed the dark palette with brand consistency in mind:

  • Backgrounds use purple tints (#0F0E1A, #161525, #1C1B2E) — not pure gray — to maintain the Aleph Cloud purple brand identity
  • Text uses warm off-white (#F0EBF7) with a slight purple tint to match
  • Brand purple was lightened from #6733FF to #7B3FE4 for better visibility on dark backgrounds
  • Status colors were brightened slightly (e.g., success from #59D16E#3DE64E) since muted colors become invisible on dark backgrounds
  • Three-tier surface system (background → contentBackground → foreground) creates visual depth

Readability Fix for Lime Highlights

The brand lime (#D4FF00) is used as the active/selected state background in navigation components. On a dark theme where text defaults to light colors, this creates an unreadable combination (light text on bright lime). We fixed this by using near-black text (#080811) specifically for elements where lime is used as a background color.

Storybook Performance Investigation

Cold startup was ~26 seconds. Profiling revealed two bottlenecks:

  1. react-docgen-typescript plugin (~7s) — extracts TypeScript prop types during Webpack sealing phase
  2. Babel transpilation (~13s) — 752 modules through twin.macro + styled-components plugins

We disabled react-docgen-typescript (saving ~7s) since the auto-generated prop tables provide limited value for this team. The Controls panel continues to work fine. The Babel cost is architectural and would require migrating to Storybook's Vite builder.

What Won't Break

The consuming app (front-aleph-cloud-page) currently does:

import { themes } from '@aleph-front/core'
<ThemeProvider theme={themes.twentysix}>

This continues to work because themes.twentysix is a deprecated alias pointing to the renamed alephCloudLight theme — same values, just reorganized. The app doesn't need any changes until it's ready to adopt runtime theme switching.

Copy link
Copy Markdown

@foxpatch-aleph foxpatch-aleph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR is well-structured and the backward-compatibility story is solid. The theme architecture, effect-gating refactor, and Storybook fixes are all correct. However, there is one clear visual bug in the dark theme (footer rendered white), one semantic issue that breaks toggleDarkMode when the legacy theme is active, one exported utility that is dead code, and a subtle runtime hole in getStoredTheme where a previously-stored 'twentysix' value passes validation but is unsafely cast to ThemeName.

src/themes/aleph-cloud/dark.ts (line 952): Bug: footer.background is set to color.white (#FFFFFF), which is a direct copy from the light theme and was never updated for dark mode. The footer will render as a pure-white box on a dark page. This should be color.foreground or color.base0 (or whatever the light theme uses as its page/surface background).

src/contexts/theme.tsx (line 57): Behavioural issue: DARK_THEMES includes 'aleph' (the legacy theme), so isDark is true when on the legacy theme. That means calling toggleDarkMode() from the legacy theme silently switches to aleph-cloud-dark, permanently discarding the user's legacy selection and overwriting localStorage. toggleDarkMode is documented as 'toggle between aleph-cloud-light and aleph-cloud-dark', so the right fix is to either exclude 'aleph' from DARK_THEMES, or gate toggleDarkMode so it is a no-op when the active theme is not one of the two aleph-cloud themes.

src/contexts/theme.tsx (line 43): Subtle runtime bug: stored in themes succeeds for 'twentysix' (it is a key of the themes object), so a value previously persisted under the old theme name passes validation and is cast as ThemeName. ThemeName does not include 'twentysix', so this is an unsound cast. In practice themes['twentysix'] resolves correctly (it's an alias), but TypeScript's type system won't catch any downstream misuse. Consider either adding 'twentysix' to the validation allowlist explicitly and resolving it to its canonical name before storing, or stripping the deprecated key from themes so it can never be retrieved.

src/themes/utils.ts (line 9): Dead code: composeTheme (and the private deepMerge it calls) is exported but never used — the light and dark theme files each define their values fully standalone. Either remove it, or add a comment explaining it is reserved for future consumers who want to build custom variants. Shipping an unused exported function in a library adds to the public API surface without benefit.

src/themes/index.ts (line 13): Nit — JSDoc @deprecated on an object property: IDE deprecation warnings (the strikethrough in editors) only fire reliably when @deprecated is on an exported declaration, not on an object literal key. If surfacing a deprecation warning to consumers of themes.twentysix is important, the standard approach is a getter: get twentysix() { return alephCloudLight } with @deprecated on the accessor, or a separately exported const with @deprecated.

src/themes/aleph-cloud/dark.ts (line 739): Nit: logo.img and logo.text are still 'twentysix' — copied from the light theme file that hasn't renamed them either. If these strings are looked up as asset identifiers, leaving them as 'twentysix' in the dark theme is inconsistent. Both the light and dark aleph-cloud themes should agree on the identifier (e.g. 'aleph-cloud'), assuming the consuming app has corresponding asset names registered.

src/themes/aleph-cloud/dark.ts (line 894): Nit: navbar.mobile.content.background is color.text (#F0EBF7, warm off-white) and color is color.white. For a dark-mode mobile nav drawer this will produce a near-white panel — likely a copy-paste from the light theme. Confirm this is intentional.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants