From 383734d26c8385a5ddcee9b99f1943c5c60f5d06 Mon Sep 17 00:00:00 2001 From: Robbert Broersma Date: Sat, 28 Sep 2024 15:21:28 +0200 Subject: [PATCH 1/6] feat: theme builder --- packages/storybook/config/main.ts | 1 + packages/storybook/package.json | 5 + .../src/theme-builder/ThemeBuilder.css | 3 + .../src/theme-builder/ThemeBuilder.tsx | 174 + .../ThemeBuilderDenhaag.stories.tsx | 199 + .../ThemeBuilderUtrecht.stories.tsx | 199 + .../ThemeBuilderVoorbeeld.stories.tsx | 199 + .../storybook/src/theme-builder/examples.tsx | 358 ++ .../storybook/src/theme-builder/property.css | 4900 +++++++++++++++++ .../storybook/src/theme-builder/steps.tsx | 105 + pnpm-lock.yaml | 22 +- style-dictionary-config.js | 10 +- 12 files changed, 6169 insertions(+), 6 deletions(-) create mode 100644 packages/storybook/src/theme-builder/ThemeBuilder.css create mode 100644 packages/storybook/src/theme-builder/ThemeBuilder.tsx create mode 100644 packages/storybook/src/theme-builder/ThemeBuilderDenhaag.stories.tsx create mode 100644 packages/storybook/src/theme-builder/ThemeBuilderUtrecht.stories.tsx create mode 100644 packages/storybook/src/theme-builder/ThemeBuilderVoorbeeld.stories.tsx create mode 100644 packages/storybook/src/theme-builder/examples.tsx create mode 100644 packages/storybook/src/theme-builder/property.css create mode 100644 packages/storybook/src/theme-builder/steps.tsx diff --git a/packages/storybook/config/main.ts b/packages/storybook/config/main.ts index d4ef5a07a..b96ed097c 100644 --- a/packages/storybook/config/main.ts +++ b/packages/storybook/config/main.ts @@ -6,6 +6,7 @@ const config: StorybookConfig = { '../../voorbeeld-design-tokens/documentation/**/*.stories.{ts,tsx}', '../../../proprietary/*/documentation/{readme,color,design-tokens,typography}.mdx', '../../../proprietary/*/documentation/*.stories.ts', + '../src/theme-builder/**/*.stories.{ts,tsx}', ], framework: { name: '@storybook/react-vite', diff --git a/packages/storybook/package.json b/packages/storybook/package.json index c8fbba4c8..bd88a4124 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -28,6 +28,7 @@ "@amsterdam/design-system-css": "0.11.0", "@amsterdam/design-system-react": "0.11.0", "@amsterdam/design-system-tokens": "0.11.0", + "@bundled-es-modules/memfs": "4.9.4", "@gemeente-denhaag/design-tokens-components": "0.2.3-alpha.403", "@gemeente-rotterdam/design-tokens": "1.0.0-alpha.34", "@nl-design-system-unstable/amsterdam-design-tokens": "workspace:*", @@ -80,17 +81,21 @@ "@storybook/blocks": "8.2.7", "@storybook/react": "8.2.7", "@storybook/react-vite": "8.2.7", + "@types/lodash": "4.17.9", "@types/react": "18.3.3", + "@utrecht/component-library-react": "5.0.0", "@utrecht/components": "6.2.0", "@utrecht/design-tokens": "1.1.0", "@utrecht/icon": "1.1.0", "@utrecht/web-component-library-stencil": "1.4.0", "clsx": "2.1.1", + "lodash": "4.17.21", "npm-run-all": "4.1.5", "react": "18.3.1", "react-dom": "18.3.1", "rimraf": "6.0.1", "storybook": "8.2.7", + "style-dictionary": "4.0.1", "vite": "5.3.5" } } diff --git a/packages/storybook/src/theme-builder/ThemeBuilder.css b/packages/storybook/src/theme-builder/ThemeBuilder.css new file mode 100644 index 000000000..fbae3f481 --- /dev/null +++ b/packages/storybook/src/theme-builder/ThemeBuilder.css @@ -0,0 +1,3 @@ +.theme-builder__example { + transition: all 1000ms ease-in-out; +} diff --git a/packages/storybook/src/theme-builder/ThemeBuilder.tsx b/packages/storybook/src/theme-builder/ThemeBuilder.tsx new file mode 100644 index 000000000..663fe84f8 --- /dev/null +++ b/packages/storybook/src/theme-builder/ThemeBuilder.tsx @@ -0,0 +1,174 @@ +import StyleDictionary from 'style-dictionary'; +import memfs from '@bundled-es-modules/memfs'; +import type { PropsWithChildren } from 'react'; +import { useState, useEffect } from 'react'; +import { ThemeBuilderStepObject } from './steps'; +import { DesignTokenTree } from '@nl-design-system-unstable/theme-toolkit/dist/design-tokens'; +import { examples } from './examples'; +import { treeToArray } from '@nl-design-system-unstable/theme-toolkit/dist/ExampleTokensCSS'; +import './ThemeBuilder.css'; +import './property.css'; + +const styleDictionaryConversion = async ( + tokens, + selector, + filterFn = undefined, +): Promise<{ css: string; json: object }> => { + const { Volume } = memfs; + const vol = new Volume(); + const sd = new StyleDictionary( + { + hooks: { + filters: { + 'my-filter': filterFn, + }, + }, + tokens, + platforms: { + css: { + transforms: ['name/kebab'], + transformGroup: 'css', + files: [ + { + destination: 'variables.css', + format: 'css/variables', + options: { + selector, + outputReferences: true, + }, + filter: filterFn ? 'my-filter' : undefined, + }, + ], + }, + json: { + transforms: ['name/kebab'], + transformGroup: 'css', + files: [ + { + destination: 'variables.json', + format: 'json/flat', + filter: filterFn ? 'my-filter' : undefined, + }, + ], + }, + }, + }, + { volume: vol }, + ); + + let css = ''; + let json = {}; + const id = Math.round(Math.random() * 1000); + console.time('Style Dictionary ' + id); + await sd.buildAllPlatforms(); + console.timeEnd('Style Dictionary ' + id); + + try { + css = vol.readFileSync('/variables.css').toString('utf-8'); + json = JSON.parse(vol.readFileSync('/variables.json').toString('utf-8')); + } catch (e) { + console.error(e); + } + + return { css, json }; +}; + +export interface ThemeBuilderProps { + step: number; + steps: ThemeBuilderStepObject[]; + theme: DesignTokenTree; + basis: DesignTokenTree; + example?: keyof typeof examples; + allTokens?: boolean; +} + +export const ThemeBuilder = ({ + steps, + theme, + basis, + step, + example, + children, + allTokens, +}: PropsWithChildren) => { + const stepData = steps[step]; + const Example = examples[example]; + const Description = stepData?.description; + + let relevantTokens = steps + .slice(0, step + 1) + .reduce((arr, step) => [...arr, ...step.tokens, ...(step.commonTokens || [])], []); + + if (allTokens) { + relevantTokens = treeToArray(theme).map((token) => token.path.join('.')); + } + + const relevantTokenSet = new Set(relevantTokens); + + const [basisTokens] = useState(basis); + const [basisThemeCss, setBasisThemeCss] = useState(''); + const [brandCss, setBrandCss] = useState(''); + const [customThemeCss, setCustomThemeCss] = useState(''); + + useEffect(() => { + styleDictionaryConversion(basisTokens, '.basis-theme').then(({ css }) => { + setBasisThemeCss(css); + }, console.error); + }, [basisTokens]); + + useEffect(() => { + styleDictionaryConversion(theme, '.brand-tokens', (token) => { + return ( + ['voorbeeld', 'groningen', 'rods'].includes(token.path[0]) || + (token.path[0] === 'utrecht' && (token.path[1] === 'color' || token.path[1] === 'typography')) || + (token.path[0] === 'denhaag' && + (token.path[1] === 'color' || token.path[1] === 'typography' || token.path[1] === 'size')) + ); + }).then(({ css }) => { + setBrandCss(css); + }, console.error); + }, [theme]); + + useEffect(() => { + styleDictionaryConversion(theme, '.step-theme', (token) => { + return relevantTokenSet.has((token.path || []).join('.')); + }).then(({ css, json }) => { + console.log(json); + setCustomThemeCss(css); + }, console.error); + }, [step, theme]); + + return ( +
+
{Example && }
+

{stepData?.name || `Stap ${step}`}

+ {Description && } + + + +
+ {relevantTokens.length} tokens up until this step +
    + {relevantTokens.map((token, index) => ( +
  • + {token} +
  • + ))} +
+
+
+ Thema voor stap {step} +
{customThemeCss}
+
+
+ Brand tokens +
{brandCss}
+
+
+ Basis thema +
{basisThemeCss}
+
+ {children} +
+ ); +}; diff --git a/packages/storybook/src/theme-builder/ThemeBuilderDenhaag.stories.tsx b/packages/storybook/src/theme-builder/ThemeBuilderDenhaag.stories.tsx new file mode 100644 index 000000000..50b79ffdd --- /dev/null +++ b/packages/storybook/src/theme-builder/ThemeBuilderDenhaag.stories.tsx @@ -0,0 +1,199 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { steps } from './steps'; +import basisTheme from '@nl-design-system-unstable/basis-design-tokens/dist/tokens.json'; +import voorbeeldTheme from '@nl-design-system-unstable/voorbeeld-design-tokens/dist/tokens.json'; +import groningenTheme from '@nl-design-system-unstable/groningen-design-tokens/dist/tokens.json'; +import utrechtTheme from '@utrecht/design-tokens/dist/tokens.cjs'; +import denhaagTheme from '@gemeente-denhaag/design-tokens-components/dist/index.json'; +import rotterdamTheme from '@gemeente-rotterdam/design-tokens/dist/tokens.cjs'; +import { ThemeBuilder } from './ThemeBuilder'; +import { examples } from './examples'; + +const meta = { + title: 'Theme Builder/Den Haag', + id: 'theme-builder-denhaag', + component: ThemeBuilder, + args: { + step: 0, + steps, + basis: basisTheme, + theme: denhaagTheme, + }, + argTypes: { + example: { + control: 'select', + options: Object.keys(examples), + }, + theme: { + control: 'select', + options: ['voorbeeld', 'groningen', 'denhaag', 'rotterdam', 'utrecht'], + mapping: { + voorbeeld: voorbeeldTheme, + groningen: groningenTheme, + rotterdam: rotterdamTheme, + utrecht: utrechtTheme, + denhaag: denhaagTheme, + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Step0: Story = { + args: { + step: 0, + }, +}; + +export const Step1: Story = { + args: { + step: 1, + }, +}; + +export const Step2: Story = { + args: { + step: 2, + }, +}; + +export const Step3: Story = { + args: { + step: 3, + }, +}; + +export const Step4: Story = { + args: { + step: 4, + }, +}; + +export const Step5: Story = { + args: { + step: 5, + }, +}; + +export const Step6: Story = { + args: { + step: 6, + }, +}; + +export const Step7: Story = { + args: { + step: 7, + }, +}; + +export const Step8: Story = { + args: { + step: 8, + }, +}; + +export const Step9: Story = { + args: { + step: 9, + }, +}; + +export const Step10: Story = { + args: { + step: 10, + }, +}; + +export const Step11: Story = { + args: { + step: 11, + }, +}; + +export const Step12: Story = { + args: { + step: 12, + }, +}; + +export const Step13: Story = { + args: { + step: 13, + }, +}; + +export const Step14: Story = { + args: { + step: 14, + }, +}; + +export const Step15: Story = { + args: { + step: 15, + }, +}; + +export const Step16: Story = { + args: { + step: 16, + }, +}; + +export const Step17: Story = { + args: { + step: 17, + }, +}; + +export const Step18: Story = { + args: { + step: 18, + }, +}; + +export const Step19: Story = { + args: { + step: 19, + }, +}; + +export const Step20: Story = { + args: { + step: 20, + }, +}; + +export const Step21: Story = { + args: { + step: 21, + }, +}; + +export const Step22: Story = { + args: { + step: 22, + }, +}; + +export const Step23: Story = { + args: { + step: 23, + }, +}; + +export const Step24: Story = { + args: { + step: 24, + }, +}; + +export const Step25: Story = { + args: { + step: 25, + }, +}; diff --git a/packages/storybook/src/theme-builder/ThemeBuilderUtrecht.stories.tsx b/packages/storybook/src/theme-builder/ThemeBuilderUtrecht.stories.tsx new file mode 100644 index 000000000..bfed3638f --- /dev/null +++ b/packages/storybook/src/theme-builder/ThemeBuilderUtrecht.stories.tsx @@ -0,0 +1,199 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { steps } from './steps'; +import basisTheme from '@nl-design-system-unstable/basis-design-tokens/dist/tokens.json'; +import voorbeeldTheme from '@nl-design-system-unstable/voorbeeld-design-tokens/dist/tokens.json'; +import groningenTheme from '@nl-design-system-unstable/groningen-design-tokens/dist/tokens.json'; +import utrechtTheme from '@utrecht/design-tokens/dist/tokens.cjs'; +import denhaagTheme from '@gemeente-denhaag/design-tokens-components/dist/index.json'; +import rotterdamTheme from '@gemeente-rotterdam/design-tokens/dist/tokens.cjs'; +import { ThemeBuilder } from './ThemeBuilder'; +import { examples } from './examples'; + +const meta = { + title: 'Theme Builder/Utrecht', + id: 'theme-builder-utrecht', + component: ThemeBuilder, + args: { + step: 0, + steps, + basis: basisTheme, + theme: utrechtTheme, + }, + argTypes: { + example: { + control: 'select', + options: Object.keys(examples), + }, + theme: { + control: 'select', + options: ['voorbeeld', 'groningen', 'denhaag', 'rotterdam', 'utrecht'], + mapping: { + voorbeeld: voorbeeldTheme, + groningen: groningenTheme, + rotterdam: rotterdamTheme, + utrecht: utrechtTheme, + denhaag: denhaagTheme, + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Step0: Story = { + args: { + step: 0, + }, +}; + +export const Step1: Story = { + args: { + step: 1, + }, +}; + +export const Step2: Story = { + args: { + step: 2, + }, +}; + +export const Step3: Story = { + args: { + step: 3, + }, +}; + +export const Step4: Story = { + args: { + step: 4, + }, +}; + +export const Step5: Story = { + args: { + step: 5, + }, +}; + +export const Step6: Story = { + args: { + step: 6, + }, +}; + +export const Step7: Story = { + args: { + step: 7, + }, +}; + +export const Step8: Story = { + args: { + step: 8, + }, +}; + +export const Step9: Story = { + args: { + step: 9, + }, +}; + +export const Step10: Story = { + args: { + step: 10, + }, +}; + +export const Step11: Story = { + args: { + step: 11, + }, +}; + +export const Step12: Story = { + args: { + step: 12, + }, +}; + +export const Step13: Story = { + args: { + step: 13, + }, +}; + +export const Step14: Story = { + args: { + step: 14, + }, +}; + +export const Step15: Story = { + args: { + step: 15, + }, +}; + +export const Step16: Story = { + args: { + step: 16, + }, +}; + +export const Step17: Story = { + args: { + step: 17, + }, +}; + +export const Step18: Story = { + args: { + step: 18, + }, +}; + +export const Step19: Story = { + args: { + step: 19, + }, +}; + +export const Step20: Story = { + args: { + step: 20, + }, +}; + +export const Step21: Story = { + args: { + step: 21, + }, +}; + +export const Step22: Story = { + args: { + step: 22, + }, +}; + +export const Step23: Story = { + args: { + step: 23, + }, +}; + +export const Step24: Story = { + args: { + step: 24, + }, +}; + +export const Step25: Story = { + args: { + step: 25, + }, +}; diff --git a/packages/storybook/src/theme-builder/ThemeBuilderVoorbeeld.stories.tsx b/packages/storybook/src/theme-builder/ThemeBuilderVoorbeeld.stories.tsx new file mode 100644 index 000000000..26f329224 --- /dev/null +++ b/packages/storybook/src/theme-builder/ThemeBuilderVoorbeeld.stories.tsx @@ -0,0 +1,199 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { steps } from './steps'; +import basisTheme from '@nl-design-system-unstable/basis-design-tokens/dist/tokens.json'; +import voorbeeldTheme from '@nl-design-system-unstable/voorbeeld-design-tokens/dist/tokens.json'; +import groningenTheme from '@nl-design-system-unstable/groningen-design-tokens/dist/tokens.json'; +import utrechtTheme from '@utrecht/design-tokens/dist/tokens.cjs'; +import denhaagTheme from '@gemeente-denhaag/design-tokens-components/dist/index.json'; +import rotterdamTheme from '@gemeente-rotterdam/design-tokens/dist/tokens.cjs'; +import { ThemeBuilder } from './ThemeBuilder'; +import { examples } from './examples'; + +const meta = { + title: 'Theme Builder/Voorbeeld Thema', + id: 'theme-builder', + component: ThemeBuilder, + args: { + step: 0, + steps, + basis: basisTheme, + theme: voorbeeldTheme, + }, + argTypes: { + example: { + control: 'select', + options: Object.keys(examples), + }, + theme: { + control: 'select', + options: ['voorbeeld', 'groningen', 'denhaag', 'rotterdam', 'utrecht'], + mapping: { + voorbeeld: voorbeeldTheme, + groningen: groningenTheme, + rotterdam: rotterdamTheme, + utrecht: utrechtTheme, + denhaag: denhaagTheme, + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Step0: Story = { + args: { + step: 0, + }, +}; + +export const Step1: Story = { + args: { + step: 1, + }, +}; + +export const Step2: Story = { + args: { + step: 2, + }, +}; + +export const Step3: Story = { + args: { + step: 3, + }, +}; + +export const Step4: Story = { + args: { + step: 4, + }, +}; + +export const Step5: Story = { + args: { + step: 5, + }, +}; + +export const Step6: Story = { + args: { + step: 6, + }, +}; + +export const Step7: Story = { + args: { + step: 7, + }, +}; + +export const Step8: Story = { + args: { + step: 8, + }, +}; + +export const Step9: Story = { + args: { + step: 9, + }, +}; + +export const Step10: Story = { + args: { + step: 10, + }, +}; + +export const Step11: Story = { + args: { + step: 11, + }, +}; + +export const Step12: Story = { + args: { + step: 12, + }, +}; + +export const Step13: Story = { + args: { + step: 13, + }, +}; + +export const Step14: Story = { + args: { + step: 14, + }, +}; + +export const Step15: Story = { + args: { + step: 15, + }, +}; + +export const Step16: Story = { + args: { + step: 16, + }, +}; + +export const Step17: Story = { + args: { + step: 17, + }, +}; + +export const Step18: Story = { + args: { + step: 18, + }, +}; + +export const Step19: Story = { + args: { + step: 19, + }, +}; + +export const Step20: Story = { + args: { + step: 20, + }, +}; + +export const Step21: Story = { + args: { + step: 21, + }, +}; + +export const Step22: Story = { + args: { + step: 22, + }, +}; + +export const Step23: Story = { + args: { + step: 23, + }, +}; + +export const Step24: Story = { + args: { + step: 24, + }, +}; + +export const Step25: Story = { + args: { + step: 25, + }, +}; diff --git a/packages/storybook/src/theme-builder/examples.tsx b/packages/storybook/src/theme-builder/examples.tsx new file mode 100644 index 000000000..08098fed2 --- /dev/null +++ b/packages/storybook/src/theme-builder/examples.tsx @@ -0,0 +1,358 @@ +import { + AccordionProvider, + Alert, + Article, + Blockquote, + Button, + ButtonGroup, + Checkbox, + DataList, + DataListActions, + DataListItem, + DataListKey, + DataListValue, + FormField, + // FormFieldCheckboxGroup, + // FormFieldCheckboxOption, + FormFieldErrorMessage, + // FormFieldRadioOption, + // FormFieldTextarea, + FormFieldTextbox, + Heading, + Heading1, + Heading2, + Heading3, + Heading4, + Heading5, + Heading6, + // IconButton, + Image, + Link, + LinkList, + LinkListLink, + OrderedList, + OrderedListItem, + PageContent, + Paragraph, + PrimaryActionButton, + RadioButton, + Separator, + Table, + TableBody, + TableCaption, + TableCell, + TableFooter, + TableHeader, + TableHeaderCell, + TableRow, + Textarea, + Textbox, + UnorderedList, + UnorderedListItem, +} from '@utrecht/component-library-react/dist/css-module'; +import type { ReactNode } from 'react'; + +export const ButtonExample = () => { + return ( + + Click me! + + ); +}; + +export const FocusVisibleExample = () => { + return ( + <> + + + Click me! + + + + Cupiditate quo delectus in et libero.{' '} + + Adipisci et repudiandae + {' '} + quae officia earum et dolore ex. Voluptates et sunt deleniti labore praesentium rerum. + + + + + +