diff --git a/.storybook/manager.ts b/.storybook/manager.ts index d39d83c2..9bec66b6 100644 --- a/.storybook/manager.ts +++ b/.storybook/manager.ts @@ -1,6 +1,22 @@ import { addons } from "@storybook/manager-api"; -import theme from "./theme"; +import { GLOBALS_UPDATED } from "@storybook/core-events"; +import { lightTheme, darkTheme } from "./theme"; + +const urlParams = new URLSearchParams(window.location.search); +const globalsParam = urlParams.get("globals"); +const isInitialDark = globalsParam?.includes("theme:dark-palette"); addons.setConfig({ - theme, + theme: isInitialDark ? darkTheme : lightTheme, +}); + +// Register a tiny addon to dynamically update the manager UI without requiring a reload +addons.register("fcc-theme-switcher", (api) => { + api.on(GLOBALS_UPDATED, ({ globals }) => { + if (globals && globals.theme) { + api.setOptions({ + theme: globals.theme === "dark-palette" ? darkTheme : lightTheme, + }); + } + }); }); diff --git a/.storybook/preview.css b/.storybook/preview.css new file mode 100644 index 00000000..3d14d9dd --- /dev/null +++ b/.storybook/preview.css @@ -0,0 +1,97 @@ +/* Storybook Docs Theme Overrides for freeCodeCamp UI */ + +/* Global Docs Background and Text */ +body.dark-palette { + background-color: var(--background-primary) !important; + color: var(--foreground-primary) !important; +} + +body.light-palette { + background-color: var(--background-primary) !important; + color: var(--foreground-primary) !important; +} + +body.dark-palette .sbdocs.sbdocs-wrapper { + background-color: var(--background-primary) !important; +} + +body.dark-palette .sbdocs.sbdocs-content { + color: var(--foreground-primary) !important; +} + +/* Headings */ +body.dark-palette .sbdocs.sbdocs-content h1:not(.sb-story *), +body.dark-palette .sbdocs.sbdocs-content h2:not(.sb-story *), +body.dark-palette .sbdocs.sbdocs-content h3:not(.sb-story *), +body.dark-palette .sbdocs.sbdocs-content h4:not(.sb-story *), +body.dark-palette .sbdocs.sbdocs-content h5:not(.sb-story *), +body.dark-palette .sbdocs.sbdocs-content h6:not(.sb-story *) { + color: var(--foreground-primary) !important; +} + +/* Paragraphs and Lists */ +body.dark-palette .sbdocs.sbdocs-content p:not(.sb-story *), +body.dark-palette .sbdocs.sbdocs-content li:not(.sb-story *), +body.dark-palette .sbdocs.sbdocs-content div:not(.sb-story *):not(.docs-story) { + color: var(--foreground-secondary) !important; +} + +/* Links */ +body.dark-palette .sbdocs.sbdocs-content a:not(.sb-story *) { + color: var(--foreground-info) !important; +} + +/* Inline Code */ +body.dark-palette .sbdocs.sbdocs-content code:not(.sb-story *) { + background-color: var(--background-tertiary) !important; + color: var(--foreground-tertiary) !important; + border-color: var(--background-quaternary) !important; +} + +/* Pre/Code blocks */ +body.dark-palette .sbdocs.sbdocs-content pre:not(.sb-story *) { + background-color: var(--background-tertiary) !important; + border-color: var(--background-quaternary) !important; +} + +body.dark-palette .sbdocs.sbdocs-content pre:not(.sb-story *) code { + background-color: transparent !important; + color: var(--foreground-primary) !important; + border: none !important; +} + +/* Arg/Props Table */ +body.dark-palette .docblock-argstable { + background-color: var(--background-secondary) !important; + border-color: var(--background-quaternary) !important; +} + +body.dark-palette .docblock-argstable th, +body.dark-palette .docblock-argstable td { + color: var(--foreground-primary) !important; + background-color: var(--background-secondary) !important; + border-color: var(--background-quaternary) !important; +} + +/* Arg/Props Table svgs (icons) */ +body.dark-palette .docblock-argstable svg { + color: var(--foreground-primary) !important; +} + +/* Source Code Block container */ +body.dark-palette .docblock-source { + background-color: var(--background-tertiary) !important; + border-color: var(--background-quaternary) !important; + box-shadow: none !important; +} + +/* Storybook docs story block container borders */ +body.dark-palette .docs-story { + border-color: var(--background-quaternary) !important; +} + +/* Storybook Component Preview Area */ +body .sb-show-main, +body .docs-story { + background-color: var(--background-secondary) !important; +} diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 04602d1c..5d706088 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,7 +1,12 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import type { Preview, Decorator } from "@storybook/react"; +import { DocsContainer as BaseDocsContainer } from "@storybook/blocks"; +import { themes } from "@storybook/theming"; +import { addons } from "@storybook/preview-api"; +import { GLOBALS_UPDATED } from "@storybook/core-events"; import "../src/base.css"; import "../src/fonts.css"; +import "./preview.css"; const THEME_OPTIONS = { light: { @@ -16,48 +21,58 @@ const THEME_OPTIONS = { }, } as const; +const applyThemeToBody = (theme: string) => { + const body = document.body; + Object.values(THEME_OPTIONS).forEach((t) => { + body.classList.remove(t.value); + }); + body.classList.add(theme); +}; + +// Apply theme to body globally (works for pure MDX pages without stories) +const channel = addons.getChannel(); +channel.on(GLOBALS_UPDATED, ({ globals }) => { + const theme = globals.theme || THEME_OPTIONS.light.value; + applyThemeToBody(theme); +}); + /** * Theme decorator that applies theme classes to the body and story container */ const WithThemeProvider: Decorator = (Story, context) => { const theme = context.globals.theme || THEME_OPTIONS.light.value; - const themeConfig = - Object.values(THEME_OPTIONS).find((t) => t.value === theme) || - THEME_OPTIONS.light; useEffect(() => { - const body = document.body; - - Object.values(THEME_OPTIONS).forEach((t) => { - body.classList.remove(t.value); - }); - - body.classList.add(theme); - - // Story page - const canvas = document.querySelector(".sb-show-main") as HTMLElement; + applyThemeToBody(theme); + }, [theme]); - // Docs page - const docsStories = document.querySelectorAll(".docs-story"); - - if (canvas) { - canvas.style.backgroundColor = themeConfig.backgroundColor; - } + return ; +}; - if (docsStories.length > 0) { - docsStories.forEach((el) => { - (el as HTMLElement).style.backgroundColor = themeConfig.backgroundColor; - }); - } +const DocsContainer = ( + props: React.ComponentProps, +) => { + const [isDark, setIsDark] = useState(() => + document.body.classList.contains("dark-palette"), + ); - return () => { - Object.values(THEME_OPTIONS).forEach((t) => { - body.classList.remove(t.value); - }); + useEffect(() => { + const handleGlobals = ({ + globals, + }: { + globals: Record; + }) => { + setIsDark(globals.theme === "dark-palette"); }; - }, [theme, themeConfig.backgroundColor]); + channel.on(GLOBALS_UPDATED, handleGlobals); + return () => channel.off(GLOBALS_UPDATED, handleGlobals); + }, []); - return ; + return ( + + {props.children} + + ); }; export const globalTypes = { @@ -88,6 +103,9 @@ export const globalTypes = { const preview: Preview = { parameters: { + docs: { + container: DocsContainer, + }, controls: { matchers: { color: /(background|color)$/i, diff --git a/.storybook/theme.ts b/.storybook/theme.ts index 01acee9c..3d754982 100644 --- a/.storybook/theme.ts +++ b/.storybook/theme.ts @@ -1,8 +1,20 @@ import { create } from "@storybook/theming"; -export default create({ +export const lightTheme = create({ base: "light", brandTitle: "freeCodeCamp.org", brandImage: "https://cdn.freecodecamp.org/platform/universal/fcc_secondary.svg", }); + +export const darkTheme = create({ + base: "dark", + brandTitle: "freeCodeCamp.org", + brandImage: "https://cdn.freecodecamp.org/platform/universal/fcc_primary.svg", + appBg: "#0a0a23", + appContentBg: "#0a0a23", + barBg: "#0a0a23", + colorSecondary: "#198eee", + textColor: "#ffffff", + textInverseColor: "#0a0a23", +});