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",
+});