From 5a998653ae0d85df18c95eb0a444bc14d1a528e5 Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Wed, 5 Mar 2025 10:54:48 -0600 Subject: [PATCH 01/10] feat(desgn-tokens): build a data-theme css file --- .changeset/brave-clouds-sneeze.md | 6 +++++ packages/paste-design-tokens/gulpfile.ts | 31 +++++++++++++++++++++++ packages/paste-design-tokens/package.json | 4 ++- yarn.lock | 9 +++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .changeset/brave-clouds-sneeze.md diff --git a/.changeset/brave-clouds-sneeze.md b/.changeset/brave-clouds-sneeze.md new file mode 100644 index 0000000000..a77d867560 --- /dev/null +++ b/.changeset/brave-clouds-sneeze.md @@ -0,0 +1,6 @@ +--- +"@twilio-paste/core": minor +"@twilio-paste/design-tokens": minor +--- + +[Design Tokens] added a new build script to generate a CSS file that applies variables for individual themes using the body[data-theme] attribute. diff --git a/packages/paste-design-tokens/gulpfile.ts b/packages/paste-design-tokens/gulpfile.ts index 84115142b5..183962195c 100644 --- a/packages/paste-design-tokens/gulpfile.ts +++ b/packages/paste-design-tokens/gulpfile.ts @@ -1,8 +1,10 @@ import * as gulp from "gulp"; import gulpif from "gulp-if"; +import gulpRename from "gulp-rename"; import terser from "gulp-terser"; import gulpTheo from "gulp-theo"; import * as theo from "theo"; +import { obj } from "through2"; import { commonTokenFormat } from "./formatters/common"; import { dTSTokenFormat } from "./formatters/d.ts"; @@ -63,6 +65,34 @@ gulp.task("tokens:css-custom-props", () => .pipe(gulp.dest(paths.dist)), ); +gulp.task("tokens:data-theme", () => + gulp + .src(paths.tokensEntry) + .pipe( + gulpTheo({ + transform: { type: "web" }, + format: { type: "custom-properties.css" }, + }), + ) + .on("error", (err: string) => { + throw new Error(err); + }) + .pipe( + obj(function (file: any, _: any, cb: any) { + if (file.isBuffer()) { + const themeName: string[] | null = file.path.match(/themes\/(.*)\//)?.[1]; + if (themeName) { + const code = file.contents.toString(); + file.contents = Buffer.from(code.replace(":root", `body[data-theme="${themeName}"]`)); + } + } + cb(null, file); + }), + ) + .pipe(gulpRename({ extname: ".data-theme.css", basename: "tokens" })) + .pipe(gulp.dest(paths.dist)), +); + gulp.task("tokens:scss", () => gulp .src(paths.tokensEntry) @@ -293,6 +323,7 @@ gulp.task( "tokens:sketchpalette", "tokens:generic:js", "tokens:generic:d:ts", + "tokens:data-theme", ), ); diff --git a/packages/paste-design-tokens/package.json b/packages/paste-design-tokens/package.json index 47eccb477f..b2ac2cb75b 100644 --- a/packages/paste-design-tokens/package.json +++ b/packages/paste-design-tokens/package.json @@ -28,8 +28,10 @@ "gulp": "^4.0.2", "gulp-cli": "^2.2.0", "gulp-if": "^3.0.0", + "gulp-rename": "^2.0.0", "gulp-terser": "^2.0.1", "gulp-theo": "2.0.1", - "lodash": "4.17.21" + "lodash": "4.17.21", + "through2": "^4.0.2" } } diff --git a/yarn.lock b/yarn.lock index e1c0b0c027..7f3a4e1339 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12760,9 +12760,11 @@ __metadata: gulp: ^4.0.2 gulp-cli: ^2.2.0 gulp-if: ^3.0.0 + gulp-rename: ^2.0.0 gulp-terser: ^2.0.1 gulp-theo: 2.0.1 lodash: 4.17.21 + through2: ^4.0.2 languageName: unknown linkType: soft @@ -28073,6 +28075,13 @@ fsevents@^1.2.7: languageName: node linkType: hard +"gulp-rename@npm:^2.0.0": + version: 2.0.0 + resolution: "gulp-rename@npm:2.0.0" + checksum: b9add0d130487dee6067206eebfc3867e4e254117edef154e8c270e3111b112335439ac1cac1519d6a32541343e04bd299484253a333b4e34c7d430039953e99 + languageName: node + linkType: hard + "gulp-terser@npm:^2.0.1": version: 2.0.1 resolution: "gulp-terser@npm:2.0.1" From 263a235749ce678c645924541a9f46d99f92a272 Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Thu, 6 Mar 2025 07:58:48 -0600 Subject: [PATCH 02/10] feat(css-theme): add the index file for theme --- packages/paste-theme/src/themes/css-variables/index.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/paste-theme/src/themes/css-variables/index.ts diff --git a/packages/paste-theme/src/themes/css-variables/index.ts b/packages/paste-theme/src/themes/css-variables/index.ts new file mode 100644 index 0000000000..e69de29bb2 From da31b8581536d9733d68d4e2f968f7f3e2d68ade Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Thu, 6 Mar 2025 07:58:58 -0600 Subject: [PATCH 03/10] feat(css-theme): add the index file for theme --- .../src/themes/css-variables/index.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/paste-theme/src/themes/css-variables/index.ts b/packages/paste-theme/src/themes/css-variables/index.ts index e69de29bb2..4610096f02 100644 --- a/packages/paste-theme/src/themes/css-variables/index.ts +++ b/packages/paste-theme/src/themes/css-variables/index.ts @@ -0,0 +1,37 @@ +import { + backgroundColors, + borderColors, + borderWidths, + boxShadows, + colorSchemes, + colors, + dataVisualization, + fontSizes, + fontWeights, + fonts, + lineHeights, + radii, + sizings, + spacings, + textColors, + zIndices, +} from "@twilio-paste/design-tokens/dist/tokens.es6"; + +export const CSSVariablesTheme = { + backgroundColors, + borderColors, + borderWidths, + radii, + fonts, + fontSizes, + fontWeights, + lineHeights, + boxShadows, + sizings, + spacings, + textColors, + zIndices, + dataVisualization, + colors, + colorSchemes, +}; From 20b1045304b2b21d8344ebacb78861f31ad701c1 Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Thu, 6 Mar 2025 13:34:22 -0600 Subject: [PATCH 04/10] feat(css-theme): added tests and working --- .../__snapshots__/themes.spec.ts.snap | 28 ++++++++++ packages/paste-theme/__tests__/themes.spec.ts | 12 ++++ .../src/generateThemeFromTokens.ts | 11 ++-- packages/paste-theme/src/themeProvider.tsx | 31 ++++++++-- .../src/themes/css-variables/index.ts | 54 ++++++++++++------ .../stories/cssVariables.stories.tsx | 56 +++++++++++++++++++ 6 files changed, 167 insertions(+), 25 deletions(-) create mode 100644 packages/paste-theme/stories/cssVariables.stories.tsx diff --git a/packages/paste-theme/__tests__/__snapshots__/themes.spec.ts.snap b/packages/paste-theme/__tests__/__snapshots__/themes.spec.ts.snap index 17787e5d6f..d1567e654b 100644 --- a/packages/paste-theme/__tests__/__snapshots__/themes.spec.ts.snap +++ b/packages/paste-theme/__tests__/__snapshots__/themes.spec.ts.snap @@ -1,5 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`CSS Theme should match the snapshot 1`] = ` +Array [ + "backgroundColors", + "borderColors", + "borderWidths", + "breakpoints", + "colorSchemes", + "dataVisualization", + "fonts", + "fontSizes", + "fontWeights", + "heights", + "iconSizes", + "lineHeights", + "maxHeights", + "maxWidths", + "minHeights", + "minWidths", + "radii", + "shadows", + "sizes", + "space", + "textColors", + "widths", + "zIndices", +] +`; + exports[`Default theme should match the snapshot 1`] = ` Array [ "backgroundColors", diff --git a/packages/paste-theme/__tests__/themes.spec.ts b/packages/paste-theme/__tests__/themes.spec.ts index 180ccb6b7c..a313ec1b7c 100644 --- a/packages/paste-theme/__tests__/themes.spec.ts +++ b/packages/paste-theme/__tests__/themes.spec.ts @@ -1,4 +1,5 @@ import { DefaultTheme, SendGridTheme } from "../src"; +import { CSSVariablesTheme } from "../src/themes/css-variables"; describe("Default theme", () => { it("should match the snapshot", () => { @@ -10,3 +11,14 @@ describe("Sendgrid theme", () => { expect(Object.keys(SendGridTheme).sort((a, b) => a.localeCompare(b))).toMatchSnapshot(); }); }); +describe("CSS Theme", () => { + it("should match the snapshot", () => { + expect(Object.keys(CSSVariablesTheme).sort((a, b) => a.localeCompare(b))).toMatchSnapshot(); + }); + it("should transform values correctly", () => { + expect(CSSVariablesTheme.space).toEqual(expect.objectContaining({ space10: "var(--space-10)" })); + expect(CSSVariablesTheme.textColors).toEqual( + expect.objectContaining({ colorTextBrand: "var(--color-text-brand)" }), + ); + }); +}); diff --git a/packages/paste-theme/src/generateThemeFromTokens.ts b/packages/paste-theme/src/generateThemeFromTokens.ts index 7b3621f315..3f044ac29f 100644 --- a/packages/paste-theme/src/generateThemeFromTokens.ts +++ b/packages/paste-theme/src/generateThemeFromTokens.ts @@ -1,3 +1,4 @@ +import { sizings as fallbackSizings } from "@twilio-paste/design-tokens/dist/themes/twilio/tokens.es6"; import type { BackgroundColorsKeys, BorderColorsKeys, @@ -19,7 +20,7 @@ import type { } from "./types/GenericThemeShape"; import { remToPx } from "./utils/remToPx"; -interface GenerateThemeFromTokensArgs { +export interface GenerateThemeFromTokensArgs { backgroundColors: Partial<{ [key in BackgroundColorsKeys]: any }>; borderColors: Partial<{ [key in BorderColorsKeys]: any }>; borderWidths: Partial<{ [key in BorderWidthsKeys]: any }>; @@ -56,11 +57,13 @@ export const generateThemeFromTokens = ({ textColors, zIndices, }: GenerateThemeFromTokensArgs): GenericThemeShape => { + // breakpoints need rm not CSS variables so need to use a fallback for the default sizings + const sizingsForBrakpoints = sizings.size0.includes("var") ? fallbackSizings : sizings; // default breakpoints const breakpoints = [ - remToPx(sizings.size40, "string"), - remToPx(sizings.size100, "string"), - remToPx(sizings.size120, "string"), + remToPx(sizingsForBrakpoints.size40, "string"), + remToPx(sizingsForBrakpoints.size100, "string"), + remToPx(sizingsForBrakpoints.size120, "string"), ]; return { diff --git a/packages/paste-theme/src/themeProvider.tsx b/packages/paste-theme/src/themeProvider.tsx index 16687744eb..6dfb853aeb 100644 --- a/packages/paste-theme/src/themeProvider.tsx +++ b/packages/paste-theme/src/themeProvider.tsx @@ -14,10 +14,13 @@ import { pasteBaseStyles } from "./styles/base"; import { pasteFonts } from "./styles/fonts"; import { pasteGlobalStyles } from "./styles/global"; import { DarkTheme, DefaultTheme, EvergreenTheme, SendGridTheme, TwilioDarkTheme, TwilioTheme } from "./themes"; +import { CSSVariablesTheme } from "./themes/css-variables"; import { getThemeFromHash } from "./utils/getThemeFromHash"; export const StyledBase = styled.div(pasteBaseStyles); +const CSSVariablesThemeKey = "CSSVariables"; + const useThemeOverwriteHook = (): string | undefined => { const [overwriteTheme, setOverwriteTheme] = React.useState(getThemeFromHash()); @@ -37,7 +40,7 @@ const useThemeOverwriteHook = (): string | undefined => { }; // eslint-disable-next-line @typescript-eslint/ban-types -function getProviderThemeProps(theme: ThemeVariants, customBreakpoints?: string[]): {} { +function getProviderThemeProps(theme: ThemeVariants | typeof CSSVariablesThemeKey, customBreakpoints?: string[]): {} { switch (theme) { case ThemeVariants.TWILIO: return { @@ -64,6 +67,11 @@ function getProviderThemeProps(theme: ThemeVariants, customBreakpoints?: string[ ...EvergreenTheme, breakpoints: customBreakpoints || EvergreenTheme.breakpoints, }; + case CSSVariablesThemeKey: + return { + ...CSSVariablesTheme, + breakpoints: customBreakpoints || CSSVariablesTheme.breakpoints, + }; case ThemeVariants.DEFAULT: default: return { @@ -73,20 +81,32 @@ function getProviderThemeProps(theme: ThemeVariants, customBreakpoints?: string[ } } -export interface ThemeProviderProps { +interface BaseThemeProviderProps { customBreakpoints?: string[]; - theme?: ThemeVariants; disableAnimations?: boolean; cacheProviderProps?: CreateCacheOptions; style?: React.CSSProperties; } +interface ThemeProviderThemeProps extends BaseThemeProviderProps { + theme?: ThemeVariants; + useCSSVariables?: never; +} + +interface ThemeProviderCSSVariablesProps extends BaseThemeProviderProps { + theme?: never; + useCSSVariables?: boolean; +} + +export type ThemeProviderProps = ThemeProviderThemeProps | ThemeProviderCSSVariablesProps; + const ThemeProvider: React.FunctionComponent> = ({ customBreakpoints, theme = ThemeVariants.DEFAULT, disableAnimations = false, // https://emotion.sh/docs/@emotion/cache#options cacheProviderProps, + useCSSVariables, ...props }) => { const [cache] = React.useState(cacheProviderProps ? createCache(cacheProviderProps) : null); @@ -98,7 +118,10 @@ const ThemeProvider: React.FunctionComponent { + const cssVariables: Partial> = {}; + + for (const [key, value] of Object.entries(tokens) as Array<[keyof GenerateThemeFromTokensArgs, string | object]>) { + if (typeof value === "object") { + cssVariables[key] = convertToCSSVariables(value); + } else { + // Convert the key to a CSS variable name colorBagroundPrimary -> --color-background-primary size10 -> --size-10 + const cssVariableName = `--${key.replace(/([A-Z])/g, "-$1").replace(/(\d+)/g, "-$1").toLowerCase()}`; + cssVariables[key] = `var(${cssVariableName})`; + } + } + + return cssVariables; }; + +export const CSSVariablesTheme = generateThemeFromTokens( + convertToCSSVariables({ + backgroundColors, + borderColors, + borderWidths, + radii, + fonts, + fontSizes, + fontWeights, + lineHeights, + boxShadows, + sizings, + spacings, + textColors, + zIndices, + dataVisualization, + colors, + colorSchemes, + }) as GenerateThemeFromTokensArgs, +); diff --git a/packages/paste-theme/stories/cssVariables.stories.tsx b/packages/paste-theme/stories/cssVariables.stories.tsx new file mode 100644 index 0000000000..d8677a5b8c --- /dev/null +++ b/packages/paste-theme/stories/cssVariables.stories.tsx @@ -0,0 +1,56 @@ +import { Box } from "@twilio-paste/box"; +import { Button } from "@twilio-paste/button"; +import "@twilio-paste/design-tokens/dist/themes/dark/tokens.data-theme.css"; +import "@twilio-paste/design-tokens/dist/themes/evergreen/tokens.data-theme.css"; +import "@twilio-paste/design-tokens/dist/themes/twilio-dark/tokens.data-theme.css"; +import "@twilio-paste/design-tokens/dist/themes/twilio/tokens.data-theme.css"; +import "@twilio-paste/design-tokens/dist/tokens.custom-properties.css"; +import { Input } from "@twilio-paste/input"; +import { Paragraph } from "@twilio-paste/paragraph"; +import { Stack } from "@twilio-paste/stack"; +import { TextArea } from "@twilio-paste/textarea"; +import * as React from "react"; + +import { Combobox } from "@twilio-paste/combobox"; +import { ThemeProvider } from "../src/themeProvider"; + +// eslint-disable-next-line import/no-default-export +export default { + title: "Theme/ThemeProvider/CSSVariables", + component: ThemeProvider, +}; + +export const StylingThemeProviderElement = (): React.ReactNode => ( + + + This theme provider uses CSS variables. You can change the theme using this combobox to switch with theme + variables should be applied. Note Storbook also ahs a theme provider wrapping the components in the view. You will + see the body color not get applied + + { + document.body.setAttribute("data-theme", value.selectedItem); + }} + /> + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Nisi porta lorem mollis aliquam ut porttitor leo. Hendrerit gravida rutrum quisque non. A arcu + cursus vitae congue mauris rhoncus aenean vel elit. Tortor dignissim convallis aenean et tortor at risus. + Vestibulum lorem sed risus ultricies. Tempor nec feugiat nisl pretium fusce id. Morbi tempus iaculis urna id + volutpat lacus laoreet non curabitur. In ante metus dictum at. Sit amet risus nullam eget felis eget nunc + lobortis. + + + + + +