Skip to content

[WB-1896] Add ThunderBlocks theme #2501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f2e5b0f
[global-theming] Add build step in wb-tokens to generate and export s…
Feb 26, 2025
31975a2
[global-theming] docs(changeset): Use color-mix() for active backgrou…
Feb 26, 2025
34f0353
[global-theming] docs(changeset): Add ThemeSwitcher component to chan…
Feb 26, 2025
cc34242
[global-theming] docs(changeset): Replaces `semanticColor` values wit…
Feb 26, 2025
e8c8795
[global-theming] Allow creating the css file during build time
Feb 26, 2025
d9eb346
[global-theming] Fix build script
Feb 26, 2025
7f3926f
[global-theming] Don't add theme switcher container if the theme is n…
Feb 26, 2025
76e46ba
[global-theming] docs(changeset): TextField: Fix outline-width on focus
Feb 27, 2025
c91ebed
[global-theming] TextArea: Fix outline-width
Feb 27, 2025
ce18e08
[global-theming] Fix lint issue from separate PR
Feb 27, 2025
3952a0f
[global-theming] Reorg files
Feb 27, 2025
4cd08c4
[global-theming] Fix imports
Feb 27, 2025
cb2c7bc
[global-theming] Fix test
Feb 27, 2025
0b4bd7b
[global-theming] Update pnpm-lock
Feb 27, 2025
5f147af
[global-theming] Address feedback
Feb 28, 2025
380864f
[global-theming] Fix merge conflicts
Mar 11, 2025
b580ae9
[global-theming] Remove classroom theme
Mar 11, 2025
7e39d5e
[global-theming] Remove clasroom theme from preview file
Mar 11, 2025
8c88cb7
[global-theming] Revert theming SupportedThemes change
Mar 11, 2025
b08b945
[global-theming-tb] Add ThunderBlocks theme
Mar 12, 2025
dc3aa14
[global-theming] Merge main into branch
Mar 12, 2025
dcaec45
[global-theming] Fix merge conflicts
Mar 12, 2025
c0edc20
[global-theming-tb] Merge branch 'global-theming' into global-theming-tb
Mar 12, 2025
068e8eb
[global-theming-tb] docs(changeset): Add thunderblocks theming support
Mar 12, 2025
2ede8a3
[global-theming-tb] Fix conflicts, add tertiary and neutral to TB theme
Apr 8, 2025
9c771d4
[global-theming-tb] Fix TB action tokens
Apr 8, 2025
9af0d87
[global-theming-tb] Merge branch 'feature/css-vars' into global-themi…
Apr 10, 2025
971c150
[global-theming-tb] Experiment with modes + themes
Apr 10, 2025
45b391a
[global-theming-tb] Add default theme (OG)
Apr 10, 2025
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
5 changes: 5 additions & 0 deletions .changeset/gold-peaches-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-form": patch
---

TextField: Fix outline-width on focus
5 changes: 5 additions & 0 deletions .changeset/heavy-islands-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-pill": patch
---

Use color-mix() for active background colors
6 changes: 6 additions & 0 deletions .changeset/nice-dancers-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/wonder-blocks-theming": minor
"@khanacademy/wonder-blocks-tokens": minor
---

Add thunderblocks theming support
5 changes: 5 additions & 0 deletions .changeset/silver-pumas-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-tokens": major
---

Replaces `semanticColor` values with references to css variables. Creates a build step to convert semanticColor tokens to css variables that can be consumed via CSS imports.
5 changes: 5 additions & 0 deletions .changeset/tidy-humans-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-theming": minor
---

Add ThemeSwitcher component to change themes using CSS variables
40 changes: 35 additions & 5 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import * as React from "react";
import {Preview} from "@storybook/react";
import {DocsContainer} from "@storybook/blocks";

import wonderBlocksTheme from "./wonder-blocks-theme";
import {Decorator} from "@storybook/react";
import {semanticColor} from "@khanacademy/wonder-blocks-tokens";
import Link from "@khanacademy/wonder-blocks-link";
import {ThemeSwitcherContext} from "@khanacademy/wonder-blocks-theming";
import {
ThemeSwitcherContext,
ThemeSwitcher,
} from "@khanacademy/wonder-blocks-theming";
import {RenderStateRoot} from "../packages/wonder-blocks-core/src";
import {Preview} from "@storybook/react";

// Import the Wonder Blocks CSS variables
// NOTE: External consumers should import the CSS variables from the
// wonder-blocks-tokens package directly.
// e.g. import "@khanacademy/wonder-blocks-tokens/styles.css";
import "../node_modules/@khanacademy/wonder-blocks-tokens/dist/css/index.css";

/**
* WB Official breakpoints
Expand Down Expand Up @@ -77,6 +88,16 @@ const parameters = {
},
},
docs: {
// Customize the DocsContainer to use the WB theme in MDX pages.
container: (props) => {
const theme = props.context.store.userGlobals.globals.theme;

return (
<ThemeSwitcher theme={theme}>
<DocsContainer {...props}>{props.children}</DocsContainer>
</ThemeSwitcher>
);
},
toc: {
// Useful for MDX pages like "Using color".
headingSelector: "h2, h3",
Expand All @@ -103,14 +124,18 @@ const withThemeSwitcher: Decorator = (
return (
<RenderStateRoot>
<ThemeSwitcherContext.Provider value={theme}>
<Story />
<ThemeSwitcher theme={theme}>
<Story />
</ThemeSwitcher>
</ThemeSwitcherContext.Provider>
</RenderStateRoot>
);
}
return (
<ThemeSwitcherContext.Provider value={theme}>
<Story />
<ThemeSwitcher theme={theme}>
<Story />
</ThemeSwitcher>
</ThemeSwitcherContext.Provider>
);
};
Expand All @@ -135,9 +160,14 @@ const preview: Preview = {
},
{
value: "khanmigo",
icon: "circle",
icon: "comment",
title: "Khanmigo",
},
{
value: "thunderblocks",
icon: "lightning",
title: "Thunder Blocks (Classroom)",
},
],
// Change title based on selected value
dynamicTitle: true,
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
},
"scripts": {
"alex": "alex packages/",
"build": "rollup -c ./build-settings/rollup.config.mjs",
"dev": "pnpm -r dev",
"build": "rollup -c ./build-settings/rollup.config.mjs && pnpm -r build",
"build:types": "pnpm tsc --build tsconfig-build.json && ./build-settings/check-type-definitions.ts",
"build:storybook": "storybook build --stats-json",
"clean": "rm -rf packages/wonder-blocks-*/dist && rm -rf packages/wonder-blocks-*/node_modules && rm -f packages/*/*.tsbuildinfo && rm -f utils/tsconfig.tsbuildinfo",
Expand All @@ -26,7 +27,7 @@
"publish:check": "pnpm run lint:pkg-json && ./utils/publish/pre-publish-check-ci.ts",
"publish:ci": "pnpm run publish:check && git diff --stat --exit-code HEAD && pnpm build && pnpm build:types && pnpm changeset publish",
"start": "pnpm start:storybook",
"start:storybook": "storybook dev -p 6061",
"start:storybook": "pnpm dev & storybook dev -p 6061",
"test:common": "pnpm run build && pnpm run lint && pnpm run typecheck && pnpm run alex",
"test:coverage": "pnpm run test:common && pnpm run jest --coverage",
"test:storybook": "vitest --project=storybook",
Expand All @@ -52,12 +53,16 @@
"@khanacademy/eslint-config": "^5.2.0",
"@khanacademy/eslint-plugin": "^3.1.1",
"@khanacademy/wonder-stuff-testing": "^3.0.5",
"@khanacademy/wonder-blocks-link": "workspace:*",
"@khanacademy/wonder-blocks-tokens": "workspace:*",
"@khanacademy/wonder-blocks-theming": "workspace:*",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^16.0.0",
"@storybook/addon-a11y": "^8.5.2",
"@storybook/addon-actions": "^8.5.2",
"@storybook/addon-docs": "^8.5.2",
"@storybook/addon-essentials": "^8.5.2",
"@storybook/blocks": "^8.5.2",
"@storybook/builder-vite": "^8.5.2",
"@storybook/experimental-addon-test": "^8.5.2",
"@storybook/preview-api": "^8.5.2",
Expand Down
11 changes: 6 additions & 5 deletions packages/wonder-blocks-pill/src/components/pill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {StyleSheet} from "aphrodite";
import type {StyleDeclaration} from "aphrodite";

import Clickable from "@khanacademy/wonder-blocks-clickable";
import {mix} from "@khanacademy/wonder-blocks-tokens";
import {View} from "@khanacademy/wonder-blocks-core";
import {
Body,
Expand Down Expand Up @@ -243,10 +242,12 @@ const _generateColorStyles = (clickable: boolean, kind: PillKind) => {
}

const pressColor =
kind === "transparent"
? semanticColor.status.neutral.background
: // NOTE(WB-1880): This will be simplified once we split this into Badge and Pill.
mix(tokens.color.offBlack32, backgroundColor);
kind === "transparent" || kind === "neutral"
? tokens.color.offBlack16
: kind === "accent"
? tokens.color.activeBlue
: // NOTE(WB-1880): This will be simplified once we split this into Badge and Pill.
`color-mix(in srgb, ${tokens.color.offBlack32}, ${backgroundColor})`;

const textColor =
kind === "accent"
Expand Down
28 changes: 28 additions & 0 deletions packages/wonder-blocks-theming/src/components/theme-switcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from "react";

import {SupportedThemes} from "../types";

type Props = {
/**
* The theme to use.
*/
theme: SupportedThemes;
/**
* The children where the theme will be applied.
*/
children: React.ReactNode;
};

/**
* ThemeSwitcher is a component that allows users to switch between themes.
*/
export function ThemeSwitcher({theme, children}: Props) {
// If no theme is provided, return the children as is
if (!theme) {
return children;
}

// Attach the CSS variables to a local scope so that they only work within
// this component
return <div className={`wb-theme-${theme}`}>{children}</div>;
}
1 change: 1 addition & 0 deletions packages/wonder-blocks-theming/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export {
export {type ThemedStylesFn, type SupportedThemes, type Themes} from "./types";
export {default as useStyles} from "./hooks/use-styles";
export {ThemeSwitcherContext} from "./utils/theme-switcher-context";
export {ThemeSwitcher} from "./components/theme-switcher";
2 changes: 1 addition & 1 deletion packages/wonder-blocks-theming/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import {StyleDeclaration} from "aphrodite";

export type ThemedStylesFn<T extends object> = (theme: T) => StyleDeclaration;

export type SupportedThemes = "default" | "khanmigo" | "dark";
export type SupportedThemes = "default" | "khanmigo" | "dark" | "thunderblocks";
export type Themes<T extends object> = Partial<Record<SupportedThemes, T>>;
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as tokens from "@khanacademy/wonder-blocks-tokens";
import {mergeTheme} from "../merge-theme";

describe("mergeTheme", () => {
Expand Down Expand Up @@ -69,7 +68,16 @@ describe("mergeTheme", () => {

it("should override the global tokens", () => {
// Arrange
const themeDefault = tokens;
const themeDefault = {
color: {
blue: "#0000f0",
green: "#00ff00",
},
spacing: {
medium: 8,
large: 16,
},
};

// Only override the blue color
const themeOverride = {
Expand Down
1 change: 0 additions & 1 deletion packages/wonder-blocks-theming/tsconfig-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@
},
"references": [
{"path": "../wonder-blocks-core/tsconfig-build.json"},
{"path": "../wonder-blocks-tokens/tsconfig-build.json"},
]
}
13 changes: 12 additions & 1 deletion packages/wonder-blocks-tokens/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@
"name": "@khanacademy/wonder-blocks-tokens",
"version": "5.1.0",
"description": "Core primitive design tokens for Web Wonder Blocks",
"exports": {
".": {
"import": "./dist/es/index.js",
"require": "./dist/index.js"
},
"./styles.css": "./dist/css/index.css"
},
"main": "dist/index.js",
"module": "dist/es/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "node -r @swc-node/register ./src/build/generate-css-variables.ts",
"dev": "node --watch -r @swc-node/register ./src/build/generate-css-variables.ts",
"test": "echo \"Error: no test specified\" && exit 1",
"prepublishOnly": "../../utils/publish/package-pre-publish-check.sh"
},
Expand All @@ -14,7 +23,9 @@
"publishConfig": {
"access": "public"
},
"dependencies": {},
"dependencies": {
"@khanacademy/wonder-blocks-theming": "workspace:*"
},
"devDependencies": {
"@khanacademy/wb-dev-build-settings": "workspace:*"
}
Expand Down
70 changes: 70 additions & 0 deletions packages/wonder-blocks-tokens/src/build/generate-css-variables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env -S node -r @swc-node/register
import fs from "fs";
import path from "path";
import ancesdir from "ancesdir";
import {generateTokens} from "../internal/generate-tokens";

const THEMES_DIR = "../theme/color";

/**
* Process all themes in the theme directory.
*
* This step will read all the theme files in the theme directory and generate
* the CSS variables for each theme.
*/
function processThemeCollection() {
return fs.readdirSync(path.resolve(__dirname, THEMES_DIR)).map((file) => {
// Remove the file extension
const filename = file.split(".")[0];
// eslint-disable-next-line @typescript-eslint/no-require-imports
const {default: themeObject} = require(`${THEMES_DIR}/${filename}`);

return {name: filename, tokens: generateTokens(themeObject)};
});
}

/**
* Generate the CSS selectors containing CSS variables for each theme.
*/
function generateCssVariablesDefinitions() {
return processThemeCollection()
.map((theme) => {
const cssVariables = Object.entries(theme.tokens)
.map(([key, value]) => `${key}: ${value};`)
.join("");

// Use the root selector for the default theme
const selector =
theme.name === "default" ? ":root" : `.wb-theme-${theme.name}`;

return `${selector} {${cssVariables}}`;
})
.join("\n");
}

/**
* Create the CSS file containing the CSS variables for each theme.
*/
function createCssFile() {
// Get the root package folder; use the CHANGELOG.md file as our root
// anchor.
const packageDir = ancesdir(__dirname, "CHANGELOG.md");

const dir = path.resolve(packageDir, "./dist/css");

if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}

// Generate the output inside dist/css/index.css
fs.writeFileSync(
path.resolve(packageDir, `${dir}/index.css`),
generateCssVariablesDefinitions(),
{flag: "w+"},
);
}

createCssFile();

// eslint-disable-next-line no-console
console.log("CSS variables generated successfully!!");
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {generateTokens} from "../generate-tokens";

describe("generateTokens", () => {
it("should generate tokens", () => {
// Arrange
const obj = {
primary: "red",
secondary: "blue",
};

// Act
const cssVars = generateTokens(obj);

// Assert
expect(cssVars).toStrictEqual({
"--wb-s-color-primary": "red",
"--wb-s-color-secondary": "blue",
});
});

it("should not generate tokens in empty objects", () => {
// Arrange
const obj = {
primary: {},
secondary: "blue",
};

// Act
const cssVars = generateTokens(obj);

// Assert
expect(cssVars).toStrictEqual({
"--wb-s-color-secondary": "blue",
});
});
});
Loading