Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions .storybook/manager.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
});
});
97 changes: 97 additions & 0 deletions .storybook/preview.css
Original file line number Diff line number Diff line change
@@ -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;
}
80 changes: 49 additions & 31 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -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 <Story />;
};

if (docsStories.length > 0) {
docsStories.forEach((el) => {
(el as HTMLElement).style.backgroundColor = themeConfig.backgroundColor;
});
}
const DocsContainer = (
props: React.ComponentProps<typeof BaseDocsContainer>,
) => {
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<string, unknown>;
}) => {
setIsDark(globals.theme === "dark-palette");
};
}, [theme, themeConfig.backgroundColor]);
channel.on(GLOBALS_UPDATED, handleGlobals);
return () => channel.off(GLOBALS_UPDATED, handleGlobals);
}, []);

return <Story />;
return (
<BaseDocsContainer {...props} theme={isDark ? themes.dark : themes.light}>
{props.children}
</BaseDocsContainer>
);
};

export const globalTypes = {
Expand Down Expand Up @@ -88,6 +103,9 @@ export const globalTypes = {

const preview: Preview = {
parameters: {
docs: {
container: DocsContainer,
},
controls: {
matchers: {
color: /(background|color)$/i,
Expand Down
14 changes: 13 additions & 1 deletion .storybook/theme.ts
Original file line number Diff line number Diff line change
@@ -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",
});