feat: Add multi-theme support (light/dark/legacy) with Storybook integration#24
feat: Add multi-theme support (light/dark/legacy) with Storybook integration#24claudioALEPH wants to merge 16 commits intomainfrom
Conversation
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.
Thought Process & Design DecisionsWhy Three Themes?The project has a naming history: aleph.im → twentysix → Aleph Cloud. The existing codebase had two themes ( Why ThemeProvider, Not CSS Variables?We considered migrating to CSS custom properties for instant runtime switching. However:
Why Shared Base + Variant Overrides?Each theme file is ~1000 lines. We considered Dark Theme Color ApproachRather than simply inverting the light theme, we designed the dark palette with brand consistency in mind:
Readability Fix for Lime HighlightsThe brand lime ( Storybook Performance InvestigationCold startup was ~26 seconds. Profiling revealed two bottlenecks:
We disabled What Won't BreakThe consuming app ( import { themes } from '@aleph-front/core'
<ThemeProvider theme={themes.twentysix}>This continues to work because |
foxpatch-aleph
left a comment
There was a problem hiding this comment.
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.
Summary
AlephThemeProviderwithuseThemeSwitch()hook,prefers-color-schemeauto-detection, and localStorage persistenceBackward Compatibility — Nothing Breaks
This PR is specifically designed to be non-breaking for
front-aleph-cloud-page:themes.twentysixstill works — it's a deprecated alias pointing toalephCloudLight. Any code doingimport { themes } from '@aleph-front/core'; themes.twentysixcontinues to work unchanged.themes.alephstill works — maps to the legacy theme with identical values (file moved tothemes/legacy/aleph.ts, same content).CoreThemetype is unchanged. All theme objects conform to the same interface.<ThemeProvider theme={themes.twentysix}>pattern works exactly as before. The newAlephThemeProvideris opt-in for when the app wants runtime switching.getThemeFamily()which correctly maps bothaleph-cloud-lightandaleph-cloud-darkto thealeph-cloudfamily.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 themesrc/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:#0F0E1A,#161525,#1C1B2E)#F0EBF7)#7B3FE4)#3DE64E, warning#FFC040, error#F04878)src/themes/legacy/aleph.ts— Original aleph theme preserved as-isRuntime Theme Switching (opt-in)
src/contexts/theme.tsx—AlephThemeProviderwrapping styled-componentsThemeProvider+GlobalStylesuseThemeSwitch()hook returning{ themeName, theme, setTheme, toggleDarkMode, isDark }prefers-color-schememedia query listener for OS-level dark mode detection'aleph-theme-preference'keytypeof windowguardsTheme Utilities
src/themes/utils.ts—deepMerge,composeTheme,getThemeFamily()helpersgetThemeFamily()maps theme names to'aleph'or'aleph-cloud'families for effect gatingStorybook Fixes
defaultValuewithinitialGlobalsexportreact-docgen-typescript(~7s savings on cold starts). Controls panel still works; only auto-generated prop tables in Docs tab are removed.Deleted Files
src/themes/aleph.ts→ moved tosrc/themes/legacy/aleph.tssrc/themes/twentysix.ts→ replaced bysrc/themes/aleph-cloud/light.tsMigration Path for the Consuming App
When ready to adopt runtime theme switching (not required by this PR):
Test Plan
npm run build)themes.twentysixalias resolves to Aleph Cloud Lightfront-aleph-cloud-page(no changes needed there)