├── .prettierignore ├── addon ├── .prettierrc ├── .prettierignore ├── .yarnrc.yml ├── manager.js ├── preview.js ├── .storybook │ ├── preview-head.html │ ├── preview.js │ ├── main.ts │ └── local-preset.ts ├── src │ ├── doc-blocks.ts │ ├── demo │ │ ├── more-tokens.less │ │ ├── more-tokens.scss │ │ ├── more-tokens.css │ │ ├── icons │ │ │ ├── bold.svg │ │ │ └── audio.svg │ │ ├── tokens.scss │ │ ├── tokens.less │ │ └── tokens.css │ ├── index.ts │ ├── types │ │ ├── tab.types.ts │ │ ├── config.types.ts │ │ ├── category.types.ts │ │ └── token.types.ts │ ├── constants.ts │ ├── preview.ts │ ├── stories │ │ ├── CircleColorPresenter.tsx │ │ ├── button.css │ │ ├── Button.stories.ts │ │ ├── Button.tsx │ │ └── Introduction.mdx │ ├── components │ │ ├── ClipboardButton.tsx │ │ ├── presenter │ │ │ ├── BorderPresenter.tsx │ │ │ ├── FontFamilyPresenter.tsx │ │ │ ├── EmptyPresenter.tsx │ │ │ ├── FontWeightPresenter.tsx │ │ │ ├── ColorPresenter.tsx │ │ │ ├── ShadowPresenter.tsx │ │ │ ├── ImagePresenter.tsx │ │ │ ├── SpacingPresenter.tsx │ │ │ ├── SvgPresenter.tsx │ │ │ ├── FontSizePresenter.tsx │ │ │ ├── LineHeightPresenter.tsx │ │ │ ├── LetterSpacingPresenter.tsx │ │ │ ├── AnimationPresenter.tsx │ │ │ ├── EasingPresenter.tsx │ │ │ ├── BorderRadiusPresenter.tsx │ │ │ └── OpacityPresenter.tsx │ │ ├── Input.tsx │ │ ├── ToolButton.tsx │ │ ├── TokenTab.tsx │ │ ├── Popup.tsx │ │ ├── SearchField.tsx │ │ ├── TokenPreview.tsx │ │ ├── TokenValue.tsx │ │ ├── DesignTokenDocBlock.tsx │ │ ├── TokenCards.tsx │ │ └── TokenTable.tsx │ ├── manager.ts │ ├── hooks │ │ ├── useTokenSearch.ts │ │ ├── useDebounce.ts │ │ ├── useFilteredTokens.ts │ │ ├── usePopup.ts │ │ └── useTokenTabs.ts │ ├── preset.ts │ ├── parsers │ │ ├── image.parser.ts │ │ ├── svg-icon.parser.ts │ │ └── postcss.parser.ts │ ├── Panel.tsx │ └── plugin.ts ├── .gitignore ├── vite.config.ts ├── tsup.node.config.ts ├── tsconfig.json ├── tsup.config.ts ├── LICENSE └── package.json ├── docs ├── teaser.png ├── screenshot-doc-block-1.png ├── screenshot-doc-block-2.png ├── screenshot-addon-panel-cards.png └── screenshot-addon-panel-table.png ├── .prettierrc ├── .gitignore ├── LICENCE └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /addon/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /addon/.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /addon/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /addon/manager.js: -------------------------------------------------------------------------------- 1 | export * from "./dist/manager"; 2 | -------------------------------------------------------------------------------- /addon/preview.js: -------------------------------------------------------------------------------- 1 | export * from "./dist/preview"; 2 | -------------------------------------------------------------------------------- /addon/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /docs/teaser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UX-and-I/storybook-design-token/HEAD/docs/teaser.png -------------------------------------------------------------------------------- /addon/src/doc-blocks.ts: -------------------------------------------------------------------------------- 1 | export { DesignTokenDocBlock } from "./components/DesignTokenDocBlock"; 2 | -------------------------------------------------------------------------------- /addon/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | storybook-static/ 4 | build-storybook.log 5 | .DS_Store 6 | .env -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": false, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /addon/src/demo/more-tokens.less: -------------------------------------------------------------------------------- 1 | /** 2 | * @tokens Less Colors 3 | * @presenter Color 4 | */ 5 | 6 | @brand: @b500; 7 | -------------------------------------------------------------------------------- /addon/src/demo/more-tokens.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @tokens SCSS Colors 3 | * @presenter Color 4 | */ 5 | 6 | $brand: $b500; 7 | -------------------------------------------------------------------------------- /docs/screenshot-doc-block-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UX-and-I/storybook-design-token/HEAD/docs/screenshot-doc-block-1.png -------------------------------------------------------------------------------- /docs/screenshot-doc-block-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UX-and-I/storybook-design-token/HEAD/docs/screenshot-doc-block-2.png -------------------------------------------------------------------------------- /docs/screenshot-addon-panel-cards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UX-and-I/storybook-design-token/HEAD/docs/screenshot-addon-panel-cards.png -------------------------------------------------------------------------------- /docs/screenshot-addon-panel-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UX-and-I/storybook-design-token/HEAD/docs/screenshot-addon-panel-table.png -------------------------------------------------------------------------------- /addon/src/demo/more-tokens.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /** 3 | * @tokens Colors 4 | * @presenter Color 5 | */ 6 | 7 | --test: var(--b500); 8 | } 9 | -------------------------------------------------------------------------------- /addon/src/index.ts: -------------------------------------------------------------------------------- 1 | export { DesignTokenDocBlock } from "./doc-blocks"; 2 | 3 | // make it work with --isolatedModules 4 | export default {}; 5 | -------------------------------------------------------------------------------- /addon/src/types/tab.types.ts: -------------------------------------------------------------------------------- 1 | import { Category } from "./category.types"; 2 | 3 | export interface Tab { 4 | categories: Category[]; 5 | label: string; 6 | } 7 | -------------------------------------------------------------------------------- /addon/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /addon/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export default { 2 | parameters: { 3 | tags: ["autodocs"], 4 | designToken: { 5 | disable: false, 6 | defaultTab: "Colors", 7 | showSearch: true, 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /addon/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ADDON_ID = "storybook-design-token"; 2 | export const PANEL_ID = `${ADDON_ID}/panel`; 3 | export const PARAM_KEY = `designToken`; 4 | 5 | export const EVENTS = { 6 | RESULT: `${ADDON_ID}/result`, 7 | REQUEST: `${ADDON_ID}/request`, 8 | CLEAR: `${ADDON_ID}/clear`, 9 | }; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | .eslintcache 11 | 12 | node_modules/ 13 | .pnp/ 14 | .pnp.js 15 | 16 | .cache/ 17 | .yarn/install-state.gz 18 | build/ 19 | dist/ 20 | storybook-static/ 21 | design-tokens.source.json 22 | 23 | .DS_Store -------------------------------------------------------------------------------- /addon/src/preview.ts: -------------------------------------------------------------------------------- 1 | import { ProjectAnnotations, Renderer } from "storybook/internal/types"; 2 | import { PARAM_KEY } from "./constants"; 3 | 4 | const preview: ProjectAnnotations = { 5 | decorators: [], 6 | initialGlobals: { 7 | [PARAM_KEY]: false, 8 | }, 9 | }; 10 | 11 | export default preview; 12 | -------------------------------------------------------------------------------- /addon/src/demo/icons/bold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /addon/src/types/config.types.ts: -------------------------------------------------------------------------------- 1 | import { PresenterMapType } from "../components/TokenPreview"; 2 | 3 | export interface Config { 4 | showSearch?: boolean; 5 | defaultTab?: string; 6 | styleInjection?: string; 7 | pageSize?: number; 8 | presenters?: PresenterMapType; 9 | tabs?: string[]; 10 | } 11 | 12 | export interface File { 13 | content: any; 14 | filename: string; 15 | } 16 | -------------------------------------------------------------------------------- /addon/src/stories/CircleColorPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PresenterProps } from "src/types/token.types"; 3 | 4 | export function CircleColorPresenter({ token }: PresenterProps) { 5 | return ( 6 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /addon/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 5 | addons: [ 6 | import.meta.resolve("./local-preset.ts"), 7 | "@storybook/addon-docs", 8 | ], 9 | framework: { 10 | name: "@storybook/react-vite", 11 | options: {}, 12 | }, 13 | }; 14 | export default config; 15 | -------------------------------------------------------------------------------- /addon/src/demo/icons/audio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /addon/.storybook/local-preset.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | 3 | export function previewAnnotations(entry: string[] = []) { 4 | return [...entry, fileURLToPath(import.meta.resolve("../dist/preview.js"))]; 5 | } 6 | 7 | export function managerEntries(entry: string[] = []) { 8 | return [...entry, fileURLToPath(import.meta.resolve("../dist/manager.js"))]; 9 | } 10 | 11 | export { viteFinal, webpackFinal } from "../dist/preset.js"; 12 | -------------------------------------------------------------------------------- /addon/src/types/category.types.ts: -------------------------------------------------------------------------------- 1 | import { Token, TokenPresenter } from "./token.types"; 2 | 3 | export interface Category { 4 | name: string; 5 | presenter?: TokenPresenter; 6 | range?: CategoryRange; 7 | source?: string; 8 | tokens: Token[]; 9 | } 10 | 11 | export interface CategoryRange { 12 | from: { 13 | column: number; 14 | line: number; 15 | }; 16 | to?: { 17 | column: number; 18 | line: number; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /addon/src/components/ClipboardButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ReactNode } from "react"; 3 | import { useCopyToClipboard } from "@uidotdev/usehooks"; 4 | 5 | interface ClipboardButtonProps { 6 | button: ReactNode; 7 | value: string; 8 | } 9 | 10 | export const ClipboardButton = ({ button, value }: ClipboardButtonProps) => { 11 | const [_, setCopied] = useCopyToClipboard(); 12 | 13 | return setCopied(value)}>{button}; 14 | }; 15 | -------------------------------------------------------------------------------- /addon/tsup.node.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig((options) => ({ 4 | entry: ["src/preset.ts"], 5 | splitting: true, 6 | minify: !options.watch, 7 | format: ["esm"], 8 | dts: { 9 | resolve: true, 10 | }, 11 | treeshake: true, 12 | sourcemap: true, 13 | clean: false, 14 | platform: "node", 15 | target: "node20.19", 16 | esbuildOptions(options) { 17 | options.conditions = ["module"]; 18 | }, 19 | })); 20 | -------------------------------------------------------------------------------- /addon/src/demo/tokens.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @tokens SCSS Colors 3 | * @presenter Color 4 | */ 5 | 6 | $b500: #00f; /* Token Description Example */ 7 | $g500: #0f0; 8 | $r500: #f00 !default; 9 | $function: rgba(255, 255, 0, 0.5); 10 | $sass-function: lighten($brand, 0.5); 11 | 12 | /** 13 | 14 | * @tokens SCSS Fonts 15 | * @presenter FontFamily 16 | */ 17 | 18 | $base: system-ui, sans-serif; 19 | 20 | /** 21 | * @tokens SCSS Z-Index 22 | */ 23 | 24 | $z-index: 1000; 25 | -------------------------------------------------------------------------------- /addon/src/demo/tokens.less: -------------------------------------------------------------------------------- 1 | /** 2 | * @tokens Less Colors 3 | * @presenter Color 4 | */ 5 | 6 | @b500: #0000ff; /* Token Description Example */ 7 | @g500: #00ff00; 8 | @r500: #ff0000; 9 | 10 | @function: rgba(255, 255, 0, 0.5); 11 | @less-function: lighten(@brand, 0.5); 12 | 13 | /** 14 | 15 | * @tokens Less Fonts 16 | * @presenter FontFamily 17 | */ 18 | 19 | @base: system-ui, sans-serif; 20 | 21 | /** 22 | * @tokens Less Z-Index 23 | */ 24 | 25 | @z-index: 1000; 26 | -------------------------------------------------------------------------------- /addon/src/components/presenter/BorderPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const BorderPresenter = ({ token }: PresenterProps) => { 7 | const Box = useMemo( 8 | () => 9 | styled.div(() => ({ 10 | border: token.value, 11 | height: 32, 12 | width: "100%", 13 | })), 14 | [token] 15 | ); 16 | 17 | return ; 18 | }; 19 | -------------------------------------------------------------------------------- /addon/src/components/presenter/FontFamilyPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const FontFamilyPresenter = ({ token }: PresenterProps) => { 7 | const Box = useMemo( 8 | () => 9 | styled.div(() => ({ 10 | fontFamily: token.value, 11 | width: "100%", 12 | })), 13 | [token] 14 | ); 15 | 16 | return Lorem ipsum; 17 | }; 18 | -------------------------------------------------------------------------------- /addon/src/components/presenter/EmptyPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | 5 | export const EmptyPresenter = () => { 6 | const Container = useMemo( 7 | () => 8 | styled.div(({ theme }) => ({ 9 | alignItems: "center", 10 | color: theme.color.mediumdark, 11 | display: "flex", 12 | height: 32, 13 | width: "100%", 14 | })), 15 | [] 16 | ); 17 | 18 | return No preview available.; 19 | }; 20 | -------------------------------------------------------------------------------- /addon/src/components/presenter/FontWeightPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const FontWeightPresenter = ({ token }: PresenterProps) => { 7 | const Box = useMemo( 8 | () => 9 | styled.div(() => ({ 10 | fontWeight: token.value as "bold" | "normal" | number, 11 | width: "100%", 12 | })), 13 | [token] 14 | ); 15 | 16 | return Lorem ipsum; 17 | }; 18 | -------------------------------------------------------------------------------- /addon/src/components/presenter/ColorPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const ColorPresenter = ({ token }: PresenterProps) => { 7 | const Color = useMemo( 8 | () => 9 | styled.div(() => ({ 10 | background: token.value, 11 | borderRadius: 2, 12 | height: 32, 13 | width: "100%", 14 | })), 15 | [token] 16 | ); 17 | 18 | return ; 19 | }; 20 | -------------------------------------------------------------------------------- /addon/src/components/presenter/ShadowPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const ShadowPresenter = ({ token }: PresenterProps) => { 7 | const Box = useMemo( 8 | () => 9 | styled.div(({ theme }) => ({ 10 | background: "#fff", 11 | boxShadow: token.value, 12 | height: 32, 13 | width: "100%", 14 | })), 15 | [token] 16 | ); 17 | 18 | return ; 19 | }; 20 | -------------------------------------------------------------------------------- /addon/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": ".", 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "incremental": false, 8 | "isolatedModules": true, 9 | "jsx": "react", 10 | "lib": ["esnext", "dom"], 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "noImplicitAny": true, 14 | "rootDir": ".", 15 | "skipLibCheck": true, 16 | "target": "esnext" 17 | }, 18 | "include": ["src/**/*", "tsup.config.ts", "tsup.node.config.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /addon/src/components/presenter/ImagePresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export function ImagePresenter({ token }: PresenterProps) { 7 | const Img = useMemo( 8 | () => 9 | styled.img(() => ({ 10 | maxHeight: 32, 11 | maxWidth: "100%", 12 | backgroundSize: "contain", 13 | })), 14 | [token] 15 | ); 16 | 17 | return ; 18 | } 19 | -------------------------------------------------------------------------------- /addon/src/components/presenter/SpacingPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const SpacingPresenter = ({ token }: PresenterProps) => { 7 | const Box = useMemo( 8 | () => 9 | styled.div(({ theme }) => ({ 10 | background: theme.color.secondary, 11 | borderRadius: 2, 12 | height: 32, 13 | width: token.value, 14 | })), 15 | [token] 16 | ); 17 | 18 | return ; 19 | }; 20 | -------------------------------------------------------------------------------- /addon/src/components/presenter/SvgPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const SvgPresenter = ({ token }: PresenterProps) => { 7 | const Box = useMemo( 8 | () => 9 | styled.div(() => ({ 10 | alignItems: "center", 11 | display: "flex", 12 | height: 32, 13 | width: "100%", 14 | })), 15 | [token] 16 | ); 17 | 18 | return ; 19 | }; 20 | -------------------------------------------------------------------------------- /addon/src/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons, types } from "storybook/manager-api"; 2 | import { Panel } from "./Panel"; 3 | import { ADDON_ID, PANEL_ID, PARAM_KEY } from "./constants"; 4 | import React from "react"; 5 | 6 | addons.register(ADDON_ID, () => { 7 | addons.add(PANEL_ID, { 8 | type: types.PANEL, 9 | title: "Design Tokens", 10 | paramKey: PARAM_KEY, 11 | match: ({ viewMode }) => viewMode === "story", 12 | render: ({ active }) => { 13 | if (!active) { 14 | return null; 15 | } 16 | return React.createElement(Panel, { active }); 17 | }, 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /addon/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig((options) => ({ 4 | entry: [ 5 | "src/index.ts", 6 | "src/preview.ts", 7 | "src/manager.ts", 8 | "src/doc-blocks.ts", 9 | ], 10 | splitting: true, 11 | minify: !options.watch, 12 | format: ["esm"], 13 | dts: { 14 | resolve: true, 15 | }, 16 | treeshake: true, 17 | sourcemap: true, 18 | clean: true, 19 | platform: "browser", 20 | external: ["react", "react-dom", "@storybook/icons"], 21 | esbuildOptions(options) { 22 | options.conditions = ["module"]; 23 | }, 24 | })); 25 | -------------------------------------------------------------------------------- /addon/src/components/presenter/FontSizePresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const FontSizePresenter = ({ token }: PresenterProps) => { 7 | const Box = useMemo( 8 | () => 9 | styled.div(() => ({ 10 | fontSize: token.value, 11 | height: token.value, 12 | lineHeight: 1, 13 | whiteSpace: "nowrap", 14 | width: "100%", 15 | })), 16 | [token] 17 | ); 18 | 19 | return Lorem ipsum; 20 | }; 21 | -------------------------------------------------------------------------------- /addon/src/components/presenter/LineHeightPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const LineHeightPresenter = ({ token }: PresenterProps) => { 7 | const Box = useMemo( 8 | () => 9 | styled.div(() => ({ 10 | height: "100%", 11 | lineHeight: token.value, 12 | overflow: "auto", 13 | width: "100%", 14 | })), 15 | [token] 16 | ); 17 | 18 | return ( 19 | 20 | Lorem, ipsum dolor sit amet consectetur adipisicing elit. Ipsam veniam eum 21 | dicta. 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /addon/src/components/presenter/LetterSpacingPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const LetterSpacingPresenter = ({ token }: PresenterProps) => { 7 | const Box = useMemo( 8 | () => 9 | styled.div(() => ({ 10 | height: "100%", 11 | letterSpacing: token.value, 12 | overflow: "auto", 13 | width: "100%", 14 | })), 15 | [token] 16 | ); 17 | 18 | return ( 19 | 20 | Lorem, ipsum dolor sit amet consectetur adipisicing elit. Ipsam veniam eum 21 | dicta. 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /addon/src/components/presenter/AnimationPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const AnimationPresenter = ({ token }: PresenterProps) => { 7 | const Animation = useMemo( 8 | () => 9 | styled.div(({ theme }) => ({ 10 | background: theme.color.secondary, 11 | borderRadius: 2, 12 | height: 32, 13 | width: "100%", 14 | })), 15 | [token] 16 | ); 17 | 18 | return ( 19 |
20 | 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /addon/src/hooks/useTokenSearch.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from "react"; 2 | import { Category } from "../types/category.types"; 3 | import { useDebounce } from "./useDebounce"; 4 | 5 | export function useTokenSearch(categories: Category[]) { 6 | const [searchText, setSearchText] = useState(""); 7 | const debouncedSearchText = useDebounce(searchText, 250); 8 | const resultCategories = useMemo(() => { 9 | return debouncedSearchText 10 | ? categories?.map((item) => ({ 11 | ...item, 12 | tokens: item.tokens.filter((token) => 13 | token.name.includes(debouncedSearchText) 14 | ), 15 | })) 16 | : categories; 17 | }, [debouncedSearchText, categories]); 18 | 19 | return { 20 | categories: resultCategories, 21 | searchText, 22 | setSearchText, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /addon/src/stories/button.css: -------------------------------------------------------------------------------- 1 | @import "../demo/tokens.css"; 2 | @import "../demo/more-tokens.css"; 3 | 4 | .storybook-button { 5 | font-family: "Nunito Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 6 | font-weight: 700; 7 | border: 0; 8 | border-radius: 3em; 9 | cursor: pointer; 10 | display: inline-block; 11 | line-height: 1; 12 | } 13 | .storybook-button--primary { 14 | color: white; 15 | background-color: var(--brand); 16 | } 17 | .storybook-button--secondary { 18 | color: #333; 19 | background-color: transparent; 20 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 21 | } 22 | .storybook-button--small { 23 | font-size: 12px; 24 | padding: 10px 16px; 25 | } 26 | .storybook-button--medium { 27 | font-size: 14px; 28 | padding: 11px 20px; 29 | } 30 | .storybook-button--large { 31 | font-size: 16px; 32 | padding: 12px 24px; 33 | } 34 | -------------------------------------------------------------------------------- /addon/src/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useDebounce(value: T, delay: number) { 4 | // State and setters for debounced value 5 | const [debouncedValue, setDebouncedValue] = useState(value); 6 | useEffect( 7 | () => { 8 | // Update debounced value after delay 9 | const handler = setTimeout(() => { 10 | setDebouncedValue(value); 11 | }, delay); 12 | // Cancel the timeout if value changes (also on delay change or unmount) 13 | // This is how we prevent debounced value from updating if value is changed ... 14 | // .. within the delay period. Timeout gets cleared and restarted. 15 | return () => { 16 | clearTimeout(handler); 17 | }; 18 | }, 19 | [value, delay] // Only re-call effect if value or delay changes 20 | ); 21 | return debouncedValue; 22 | } 23 | -------------------------------------------------------------------------------- /addon/src/components/presenter/EasingPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { keyframes, styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const EasingPresenter = ({ token }: PresenterProps) => { 7 | const animation = keyframes` 8 | 0% { 9 | transform: scaleX(0); 10 | } 11 | 50% { 12 | transform: scaleX(1); 13 | } 14 | 100% { 15 | transform: scaleX(0); 16 | } 17 | `; 18 | 19 | const Box = useMemo( 20 | () => 21 | styled.div(({ theme }) => ({ 22 | animation: `${animation} 2s infinite`, 23 | animationTimingFunction: token.value, 24 | background: theme.color.secondary, 25 | borderRadius: 2, 26 | height: 32, 27 | transformOrigin: "left", 28 | width: "100%", 29 | })), 30 | [token] 31 | ); 32 | 33 | return ; 34 | }; 35 | -------------------------------------------------------------------------------- /addon/src/components/presenter/BorderRadiusPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const BorderRadiusPresenter = ({ token }: PresenterProps) => { 7 | const Container = useMemo( 8 | () => 9 | styled.div(() => ({ 10 | minHeight: 32, 11 | height: 32, 12 | overflow: "auto", 13 | })), 14 | [] 15 | ); 16 | 17 | const Box = useMemo( 18 | () => 19 | styled.div(({ theme }) => ({ 20 | background: theme.color.secondary, 21 | borderRadius: token.value, 22 | minHeight: `calc(${token.value} * 2)`, 23 | minWidth: `calc(${token.value} * 2)`, 24 | overflow: "hidden", 25 | width: "100%", 26 | })), 27 | [token] 28 | ); 29 | 30 | return ( 31 | 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /addon/src/stories/Button.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react-vite"; 2 | 3 | import { Button } from "./Button"; 4 | 5 | const meta: Meta = { 6 | title: "Example/Button", 7 | component: Button, 8 | argTypes: { 9 | backgroundColor: { control: "color" }, 10 | }, 11 | parameters: { 12 | myAddonParameter: ` 13 | 14 | a.id} /> 15 | 16 | `, 17 | }, 18 | }; 19 | 20 | export default meta; 21 | type Story = StoryObj; 22 | 23 | export const Primary: Story = { 24 | // More on args: https://storybook.js.org/docs/react/writing-stories/args 25 | args: { 26 | primary: true, 27 | label: "Button", 28 | }, 29 | }; 30 | 31 | export const ColorsOnly: Story = { 32 | args: { ...Primary.args }, 33 | parameters: { 34 | designToken: { 35 | tabs: ['Colors'] 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /addon/src/components/presenter/OpacityPresenter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { PresenterProps } from "../../types/token.types"; 5 | 6 | export const OpacityPresenter = ({ token }: PresenterProps) => { 7 | const Container = useMemo( 8 | () => 9 | styled.div(() => ({ 10 | display: "flex", 11 | height: "2rem", 12 | width: "100%", 13 | })), 14 | [token] 15 | ); 16 | 17 | const Circle = useMemo( 18 | () => 19 | styled.div(() => ({ 20 | backgroundColor: "#000", 21 | borderRadius: "50%", 22 | height: "2rem", 23 | opacity: token.value, 24 | width: "2rem", 25 | 26 | "&:nth-of-type(2)": { 27 | transform: "translateX(-50%)", 28 | }, 29 | })), 30 | [token] 31 | ); 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /addon/src/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "storybook/theming"; 2 | 3 | export const Input = styled.input(({ theme }) => ({ 4 | appearance: "none", 5 | background: theme.input.background, 6 | border: "0 none", 7 | borderRadius: theme.input.borderRadius, 8 | boxShadow: `${theme.input.border} 0 0 0 1px inset`, 9 | boxSizing: "inherit", 10 | color: theme.input.color || "inherit", 11 | display: " block", 12 | fontSize: theme.typography.size.s2 - 1, 13 | lineHeight: "20px", 14 | margin: " 0", 15 | padding: "6px 10px", 16 | position: "relative", 17 | transition: "all 200ms ease-out", 18 | width: "100%", 19 | 20 | "&:focus": { 21 | boxShadow: `${theme.color.secondary} 0 0 0 1px inset`, 22 | outline: "none", 23 | }, 24 | "&[disabled]": { 25 | cursor: "not-allowed", 26 | opacity: 0.5, 27 | }, 28 | 29 | "&:-webkit-autofill": { 30 | WebkitBoxShadow: `0 0 0 3em ${theme.color.lightest} inset`, 31 | }, 32 | 33 | "::placeholder": { 34 | color: theme.color.mediumdark, 35 | }, 36 | })); 37 | -------------------------------------------------------------------------------- /addon/src/types/token.types.ts: -------------------------------------------------------------------------------- 1 | export interface Token { 2 | description?: string; 3 | isAlias?: boolean; 4 | name: string; 5 | categoryName?: string; 6 | presenter?: TokenPresenter; 7 | rawValue: string; 8 | sourceType: TokenSourceType; 9 | value: string; 10 | sourcePath: string; 11 | } 12 | 13 | export enum TokenPresenter { 14 | ANIMATION = "Animation", 15 | BORDER = "Border", 16 | BORDER_RADIUS = "BorderRadius", 17 | COLOR = "Color", 18 | EASING = "Easing", 19 | FONT_FAMILY = "FontFamily", 20 | FONT_SIZE = "FontSize", 21 | FONT_WEIGHT = "FontWeight", 22 | GRADIENT = "Gradient", 23 | LINE_HEIGHT = "LineHeight", 24 | LETTER_SPACING = "LetterSpacing", 25 | OPACITY = "Opacity", 26 | SHADOW = "Shadow", 27 | SPACING = "Spacing", 28 | SVG = "Svg", 29 | IMAGE = "Image", 30 | } 31 | 32 | export enum TokenSourceType { 33 | CSS = "CSS", 34 | LESS = "Less", 35 | SCSS = "SCSS", 36 | SVG = "SVG", 37 | THEO = "THEO", 38 | IMAGE = "IMAGE", 39 | } 40 | 41 | export interface PresenterProps { 42 | token: Token; 43 | } 44 | -------------------------------------------------------------------------------- /addon/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Storybook contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Philipp Siekmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /addon/src/hooks/useFilteredTokens.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { Token } from "../types/token.types"; 3 | import { Category } from "../types/category.types"; 4 | 5 | export const useFilteredTokens = ( 6 | categories: Category[], 7 | filterNames?: string[], 8 | theme?: string 9 | ): Token[] => { 10 | return useMemo(() => { 11 | const allTokens = categories.flatMap((category) => category.tokens); 12 | 13 | // Filter tokens by theme if passed 14 | const themeFilteredTokens = theme 15 | ? allTokens.filter((token) => { 16 | const tokenTheme = token.sourcePath.includes(theme); 17 | return tokenTheme; 18 | }) 19 | : allTokens; 20 | 21 | // Filter tokens by variable name 22 | const nameFilteredTokens = filterNames 23 | ? themeFilteredTokens.filter((token) => filterNames.includes(token.name)) 24 | : themeFilteredTokens; 25 | 26 | // Make tokens unique 27 | const uniqueTokens = nameFilteredTokens.filter( 28 | (token, index, self) => 29 | self.findIndex((t) => t.name === token.name) === index 30 | ); 31 | 32 | return uniqueTokens; 33 | }, [categories, filterNames, theme]); 34 | }; 35 | -------------------------------------------------------------------------------- /addon/src/stories/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./button.css"; 3 | 4 | interface ButtonProps { 5 | /** 6 | * Is this the principal call to action on the page? 7 | */ 8 | primary?: boolean; 9 | /** 10 | * What background color to use 11 | */ 12 | backgroundColor?: string; 13 | /** 14 | * How large should the button be? 15 | */ 16 | size?: "small" | "medium" | "large"; 17 | /** 18 | * Button contents 19 | */ 20 | label: string; 21 | /** 22 | * Optional click handler 23 | */ 24 | onClick?: () => void; 25 | } 26 | 27 | /** 28 | * Primary UI component for user interaction 29 | */ 30 | export const Button = ({ 31 | primary = false, 32 | size = "medium", 33 | backgroundColor, 34 | label, 35 | ...props 36 | }: ButtonProps) => { 37 | const mode = primary 38 | ? "storybook-button--primary" 39 | : "storybook-button--secondary"; 40 | return ( 41 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /addon/src/components/ToolButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MouseEventHandler, ReactNode, useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | 5 | interface ToolButtonProps { 6 | children: ReactNode | ReactNode[]; 7 | onClick?: MouseEventHandler; 8 | } 9 | 10 | export const ToolButton = ({ children, onClick }: ToolButtonProps) => { 11 | const Button = useMemo( 12 | () => 13 | styled.button(({ theme }) => ({ 14 | alignItems: "center", 15 | backgroundColor: "transparent", 16 | border: "none", 17 | cursor: "pointer", 18 | display: "inline-flex", 19 | height: 18, 20 | justifyContent: "center", 21 | marginLeft: 4, 22 | padding: 0, 23 | verticalAlign: "middle", 24 | width: 18, 25 | color: theme.color.defaultText, 26 | 27 | "&:hover": { 28 | color: theme.color.secondary, 29 | }, 30 | 31 | "> svg": { 32 | height: 13, 33 | position: "relative", 34 | top: -1, 35 | width: 13, 36 | }, 37 | })), 38 | [] 39 | ); 40 | 41 | return ; 42 | }; 43 | -------------------------------------------------------------------------------- /addon/src/preset.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { 3 | StorybookDesignTokenPlugin, 4 | viteStorybookDesignTokenPlugin, 5 | } from "./plugin"; 6 | 7 | type AddonOptions = { 8 | designTokenGlob?: string; 9 | presets: any; 10 | }; 11 | 12 | export function managerEntries(entry: any[] = []) { 13 | return [...entry, fileURLToPath(import.meta.resolve("./manager"))]; 14 | } 15 | 16 | export function previewAnnotations(entry: any[] = []) { 17 | return [...entry, fileURLToPath(import.meta.resolve("./preview"))]; 18 | } 19 | 20 | export const viteFinal = async ( 21 | viteConfig: Record, 22 | options: any 23 | ) => { 24 | viteConfig.plugins = viteConfig.plugins || []; 25 | viteConfig.plugins.push(viteStorybookDesignTokenPlugin(options)); 26 | 27 | return viteConfig; 28 | }; 29 | 30 | export async function webpackFinal( 31 | config: any, 32 | { designTokenGlob, presets }: AddonOptions 33 | ) { 34 | const version = await presets.apply("webpackVersion"); 35 | 36 | if (version >= 5) { 37 | config.plugins.push(new StorybookDesignTokenPlugin(designTokenGlob)); 38 | } else { 39 | throw Error( 40 | "Webpack 4 is not supported by the storybook-design-token addon." 41 | ); 42 | } 43 | 44 | return config; 45 | } 46 | -------------------------------------------------------------------------------- /addon/src/stories/Introduction.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from "@storybook/addon-docs/blocks"; 2 | import { DesignTokenDocBlock } from "../../dist/doc-blocks"; 3 | import { CircleColorPresenter } from "./CircleColorPresenter"; 4 | 5 | 6 | 7 | # Storybook Design Tokens 8 | 9 | Display design token documentation generated from your stylesheets and icon files. Preview design token changes in the browser. Add your design tokens to your Storybook Docs pages using the custom Doc Blocks. 10 | 11 | ## Colors (Table) 12 | 13 | 14 | 15 | ## Colors (Cards) 16 | 17 | 18 | 19 | ## Colors with Custom presenter (Cards) 20 | 21 | 26 | 27 | ## Colors with Custom filter 28 | 29 | 34 | 35 | ## Token usage map 36 | 37 | 45 | -------------------------------------------------------------------------------- /addon/src/components/TokenTab.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TokenCards } from "./TokenCards"; 3 | import { TokenTable } from "./TokenTable"; 4 | import { SearchField } from "./SearchField"; 5 | import { Category } from "../types/category.types"; 6 | import { useTokenSearch } from "../hooks/useTokenSearch"; 7 | import { PresenterMapType } from "./TokenPreview"; 8 | 9 | export type TokenViewType = "card" | "table"; 10 | 11 | interface TokenTabProps { 12 | categories: Category[]; 13 | viewType: TokenViewType; 14 | /** 15 | * @default true 16 | */ 17 | showSearch?: boolean; 18 | pageSize?: number; 19 | presenters?: PresenterMapType; 20 | filterNames?: string[]; 21 | } 22 | 23 | export function TokenTab({ 24 | categories: categoriesProp, 25 | viewType = "table", 26 | showSearch = true, 27 | pageSize, 28 | presenters, 29 | }: TokenTabProps) { 30 | const { searchText, setSearchText, categories } = 31 | useTokenSearch(categoriesProp); 32 | 33 | return ( 34 |
35 | {showSearch && ( 36 | 41 | )} 42 | {viewType === "card" && ( 43 | 48 | )} 49 | {viewType === "table" && ( 50 | 51 | )} 52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /addon/src/parsers/image.parser.ts: -------------------------------------------------------------------------------- 1 | import { Category } from "../types/category.types"; 2 | import { File } from "../types/config.types"; 3 | import { Token, TokenPresenter, TokenSourceType } from "../types/token.types"; 4 | import { extname, basename, relative } from "path"; 5 | import { readFileSync } from "fs"; 6 | 7 | export async function parsePngFiles( 8 | files: File[] = [] 9 | ): Promise<{ categories: Category[] }> { 10 | const tokens = determineTokens(files); 11 | 12 | let categoryNames = tokens 13 | .map((token) => token.categoryName) 14 | .filter((v, i, a) => a.indexOf(v) === i); 15 | 16 | return { 17 | categories: categoryNames.map((name) => { 18 | return { 19 | name: name || "Images", 20 | presenter: TokenPresenter.IMAGE, 21 | tokens: tokens.filter((token) => token.categoryName === name), 22 | }; 23 | }), 24 | }; 25 | } 26 | 27 | function determineTokens(files: File[]): Token[] { 28 | if (!files) { 29 | return []; 30 | } 31 | 32 | return files 33 | .map((file) => { 34 | const path = relative(process.cwd(), file.filename); 35 | return { 36 | name: basename(file.filename, extname(file.filename)), 37 | description: path, 38 | categoryName: "Images", 39 | presenter: TokenPresenter.IMAGE, 40 | rawValue: path, 41 | sourceType: TokenSourceType.IMAGE, 42 | value: toBase64(file.filename), 43 | }; 44 | }) 45 | .filter((token) => token.name); 46 | } 47 | 48 | function toBase64(filePath: string) { 49 | // read binary data 50 | const bitmap = readFileSync(filePath); 51 | // convert binary data to base64 encoded string 52 | return Buffer.from(bitmap).toString("base64"); 53 | } 54 | -------------------------------------------------------------------------------- /addon/src/components/Popup.tsx: -------------------------------------------------------------------------------- 1 | import { styled, keyframes } from "storybook/theming"; 2 | import { transparentize } from "polished"; 3 | 4 | const fadeIn = keyframes({ 5 | from: { 6 | opacity: 0, 7 | transform: "translateY(5px)", 8 | }, 9 | to: { 10 | opacity: 1, 11 | transform: "translateY(0)", 12 | }, 13 | }); 14 | 15 | export const Popup = styled.div(({ theme }) => ({ 16 | position: "absolute", 17 | background: theme.background.content, 18 | border: `1px solid ${theme.color.border}`, 19 | borderRadius: theme.borderRadius, 20 | padding: "12px 15px", 21 | boxShadow: `0 4px 12px ${transparentize(0.85, theme.color.darker)}`, 22 | color: theme.color.defaultText, 23 | fontFamily: theme.typography.fonts.base, 24 | fontSize: theme.typography.size.s2, 25 | minWidth: 150, 26 | maxWidth: 300, 27 | zIndex: 1000, 28 | animation: `${fadeIn} 0.2s ease-out`, 29 | 30 | "& > div:first-child": { 31 | fontWeight: theme.typography.weight.bold, 32 | marginBottom: 8, 33 | color: theme.color.darkest, 34 | }, 35 | 36 | "& > ul": { 37 | margin: 0, 38 | padding: 0, 39 | listStyle: "none", 40 | maxHeight: 200, 41 | overflowY: "auto", 42 | 43 | "& > li": { 44 | padding: "4px 8px", 45 | borderRadius: theme.borderRadius / 2, 46 | color: transparentize(0.1, theme.color.defaultText), 47 | background: transparentize(0.95, theme.color.medium), 48 | marginBottom: 4, 49 | transition: "background 0.2s ease", 50 | whiteSpace: "nowrap", 51 | overflow: "hidden", 52 | textOverflow: "ellipsis", 53 | 54 | "&:hover": { 55 | background: transparentize(0.85, theme.color.medium), 56 | }, 57 | 58 | "&:last-child": { 59 | marginBottom: 0, 60 | }, 61 | }, 62 | }, 63 | })); 64 | -------------------------------------------------------------------------------- /addon/src/parsers/svg-icon.parser.ts: -------------------------------------------------------------------------------- 1 | import { JSDOM } from "jsdom"; 2 | import { basename, extname } from "path"; 3 | 4 | import { Category } from "../types/category.types"; 5 | import { File } from "../types/config.types"; 6 | import { Token, TokenPresenter, TokenSourceType } from "../types/token.types"; 7 | 8 | export async function parseSvgFiles( 9 | files: File[] = [] 10 | ): Promise<{ categories: Category[] }> { 11 | const tokens = determineTokens(files); 12 | 13 | let categoryNames = tokens 14 | .map((token) => token.categoryName) 15 | .filter((v, i, a) => a.indexOf(v) === i); 16 | 17 | return { 18 | categories: categoryNames.map((name) => { 19 | return { 20 | name: name || "SVG Icons", 21 | presenter: TokenPresenter.SVG, 22 | tokens: tokens.filter((token) => token.categoryName === name), 23 | }; 24 | }), 25 | }; 26 | } 27 | 28 | function determineTokens(files: File[]): Token[] { 29 | if (!files) { 30 | return []; 31 | } 32 | 33 | const { document } = new JSDOM().window; 34 | 35 | return files 36 | .map((file) => { 37 | const div = document.createElement("div"); 38 | div.innerHTML = file.content; 39 | 40 | const svgs = Array.from(div.querySelectorAll("svg")); 41 | const name = basename(file.filename, extname(file.filename)); 42 | 43 | return svgs 44 | .map((svg, index, array) => ({ 45 | name: 46 | svg?.getAttribute("data-token-name") || 47 | svg?.getAttribute("id") || 48 | (array.length > 1 ? `${name}-${index + 1}` : name), 49 | description: svg?.getAttribute("data-token-description") || "", 50 | categoryName: svg?.getAttribute("data-token-category") || "SVG Icons", 51 | presenter: TokenPresenter.SVG, 52 | rawValue: svg.outerHTML, 53 | sourceType: TokenSourceType.SVG, 54 | value: svg.outerHTML, 55 | })) 56 | .filter((token) => token.name); 57 | }) 58 | .flat(); 59 | } 60 | -------------------------------------------------------------------------------- /addon/src/components/SearchField.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useCallback } from "react"; 3 | import { SearchIcon, CrossIcon } from "@storybook/icons"; 4 | import { styled } from "storybook/theming"; 5 | import { Input } from "./Input"; 6 | 7 | const SearchHolder = styled.div(({ theme }) => ({ 8 | display: "flex", 9 | flexDirection: "column", 10 | position: "relative", 11 | "&:focus-within svg": { 12 | color: theme.color.defaultText, 13 | }, 14 | })); 15 | 16 | const StyledSearchIcon = styled(SearchIcon)(({ theme }) => ({ 17 | width: 12, 18 | height: 12, 19 | position: "absolute", 20 | top: 10, 21 | left: 10, 22 | zIndex: 1, 23 | pointerEvents: "none", 24 | color: theme.textMutedColor, 25 | })); 26 | 27 | const ClearButton = styled.button(({ theme }) => ({ 28 | width: 16, 29 | height: 16, 30 | padding: 4, 31 | position: "absolute", 32 | top: 8, 33 | right: 8, 34 | zIndex: 1, 35 | background: "rgba(0,0,0,0.1)", 36 | border: "none", 37 | borderRadius: 16, 38 | color: theme.color.defaultText, 39 | cursor: "pointer", 40 | display: "flex", 41 | alignItems: "center", 42 | justifyContent: "center", 43 | })); 44 | 45 | const SearchInput = styled(Input)(({ theme }) => ({ 46 | paddingLeft: 28, 47 | paddingRight: 28, 48 | height: 32, 49 | })); 50 | 51 | interface SearchFieldProps { 52 | style?: React.CSSProperties; 53 | value: string; 54 | onChange: (value: string) => void; 55 | } 56 | 57 | export function SearchField({ value, onChange, style }: SearchFieldProps) { 58 | const handleChange: React.ChangeEventHandler = useCallback( 59 | (e) => { 60 | onChange(e.target.value); 61 | }, 62 | [onChange] 63 | ); 64 | 65 | const handleClear = useCallback(() => { 66 | onChange(""); 67 | }, [onChange]); 68 | 69 | return ( 70 | 71 | 72 | 77 | 78 | 79 | 80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /addon/src/Panel.tsx: -------------------------------------------------------------------------------- 1 | import { ActionBar, AddonPanel, ScrollArea, Tabs } from "storybook/internal/components"; 2 | import { useParameter } from "storybook/manager-api"; 3 | import React from "react"; 4 | import { TokenTab } from "./components/TokenTab"; 5 | import { useTokenTabs } from "./hooks/useTokenTabs"; 6 | import { Config } from "./types/config.types"; 7 | 8 | interface PanelProps { 9 | active: boolean; 10 | } 11 | 12 | export const Panel: React.FC = (props) => { 13 | const config = useParameter("designToken"); 14 | 15 | const { 16 | activeCategory, 17 | cardView, 18 | setActiveCategory, 19 | setCardView, 20 | styleInjections, 21 | tabs, 22 | } = useTokenTabs(config); 23 | 24 | // React shows a warning in the console when the count of tabs is changed because identifiers of tabs are used in the dependency array of the useMemo hook. 25 | // To prevent this, we fully re-render the Tabs control by providing a new key when tabs are changed. 26 | // https://github.com/storybookjs/storybook/blob/176017d03224f8d0b4add227ebf29a3705f994f5/code/ui/components/src/components/tabs/tabs.tsx#L151 27 | const key = (tabs ?? []).map(item => item.label).join('-'); 28 | 29 | return ( 30 | 31 | <> 32 | 33 | 34 | 35 | setActiveCategory(id) }} 38 | selected={activeCategory} 39 | > 40 | {tabs.map((tab) => { 41 | return ( 42 |
43 | {activeCategory === tab.label && ( 44 | 51 | )} 52 |
53 | ); 54 | })} 55 |
56 |
57 | 58 | { 63 | setCardView(!cardView); 64 | }, 65 | title: cardView ? "Table View" : "Card View", 66 | }, 67 | ]} 68 | /> 69 | 70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /addon/src/hooks/usePopup.ts: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useLayoutEffect, useCallback } from "react"; 2 | 3 | interface PopupState { 4 | top: number; 5 | left: number; 6 | tokenName: string; 7 | } 8 | 9 | interface UsePopupProps { 10 | usageMap?: Record; 11 | getElementRef: (item: T) => HTMLElement | null; 12 | getTokenName: (item: T) => string; 13 | } 14 | 15 | export function usePopup({ 16 | usageMap, 17 | getElementRef, 18 | getTokenName, 19 | }: UsePopupProps) { 20 | const [popup, setPopup] = useState(null); 21 | const timeoutRef = useRef(null); 22 | const popupRef = useRef(null); 23 | 24 | const handleContextMenu = useCallback( 25 | (event: React.MouseEvent, item: T) => { 26 | const tokenName = getTokenName(item); 27 | if (!usageMap) return; 28 | event.preventDefault(); 29 | 30 | const element = getElementRef(item); 31 | if (!element) return; 32 | 33 | const rect = element.getBoundingClientRect(); 34 | const top = rect.bottom + window.scrollY + 5; // Position below the element 35 | const left = rect.left + window.scrollX; 36 | 37 | setPopup({ top, left, tokenName }); 38 | if (timeoutRef.current) clearTimeout(timeoutRef.current); 39 | }, 40 | [usageMap, getElementRef, getTokenName] 41 | ); 42 | 43 | const closePopup = useCallback(() => { 44 | setPopup(null); 45 | if (timeoutRef.current) clearTimeout(timeoutRef.current); 46 | }, []); 47 | 48 | useLayoutEffect(() => { 49 | const handleClick = (event: MouseEvent) => { 50 | if ( 51 | popupRef.current && 52 | !popupRef.current.contains(event.target as Node) 53 | ) { 54 | closePopup(); 55 | } 56 | }; 57 | 58 | const handleScroll = (event: Event) => { 59 | if (!popup) return; 60 | 61 | const target = event.target as Node; 62 | if (popupRef.current && !popupRef.current.contains(target)) { 63 | if (timeoutRef.current) clearTimeout(timeoutRef.current); 64 | timeoutRef.current = window.setTimeout(closePopup, 2000); 65 | } 66 | }; 67 | 68 | document.addEventListener("click", handleClick); 69 | document.addEventListener("scroll", handleScroll, true); 70 | return () => { 71 | document.removeEventListener("click", handleClick); 72 | document.removeEventListener("scroll", handleScroll, true); 73 | }; 74 | }, [popup, closePopup]); 75 | 76 | return { 77 | popup, 78 | popupRef, 79 | handleContextMenu, 80 | closePopup, 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /addon/src/components/TokenPreview.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PresenterProps, Token, TokenPresenter } from "../types/token.types"; 3 | import { AnimationPresenter } from "./presenter/AnimationPresenter"; 4 | import { BorderPresenter } from "./presenter/BorderPresenter"; 5 | import { BorderRadiusPresenter } from "./presenter/BorderRadiusPresenter"; 6 | import { ColorPresenter } from "./presenter/ColorPresenter"; 7 | import { EasingPresenter } from "./presenter/EasingPresenter"; 8 | import { EmptyPresenter } from "./presenter/EmptyPresenter"; 9 | import { FontFamilyPresenter } from "./presenter/FontFamilyPresenter"; 10 | import { FontSizePresenter } from "./presenter/FontSizePresenter"; 11 | import { FontWeightPresenter } from "./presenter/FontWeightPresenter"; 12 | import { LineHeightPresenter } from "./presenter/LineHeightPresenter"; 13 | import { LetterSpacingPresenter } from "./presenter/LetterSpacingPresenter"; 14 | import { OpacityPresenter } from "./presenter/OpacityPresenter"; 15 | import { ShadowPresenter } from "./presenter/ShadowPresenter"; 16 | import { SpacingPresenter } from "./presenter/SpacingPresenter"; 17 | import { SvgPresenter } from "./presenter/SvgPresenter"; 18 | import { ImagePresenter } from "./presenter/ImagePresenter"; 19 | 20 | interface TokenPreviewProps { 21 | token: Token; 22 | presenters?: PresenterMapType; 23 | } 24 | 25 | export const TokenPreview = ({ token, presenters }: TokenPreviewProps) => { 26 | const presenter = token.presenter; 27 | 28 | const all = { ...PresenterMap, ...(presenters || {}) }; 29 | 30 | const PresenterComponent = 31 | presenter != null ? all[presenter] : EmptyPresenter; 32 | 33 | return ; 34 | }; 35 | 36 | export interface PresenterMapType { 37 | [key: string]: 38 | | React.FunctionComponent 39 | | React.ComponentClass; 40 | } 41 | 42 | const PresenterMap: PresenterMapType = { 43 | [`${TokenPresenter.ANIMATION}`]: AnimationPresenter, 44 | [`${TokenPresenter.BORDER}`]: BorderPresenter, 45 | [`${TokenPresenter.BORDER_RADIUS}`]: BorderRadiusPresenter, 46 | [`${TokenPresenter.COLOR}`]: ColorPresenter, 47 | [`${TokenPresenter.EASING}`]: EasingPresenter, 48 | [`${TokenPresenter.FONT_FAMILY}`]: FontFamilyPresenter, 49 | [`${TokenPresenter.FONT_SIZE}`]: FontSizePresenter, 50 | [`${TokenPresenter.FONT_WEIGHT}`]: FontWeightPresenter, 51 | [`${TokenPresenter.LINE_HEIGHT}`]: LineHeightPresenter, 52 | [`${TokenPresenter.LETTER_SPACING}`]: LetterSpacingPresenter, 53 | [`${TokenPresenter.OPACITY}`]: OpacityPresenter, 54 | [`${TokenPresenter.SHADOW}`]: ShadowPresenter, 55 | [`${TokenPresenter.SPACING}`]: SpacingPresenter, 56 | [`${TokenPresenter.SVG}`]: SvgPresenter, 57 | [`${TokenPresenter.IMAGE}`]: ImagePresenter, 58 | }; 59 | -------------------------------------------------------------------------------- /addon/src/components/TokenValue.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useEffect, useMemo, useState } from "react"; 3 | import { CloseIcon } from "@storybook/icons"; 4 | import { styled } from "storybook/theming"; 5 | import { Token, TokenSourceType } from "../types/token.types"; 6 | import { Input } from "./Input"; 7 | import { ToolButton } from "./ToolButton"; 8 | 9 | interface TokenValueProps { 10 | onValueChange: (newValue: any) => void; 11 | readonly?: boolean; 12 | token: Token; 13 | } 14 | 15 | export const TokenValue = ({ 16 | onValueChange, 17 | readonly, 18 | token, 19 | }: TokenValueProps) => { 20 | const [rawValue, setRawValue] = useState(token.rawValue); 21 | 22 | const Container = useMemo( 23 | () => 24 | styled.div(({}) => ({ 25 | position: "relative", 26 | })), 27 | [] 28 | ); 29 | 30 | const ResetButton = useMemo( 31 | () => 32 | styled.span(({}) => ({ 33 | position: "absolute", 34 | right: 8, 35 | top: "50%", 36 | transform: "translate3d(0, -50%, 0)", 37 | })), 38 | [] 39 | ); 40 | 41 | const RawValue = useMemo( 42 | () => 43 | styled.span(({}) => ({ 44 | overflow: "hidden", 45 | wordBreak: "break-all", 46 | WebkitLineClamp: 3, 47 | WebkitBoxOrient: "vertical", 48 | display: "-webkit-box", 49 | })), 50 | [] 51 | ); 52 | 53 | const showRawValue = 54 | token.sourceType !== TokenSourceType.CSS && 55 | token.sourceType !== TokenSourceType.SVG; 56 | 57 | useEffect(() => { 58 | const previewIframe: HTMLIFrameElement = document.querySelector( 59 | "#storybook-preview-iframe" 60 | ) as HTMLIFrameElement; 61 | 62 | const tokenElement = previewIframe?.contentWindow?.document.documentElement; 63 | 64 | if (tokenElement !== undefined && tokenElement !== null) { 65 | if (token.rawValue !== rawValue) { 66 | tokenElement.style.setProperty(token.name, rawValue); 67 | } else { 68 | tokenElement.style.setProperty(token.name, token.rawValue); 69 | } 70 | } 71 | }, [rawValue]); 72 | 73 | return ( 74 | 75 | {showRawValue && {rawValue}} 76 | 77 | {!showRawValue && ( 78 | { 81 | const newRawValue = (event.target as HTMLInputElement).value; 82 | 83 | setRawValue(newRawValue); 84 | onValueChange(newRawValue); 85 | }} 86 | value={rawValue} 87 | /> 88 | )} 89 | 90 | {!showRawValue && token.rawValue !== rawValue && ( 91 | 92 | { 94 | setRawValue(token.rawValue); 95 | onValueChange(token.rawValue); 96 | }} 97 | > 98 | 99 | 100 | 101 | )} 102 | 103 | ); 104 | }; 105 | -------------------------------------------------------------------------------- /addon/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-design-token", 3 | "version": "5.0.0", 4 | "description": "Storybook addon to display design token documentation generated from your stylesheets and icon files.", 5 | "type": "module", 6 | "keywords": [ 7 | "design token", 8 | "design system", 9 | "design pattern", 10 | "storybook-addon" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/UX-and-I/storybook-design-token" 15 | }, 16 | "author": "Philipp Siekmann ", 17 | "license": "MIT", 18 | "exports": { 19 | ".": { 20 | "types": "./dist/index.d.ts", 21 | "default": "./dist/index.js" 22 | }, 23 | "./manager": { 24 | "types": "./dist/manager.d.ts", 25 | "default": "./dist/manager.js" 26 | }, 27 | "./preview": { 28 | "types": "./dist/preview.d.ts", 29 | "default": "./dist/preview.js" 30 | }, 31 | "./preset": { 32 | "types": "./dist/preset.d.ts", 33 | "default": "./dist/preset.js" 34 | }, 35 | "./doc-blocks": { 36 | "types": "./dist/doc-blocks.d.ts", 37 | "default": "./dist/doc-blocks.js" 38 | }, 39 | "./package.json": "./package.json" 40 | }, 41 | "main": "dist/index.js", 42 | "types": "dist/index.d.ts", 43 | "files": [ 44 | "dist/**/*", 45 | "README.md", 46 | "*.js", 47 | "*.d.ts" 48 | ], 49 | "engines": { 50 | "node": ">=20.19" 51 | }, 52 | "scripts": { 53 | "clean": "rimraf ./dist", 54 | "prebuild": "yarn clean", 55 | "build": "tsup & yarn build:preset", 56 | "build:watch": "tsup --watch", 57 | "build:preset": "tsup --config tsup.node.config.ts", 58 | "build:preset:watch": "tsup --config tsup.node.config.ts --watch", 59 | "test": "echo \"Error: no test specified\" && exit 1", 60 | "start": "run-p build:watch build:preset:watch 'storybook --quiet'", 61 | "storybook": "storybook dev -p 6006", 62 | "build-storybook": "storybook build" 63 | }, 64 | "devDependencies": { 65 | "@storybook/addon-docs": "^10.0.0", 66 | "@storybook/react-vite": "^10.0.0", 67 | "@types/jsdom": "^21.1.1", 68 | "@types/node": "^20.19.0", 69 | "@types/prettier": "^2.7.2", 70 | "@types/react": "^19.1.6", 71 | "@types/react-dom": "^19.1.1", 72 | "@vitejs/plugin-react": "^4.5.1", 73 | "npm-run-all": "^4.1.5", 74 | "react": "^19.0.0", 75 | "react-dom": "^19.0.0", 76 | "rimraf": "^3.0.2", 77 | "storybook": "^10.0.0", 78 | "tsup": "^8.0.0", 79 | "typescript": "^5.0.0", 80 | "vite": "^6.3.5", 81 | "zx": "^1.14.1" 82 | }, 83 | "peerDependencies": { 84 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", 85 | "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", 86 | "storybook": "^10.0.0" 87 | }, 88 | "peerDependenciesMeta": { 89 | "react": { 90 | "optional": true 91 | }, 92 | "react-dom": { 93 | "optional": true 94 | } 95 | }, 96 | "publishConfig": { 97 | "access": "public" 98 | }, 99 | "storybook": { 100 | "displayName": "Storybook Design Token", 101 | "supportedFrameworks": [ 102 | "react", 103 | "vue", 104 | "angular", 105 | "web-components", 106 | "ember", 107 | "html", 108 | "svelte", 109 | "preact" 110 | ], 111 | "icon": "https://raw.githubusercontent.com/UX-and-I/storybook-design-token/master/docs/teaser.png" 112 | }, 113 | "dependencies": { 114 | "@storybook/icons": "^1.4.0", 115 | "@tanstack/react-virtual": "^3.13.9", 116 | "@uidotdev/usehooks": "^2.4.1", 117 | "glob": "^9.3.0", 118 | "jsdom": "^21.1.1", 119 | "polished": "^4.1.3", 120 | "postcss": "^8.3.11", 121 | "postcss-scss": "^4.0.2", 122 | "prettier": "^2.8.5", 123 | "raw-loader": "^4.0.2" 124 | }, 125 | "resolutions": { 126 | "jackspeak": "2.1.1" 127 | }, 128 | "packageManager": "yarn@4.10.3" 129 | } 130 | -------------------------------------------------------------------------------- /addon/src/hooks/useTokenTabs.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from "react"; 2 | import { useLocalStorage } from "@uidotdev/usehooks"; 3 | 4 | import { Category } from "../types/category.types"; 5 | import { Config } from "../types/config.types"; 6 | 7 | export function useTokenTabs(config?: Config) { 8 | const [tokenFiles, setTokenFiles] = useState<{ 9 | [type: string]: { categories: Category[]; injectionStyles: string }; 10 | }>(); 11 | 12 | const [cssCategories, setCssCategories] = useState([]); 13 | const [lessCategories, setLessCategories] = useState([]); 14 | const [scssCategories, setScssCategories] = useState([]); 15 | const [svgIconCategories, setSvgIconCategories] = useState([]); 16 | const [imageCategories, setImageIconCategories] = useState([]); 17 | 18 | const [activeCategory, setActiveCategory] = useState(); 19 | const [cardView, setCardView] = useLocalStorage( 20 | "storybook-design-token-addon-card", 21 | false 22 | ); 23 | 24 | const [styleInjections, setStyleInjections] = useState(""); 25 | 26 | const tabs = useMemo(() => { 27 | const categories = [ 28 | ...cssCategories, 29 | ...lessCategories, 30 | ...scssCategories, 31 | ...svgIconCategories, 32 | ...imageCategories, 33 | ].filter( 34 | (category) => category !== undefined && category?.tokens.length > 0 35 | ); 36 | 37 | const categoryNames = Array.from( 38 | new Set(categories.map((category) => category?.name)) 39 | ); 40 | 41 | let tabs = categoryNames.map((name) => ({ 42 | label: name, 43 | categories: categories.filter( 44 | (category) => category?.name === name 45 | ) as Category[], 46 | })); 47 | 48 | if ((config?.tabs ?? []).length !== 0) { 49 | tabs = tabs.filter((tab) => config.tabs.includes(tab.label)); 50 | } 51 | 52 | return tabs; 53 | }, [ 54 | cssCategories, 55 | lessCategories, 56 | scssCategories, 57 | svgIconCategories, 58 | imageCategories, 59 | config, 60 | ]); 61 | 62 | useEffect(() => { 63 | async function fetchTokenFiles() { 64 | const designTokenSource = await ( 65 | await fetch("./design-tokens.source.json") 66 | ).text(); 67 | 68 | setTokenFiles(JSON.parse(designTokenSource)); 69 | } 70 | 71 | fetchTokenFiles(); 72 | }, []); 73 | 74 | useEffect(() => { 75 | const cssTokens = tokenFiles?.cssTokens; 76 | const lessTokens = tokenFiles?.lessTokens; 77 | const scssTokens = tokenFiles?.scssTokens; 78 | const svgTokens = tokenFiles?.svgTokens; 79 | const imageTokens = tokenFiles?.imageTokens; 80 | 81 | setStyleInjections(config?.styleInjection || ""); 82 | 83 | if (cssTokens) { 84 | setCssCategories(cssTokens.categories); 85 | setStyleInjections((current) => current + cssTokens.injectionStyles); 86 | } 87 | 88 | if (lessTokens) { 89 | setLessCategories(lessTokens.categories); 90 | setStyleInjections((current) => current + lessTokens.injectionStyles); 91 | } 92 | 93 | if (scssTokens) { 94 | setScssCategories(scssTokens.categories); 95 | setStyleInjections((current) => current + scssTokens.injectionStyles); 96 | } 97 | 98 | if (svgTokens) { 99 | setSvgIconCategories(svgTokens.categories); 100 | } 101 | 102 | if (imageTokens) { 103 | setImageIconCategories(imageTokens.categories); 104 | } 105 | }, [config, tokenFiles]); 106 | 107 | useEffect(() => { 108 | if ( 109 | config?.defaultTab && 110 | tabs.find((item) => item.label === config.defaultTab) 111 | ) { 112 | setActiveCategory(config.defaultTab); 113 | } else if (tabs.length > 0) { 114 | setActiveCategory(tabs[0].label); 115 | } 116 | }, [config, tabs]); 117 | 118 | return { 119 | activeCategory, 120 | cardView, 121 | setActiveCategory, 122 | setCardView, 123 | styleInjections, 124 | tabs, 125 | }; 126 | } 127 | -------------------------------------------------------------------------------- /addon/src/components/DesignTokenDocBlock.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | import { styled } from "storybook/theming"; 4 | import { useTokenSearch } from "../hooks/useTokenSearch"; 5 | import { useTokenTabs } from "../hooks/useTokenTabs"; 6 | import { Category } from "../types/category.types"; 7 | import { SearchField } from "./SearchField"; 8 | import { TokenCards } from "./TokenCards"; 9 | import { TokenTable } from "./TokenTable"; 10 | import type { TokenViewType } from "./TokenTab"; 11 | import { PresenterMapType } from "./TokenPreview"; 12 | 13 | export interface DesignTokenDocBlockProps { 14 | categoryName: string; 15 | maxHeight?: number; 16 | showValueColumn?: boolean; 17 | viewType: TokenViewType; 18 | filterNames?: string[]; 19 | usageMap?: Record; 20 | theme?: string; 21 | /** 22 | * @default true 23 | */ 24 | showSearch?: boolean; 25 | pageSize?: number; 26 | presenters?: PresenterMapType; 27 | } 28 | 29 | const Container = styled.div(({}) => ({ 30 | margin: "25px 0 40px", 31 | 32 | "*": { 33 | boxSizing: "border-box" as const, 34 | }, 35 | })); 36 | 37 | const Card = styled.div(() => ({ 38 | boxShadow: 39 | "rgb(0 0 0 / 10%) 0px 1px 3px 1px, rgb(0 0 0 / 7%) 0px 0px 0px 1px", 40 | borderRadius: 4, 41 | })); 42 | 43 | export const DesignTokenDocBlock = ({ 44 | filterNames, 45 | usageMap, 46 | categoryName, 47 | maxHeight = 600, 48 | showValueColumn = true, 49 | viewType = "table", 50 | showSearch = true, 51 | pageSize, 52 | presenters, 53 | theme, 54 | }: DesignTokenDocBlockProps) => { 55 | const { tabs } = useTokenTabs({ pageSize, showSearch, presenters }); 56 | 57 | const tab = useMemo( 58 | () => tabs.find((t) => t.label === categoryName), 59 | [categoryName, tabs] 60 | ); 61 | 62 | if (!tab) { 63 | return null; 64 | } 65 | 66 | return ( 67 | 79 | ); 80 | }; 81 | 82 | interface DesignTokenDocBlockViewProps 83 | extends Omit { 84 | categories: Category[]; 85 | } 86 | /** 87 | * NOTE: Every searchText change causes full page mount/unmount, so input loses focus after input of every next character. 88 | * So the aim of DesignTokenDocBlockView component prevent re-renders, as it contains searchText change inside. 89 | */ 90 | function DesignTokenDocBlockView({ 91 | viewType, 92 | categories: categoriesProp, 93 | maxHeight, 94 | showValueColumn, 95 | showSearch, 96 | pageSize, 97 | presenters, 98 | filterNames, 99 | usageMap, 100 | theme, 101 | }: DesignTokenDocBlockViewProps) { 102 | const { searchText, setSearchText, categories } = useTokenSearch( 103 | categoriesProp ?? [] 104 | ); 105 | 106 | return ( 107 | 108 | {showSearch && ( 109 | { 112 | setSearchText(value); 113 | }} 114 | style={{ margin: "12px 0" }} 115 | /> 116 | )} 117 | {viewType === "table" && ( 118 | 119 | 129 | 130 | )} 131 | {viewType === "card" && ( 132 | 143 | )} 144 | 145 | ); 146 | } 147 | -------------------------------------------------------------------------------- /addon/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; 2 | import glob from "glob"; 3 | import path from "path"; 4 | 5 | import { parsePngFiles } from "./parsers/image.parser"; 6 | import { parseCssFiles } from "./parsers/postcss.parser"; 7 | import { parseSvgFiles } from "./parsers/svg-icon.parser"; 8 | import { TokenSourceType } from "./types/token.types"; 9 | 10 | function getTokenFilePaths(context: any, designTokenGlob?: string): string[] { 11 | const pattern = path 12 | .join( 13 | context, 14 | designTokenGlob || 15 | process.env.DESIGN_TOKEN_GLOB || 16 | "**/*.{css,scss,less,svg,png,jpeg,gif}" 17 | ) 18 | .replace(/\\/g, "/"); 19 | 20 | return glob.sync(pattern, { 21 | ignore: ["**/node_modules/**", "**/storybook-static/**", "**/*.chunk.*"], 22 | }); 23 | } 24 | 25 | function addFilesToWebpackDeps(compilation: any, files: string[]) { 26 | compilation.fileDependencies.addAll(files); 27 | } 28 | 29 | async function generateTokenFilesJsonString(files: string[]): Promise { 30 | const tokenFiles = files 31 | .map((path) => ({ 32 | filename: path, 33 | content: readFileSync(path, "utf-8"), 34 | })) 35 | .filter( 36 | (file) => 37 | file.content.includes("@tokens") || 38 | file.filename.endsWith(".svg") || 39 | isImageExtension(file.filename) 40 | ); 41 | 42 | const cssTokens = await parseCssFiles( 43 | tokenFiles.filter((file) => file.filename.endsWith(".css")), 44 | TokenSourceType.CSS, 45 | true 46 | ); 47 | 48 | const scssTokens = await parseCssFiles( 49 | tokenFiles.filter((file) => file.filename.endsWith(".scss")), 50 | TokenSourceType.SCSS, 51 | true 52 | ); 53 | 54 | const lessTokens = await parseCssFiles( 55 | tokenFiles.filter((file) => file.filename.endsWith(".less")), 56 | TokenSourceType.LESS, 57 | true 58 | ); 59 | 60 | const svgTokens = await parseSvgFiles( 61 | tokenFiles.filter((file) => file.filename.endsWith(".svg")) 62 | ); 63 | 64 | const imageTokens = await parsePngFiles( 65 | tokenFiles.filter((file) => isImageExtension(file.filename)) 66 | ); 67 | 68 | return JSON.stringify({ 69 | cssTokens, 70 | scssTokens, 71 | lessTokens, 72 | svgTokens, 73 | imageTokens, 74 | }); 75 | } 76 | 77 | export class StorybookDesignTokenPlugin { 78 | constructor(private designTokenGlob?: string) {} 79 | 80 | public apply(compiler: any) { 81 | compiler.hooks.initialize.tap("StorybookDesignTokenPlugin", () => { 82 | const files = getTokenFilePaths(compiler.context, this.designTokenGlob); 83 | 84 | compiler.hooks.emit.tap( 85 | "StorybookDesignTokenPlugin", 86 | (compilation: any) => { 87 | addFilesToWebpackDeps(compilation, files); 88 | } 89 | ); 90 | 91 | compiler.hooks.thisCompilation.tap( 92 | "StorybookDesignTokenPlugin", 93 | (compilation: any) => { 94 | compilation.hooks.processAssets.tapAsync( 95 | { 96 | name: "HtmlWebpackPlugin", 97 | stage: 98 | compiler.webpack.Compilation 99 | .PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE, 100 | }, 101 | async (compilationAssets: any, callback: any) => { 102 | const sourceString = await generateTokenFilesJsonString(files); 103 | 104 | compilationAssets["design-tokens.source.json"] = { 105 | source: () => { 106 | return sourceString; 107 | }, 108 | size: () => { 109 | return sourceString.length; 110 | }, 111 | }; 112 | 113 | callback(); 114 | } 115 | ); 116 | } 117 | ); 118 | }); 119 | } 120 | } 121 | 122 | export function viteStorybookDesignTokenPlugin(options: any) { 123 | let publicDir: string; 124 | let rootDir: string; 125 | let files: string[]; 126 | 127 | return { 128 | name: "vite-storybook-design-token-plugin", 129 | configResolved(resolvedConfig: any) { 130 | publicDir = resolvedConfig.publicDir; 131 | rootDir = resolvedConfig.root; 132 | }, 133 | transform: async function () { 134 | if (!publicDir) { 135 | return; 136 | } 137 | 138 | files = getTokenFilePaths("./", options?.designTokenGlob).map( 139 | (file) => `./${file}` 140 | ); 141 | 142 | const sourceString = await generateTokenFilesJsonString(files); 143 | 144 | if (!existsSync(publicDir)) { 145 | mkdirSync(publicDir); 146 | } 147 | 148 | writeFileSync( 149 | path.join(publicDir, "design-tokens.source.json"), 150 | sourceString 151 | ); 152 | }, 153 | } as any; 154 | } 155 | 156 | function isImageExtension(filename: string) { 157 | return ( 158 | filename.endsWith(".jpeg") || 159 | filename.endsWith(".png") || 160 | filename.endsWith(".gif") 161 | ); 162 | } 163 | -------------------------------------------------------------------------------- /addon/src/components/TokenCards.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo, useState, useRef } from "react"; 3 | import { CopyIcon, InfoIcon } from "@storybook/icons"; 4 | import { 5 | Button, 6 | TooltipMessage, 7 | TooltipNote, 8 | WithTooltip, 9 | } from "storybook/internal/components"; 10 | import { styled } from "storybook/theming"; 11 | import { createPortal } from "react-dom"; 12 | 13 | import { Category } from "../types/category.types"; 14 | import { ClipboardButton } from "./ClipboardButton"; 15 | import { PresenterMapType, TokenPreview } from "./TokenPreview"; 16 | import { TokenValue } from "./TokenValue"; 17 | import { ToolButton } from "./ToolButton"; 18 | import { useFilteredTokens } from "../hooks/useFilteredTokens"; 19 | import { Popup } from "./Popup"; 20 | import { usePopup } from "../hooks/usePopup"; 21 | import { Token } from "../types/token.types"; 22 | 23 | interface TokenCardsProps { 24 | categories: Category[]; 25 | padded?: boolean; 26 | readonly?: boolean; 27 | showValueColumn?: boolean; 28 | pageSize?: number; 29 | presenters?: PresenterMapType; 30 | filterNames?: string[]; 31 | usageMap?: Record; 32 | theme?: string; 33 | } 34 | 35 | export const TokenCards = ({ 36 | categories, 37 | padded = true, 38 | readonly, 39 | showValueColumn = true, 40 | pageSize = 50, 41 | presenters, 42 | filterNames, 43 | usageMap, 44 | theme, 45 | }: TokenCardsProps) => { 46 | const [tokenValueOverwrites, setTokenValueOverwrites] = useState<{ 47 | [tokenName: string]: any; 48 | }>({}); 49 | const [page, setPage] = useState(0); 50 | const cardRefs = useRef>(new Map()); 51 | 52 | const { popup, popupRef, handleContextMenu } = usePopup({ 53 | usageMap, 54 | getElementRef: (token: Token) => cardRefs.current.get(token.name) || null, 55 | getTokenName: (token: Token) => token.name, 56 | }); 57 | 58 | const Container = useMemo( 59 | () => 60 | styled.div(() => ({ 61 | display: "grid", 62 | columnGap: 12, 63 | gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", 64 | padding: padded ? 15 : undefined, 65 | rowGap: 12, 66 | })), 67 | [] 68 | ); 69 | 70 | const Card = useMemo( 71 | () => 72 | styled.div(({ theme }) => ({ 73 | boxShadow: 74 | "rgb(0 0 0 / 10%) 0px 1px 3px 1px, rgb(0 0 0 / 7%) 0px 0px 0px 1px", 75 | borderRadius: 4, 76 | color: theme.color.defaultText, 77 | fontFamily: theme.typography.fonts.base, 78 | fontSize: theme.typography.size.s1, 79 | padding: 12, 80 | overflow: "hidden", 81 | 82 | ":hover": { 83 | backgroundColor: theme.background.hoverable, 84 | }, 85 | 86 | "> *:not(:last-child)": { 87 | marginBottom: 8, 88 | }, 89 | 90 | svg: { 91 | maxWidth: "100%", 92 | maxHeight: "100%", 93 | }, 94 | })), 95 | [] 96 | ); 97 | 98 | const TokenName = styled.span(({ theme }) => ({ 99 | fontFamily: theme.typography.fonts.mono, 100 | fontWeight: theme.typography.weight.bold, 101 | fontSize: theme.typography.size.s1, 102 | })); 103 | 104 | const Pagination = useMemo( 105 | () => 106 | styled.div(({ theme }) => ({ 107 | alignItems: "center", 108 | color: theme.color.defaultText, 109 | display: "flex", 110 | fontFamily: theme.typography.fonts.base, 111 | fontSize: theme.typography.size.s1, 112 | justifyContent: "space-between", 113 | paddingRight: padded ? 15 : undefined, 114 | paddingBottom: padded ? 48 : undefined, 115 | paddingLeft: padded ? 15 : undefined, 116 | marginTop: 8, 117 | 118 | "& > div": { 119 | display: "flex", 120 | gap: 8, 121 | }, 122 | })), 123 | [] 124 | ); 125 | 126 | const tokens = useFilteredTokens(categories, filterNames, theme); 127 | 128 | const pages = useMemo(() => Math.ceil(tokens.length / pageSize), [tokens]); 129 | 130 | return ( 131 | <> 132 | 133 | {tokens 134 | .slice(page * pageSize, page * pageSize + pageSize) 135 | .map((token, index) => ( 136 | { 139 | if (el) cardRefs.current.set(token.name, el); 140 | }} 141 | onContextMenu={(e: React.MouseEvent) => 142 | handleContextMenu(e, token) 143 | } 144 | > 145 |
146 | {token.name} 147 | 148 | } 151 | > 152 | 155 | 156 | 157 | } 158 | value={token.name} 159 | /> 160 | 161 | 162 | {token.description && ( 163 | } 165 | > 166 | 167 | 168 | 169 | 170 | )} 171 |
172 | 173 | {showValueColumn && ( 174 | { 176 | setTokenValueOverwrites((tokenValueOverwrites) => ({ 177 | ...tokenValueOverwrites, 178 | [token.name]: 179 | newValue === token.rawValue ? undefined : newValue, 180 | })); 181 | }} 182 | readonly={readonly} 183 | token={token} 184 | /> 185 | )} 186 | 187 | 194 |
195 | ))} 196 |
197 | 198 | {pages > 1 && ( 199 | 200 | 201 | Page {page + 1} of {pages} 202 | 203 |
204 | 212 | 220 |
221 |
222 | )} 223 | 224 | {popup && 225 | usageMap && 226 | createPortal( 227 | 228 | {usageMap && usageMap[popup.tokenName] ? ( 229 | <> 230 |
Used in:
231 |
    232 | {usageMap[popup.tokenName].map((usage, index) => ( 233 |
  • {usage}
  • 234 | ))} 235 |
236 | 237 | ) : ( 238 |
Token might be unused or global, please check
239 | )} 240 |
, 241 | document.body 242 | )} 243 | 244 | ); 245 | }; 246 | -------------------------------------------------------------------------------- /addon/src/parsers/postcss.parser.ts: -------------------------------------------------------------------------------- 1 | import postcss, { AtRule, Comment, Declaration, Plugin } from "postcss"; 2 | import scss from "postcss-scss"; 3 | 4 | import { Category, CategoryRange } from "../types/category.types"; 5 | import { File } from "../types/config.types"; 6 | import { Token, TokenPresenter, TokenSourceType } from "../types/token.types"; 7 | 8 | export async function parseCssFiles( 9 | files: File[] = [], 10 | sourceType: TokenSourceType, 11 | injectVariables?: boolean 12 | ): Promise<{ categories: Category[]; injectionStyles: string }> { 13 | const relevantFiles = files.filter( 14 | (file, index, files) => 15 | file.content && 16 | !files.some((f, i) => f.content === file.content && i < index) 17 | ); 18 | 19 | const nodes = await getNodes(relevantFiles.filter((file) => file.content)); 20 | 21 | const categories = determineCategories( 22 | nodes.comments, 23 | nodes.declarations, 24 | sourceType 25 | ); 26 | 27 | let injectionStyles = nodes?.keyframes.map((k) => k.toString()).join(" "); 28 | 29 | if (injectVariables) { 30 | injectionStyles = 31 | injectionStyles + 32 | `:root { 33 | ${nodes.declarations 34 | .map((declaration) => declaration.toString()) 35 | .join(";")} 36 | }`; 37 | } 38 | 39 | return { categories, injectionStyles }; 40 | } 41 | 42 | function determineCategories( 43 | comments: Comment[], 44 | declarations: Declaration[], 45 | sourceType: TokenSourceType 46 | ): Category[] { 47 | const categoryComments = comments.filter( 48 | (comment) => 49 | comment.text.includes("@tokens ") || comment.text.includes("@tokens-end") 50 | ); 51 | 52 | return categoryComments 53 | .map((comment, index) => { 54 | if (comment.text.includes("@tokens-end")) { 55 | return undefined; 56 | } 57 | 58 | const nextComment = categoryComments[index + 1]; 59 | const nextCommentIsInAnotherFile = 60 | comment.source?.input.file !== nextComment?.source?.input.file; 61 | const nameResults = /@tokens (.+)/g.exec(comment.text); 62 | const presenterResults = /@presenter (.+)/g.exec(comment.text); 63 | 64 | const presenter: TokenPresenter = presenterResults?.[1] as TokenPresenter; 65 | 66 | if ( 67 | presenter && 68 | !Object.values(TokenPresenter).includes( 69 | (presenter || "") as TokenPresenter 70 | ) 71 | ) { 72 | throw new Error(`Presenter "${presenter}" is not valid.`); 73 | } 74 | 75 | const range: CategoryRange = { 76 | from: { 77 | column: comment.source?.start?.column || 0, 78 | line: comment.source?.start?.line || 0, 79 | }, 80 | to: 81 | !nextCommentIsInAnotherFile && nextComment?.prev() 82 | ? { 83 | column: nextComment.prev()?.source?.end?.column || 0, 84 | line: nextComment.prev()?.source?.end?.line || 0, 85 | } 86 | : !nextCommentIsInAnotherFile && nextComment 87 | ? { 88 | column: nextComment.source?.start?.column || 0, 89 | line: nextComment.source?.start?.line || 0, 90 | } 91 | : undefined, 92 | }; 93 | 94 | const source = comment.source?.input.from || ""; 95 | 96 | return { 97 | name: nameResults?.[1] || "", 98 | presenter, 99 | range, 100 | source, 101 | tokens: determineTokensForCategory( 102 | source, 103 | range, 104 | declarations, 105 | comments, 106 | sourceType, 107 | presenter 108 | ), 109 | }; 110 | }) 111 | .filter(isCategory); 112 | } 113 | 114 | function determineTokensForCategory( 115 | source: string, 116 | range: CategoryRange, 117 | declarations: Declaration[], 118 | comments: Comment[], 119 | sourceType: TokenSourceType, 120 | presenter: TokenPresenter 121 | ): Token[] { 122 | const declarationsWithinRange = declarations.filter( 123 | (declaration) => 124 | declaration.source?.input.from === source && 125 | (declaration.source?.start?.line || -1) > range.from.line && 126 | (!range.to || (declaration.source?.start?.line || -1) <= range.to.line) 127 | ); 128 | 129 | return declarationsWithinRange 130 | .map((declaration) => { 131 | const description = comments.find( 132 | (comment) => 133 | comment.source?.input.file === declaration.source?.input.file && 134 | comment.source?.start?.line === declaration.source?.end?.line 135 | ); 136 | 137 | const value = determineTokenValue(declaration.value, declarations); 138 | let presenterToken: TokenPresenter | undefined; 139 | 140 | if (description) { 141 | const presenterResultsToken = /@presenter (.+)/g.exec(description.text); 142 | 143 | if (presenterResultsToken) { 144 | presenterToken = presenterResultsToken[1] as TokenPresenter; 145 | description.text = description.text.replace( 146 | presenterResultsToken[0] || "", 147 | "" 148 | ); 149 | } 150 | } 151 | 152 | return { 153 | description: description?.text, 154 | isAlias: value !== declaration.value, 155 | name: declaration.prop, 156 | presenter: presenterToken || presenter, 157 | rawValue: declaration.value, 158 | sourceType, 159 | value, 160 | sourcePath: declaration.source?.input.from || "", 161 | }; 162 | }) 163 | .slice() 164 | .reverse() 165 | .filter( 166 | (token, index, tokens) => 167 | index === tokens.findIndex((t) => t.name === token.name) 168 | ) 169 | .reverse(); 170 | } 171 | 172 | function determineTokenValue( 173 | rawValue: string, 174 | declarations: Declaration[] 175 | ): string { 176 | rawValue = rawValue.replace(/!default/g, "").replace(/!global/g, ""); 177 | 178 | const regex = /\bvar\(([^)]+)\)|(\$[a-zA-Z0-9-_]+|@[a-zA-Z0-9-_]+)/g; 179 | let match; 180 | let replacedString = rawValue; 181 | while ((match = regex.exec(rawValue)) !== null) { 182 | const variableMatch = match[0]; 183 | const variableName = variableMatch.replace(/\(|\)|var\(|@|\$/g, ""); 184 | const replacement = 185 | declarations.find( 186 | (declaration) => 187 | declaration.prop === variableName || 188 | declaration.prop === `$${variableName}` || 189 | declaration.prop === `@${variableName}` 190 | )?.value || ""; 191 | 192 | replacedString = replacedString.replace(variableMatch, replacement); 193 | } 194 | 195 | return replacedString; 196 | } 197 | 198 | async function getNodes(files: File[]): Promise<{ 199 | comments: Comment[]; 200 | declarations: Declaration[]; 201 | keyframes: AtRule[]; 202 | }> { 203 | const comments: Comment[] = []; 204 | const declarations: Declaration[] = []; 205 | const keyframes: AtRule[] = []; 206 | 207 | const plugin: Plugin = { 208 | postcssPlugin: "storybook-design-token-parser", 209 | Once(root) { 210 | root.walkAtRules((atRule) => { 211 | if (atRule.name === "keyframes") { 212 | keyframes.push(atRule); 213 | return; 214 | } 215 | 216 | const variableAtRule = atRule; 217 | 218 | if (variableAtRule.name.endsWith(":")) { 219 | declarations.push({ 220 | ...variableAtRule, 221 | prop: `@${variableAtRule.name.substr( 222 | 0, 223 | variableAtRule.name.length - 1 224 | )}`, 225 | value: variableAtRule.params, 226 | } as any); 227 | } 228 | }); 229 | 230 | root.walkComments((comment) => { 231 | comments.push(comment); 232 | }); 233 | 234 | root.walkDecls((declaration) => { 235 | if ( 236 | declaration.prop.startsWith("--") || 237 | declaration.prop.startsWith("$") 238 | ) { 239 | declarations.push(declaration); 240 | } 241 | }); 242 | }, 243 | }; 244 | 245 | await Promise.all( 246 | files.map((file) => { 247 | const syntax: any = 248 | file.filename.endsWith(".scss") || file.filename.endsWith(".less") 249 | ? scss 250 | : undefined; 251 | 252 | return postcss([plugin]).process(file.content, { 253 | from: file.filename, 254 | syntax, 255 | }); 256 | }) 257 | ); 258 | 259 | return { comments, declarations, keyframes }; 260 | } 261 | 262 | function isCategory(object: any): object is Category { 263 | return !!object && object.hasOwnProperty("presenter"); 264 | } 265 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ **This is the documentation for v5 which supports Storybook v10 and newer (ESM-only). Please check the v4 branch for Storybook v9, or the v3 branch for Storybook v7 and v8.** ⚠️ 2 | 3 | # Storybook Design Token Addon 4 | 5 | [![Netlify Status](https://api.netlify.com/api/v1/badges/de6a7567-7e09-4753-a3b9-5a058dc8f03f/deploy-status)](https://app.netlify.com/sites/storybook-design-token-v1/deploys) 6 | 7 | Display design token documentation generated from your stylesheets and icon files. Preview design token changes in the browser. Add your design tokens to your Storybook Docs pages using the custom Doc Blocks. 8 | 9 | **[Show me the demo](https://storybook-design-token-v1.netlify.app/?path=/docs/demo--docs)** 10 | 11 | ![Teaser image](docs/teaser.png) 12 | 13 | **Contents:** 14 | 15 | - [Storybook Design Token Addon](#storybook-design-token-addon) 16 | - [Get started](#get-started) 17 | - [Available presenters](#available-presenters) 18 | - [Advanced configuration](#advanced-configuration) 19 | - [Default tab](#default-tab) 20 | - [Visible tabs](#visible-tabs) 21 | - [Style injection](#style-injection) 22 | - [Disable the addon panel](#disable-the-addon-panel) 23 | - [Token search visibility](#token-search-visibility) 24 | - [Pagination](#pagination) 25 | - [Specify a custom glob for your token files](#specify-a-custom-glob-for-your-token-files) 26 | - [Design Token Doc Block](#design-token-doc-block) 27 | - [Custom Presenters](#custom-presenters) 28 | - [Custom filters](#custom-filters) 29 | - [Browser support](#browser-support) 30 | 31 | ## Get started 32 | 33 | First, install the addon. 34 | 35 | ```sh 36 | $ yarn add --dev storybook-design-token 37 | # or 38 | $ npm add --save-dev storybook-design-token 39 | ``` 40 | 41 | Add the addon to your storybook addon list inside `.storybook/main.js`: 42 | 43 | ```javascript 44 | export default { 45 | addons: ['storybook-design-token'] 46 | }; 47 | ``` 48 | 49 | The last step is to annotate your design tokens with a category name and a presenter. You can do this by adding special comment blocks to your stylesheets. Below is an example of a css stylesheet defining three categories ("Animations", "Colors", "Others"). It works the same way for scss and less files. 50 | 51 | ```css 52 | :root { 53 | /** 54 | * @tokens Animations 55 | * @presenter Animation 56 | */ 57 | 58 | --animation-rotate: rotate 1.2s infinite cubic-bezier(0.55, 0, 0.1, 1); 59 | 60 | /** 61 | * @tokens Colors 62 | * @presenter Color 63 | */ 64 | 65 | --b100: hsl(240, 100%, 90%); /* Token Description Example @presenter Color */ 66 | --b200: hsl(240, 100%, 80%); 67 | --b300: hsl(240, 100%, 70%); 68 | 69 | /** 70 | * @tokens Others 71 | */ 72 | --border-normal: 3px dashed red; /* Token Description Example @presenter BorderRadius */ 73 | } 74 | ``` 75 | 76 | The presenter controls how your token previews are rendered. See the next section for a complete list of available presenters. You can omit the presenter definition if you don't want to render a preview or no presenter works with your token. 77 | 78 | By default, a token category ends with the comment block of the next category. If you want to end a category block before the next category comment, you can insert a special comment to end the block early: 79 | 80 | ```css 81 | /** 82 | * @tokens-end 83 | */ 84 | ``` 85 | 86 | To list your svg icons, the addon parses your svg files searching for svg elements. **Important: Only svg elements with an `id` or `data-token-name` attribute are added to the token list.** You can provide descriptions and category names for your icons using the (optional) attributes `data-token-description` and `data-token-category`. 87 | 88 | ## Available presenters 89 | 90 | Please check the **[demo](https://storybook-design-token-v1.netlify.app/?path=/story/components-button--button)** to see the presenters in action. 91 | 92 | - Animation 93 | - Border 94 | - BorderRadius 95 | - Color 96 | - Easing 97 | - FontFamily 98 | - FontSize 99 | - FontWeight 100 | - LetterSpacing 101 | - LineHeight 102 | - Opacity 103 | - Shadow 104 | - Spacing 105 | 106 | ## Advanced configuration 107 | 108 | ### Default tab 109 | 110 | You can specify the default tab shown in the addon panel. Set it to the corresponding category name. 111 | 112 | `.storybook/preview.js` 113 | 114 | ```javascript 115 | export default { 116 | parameters: { 117 | designToken: { 118 | defaultTab: 'Colors' 119 | } 120 | } 121 | }; 122 | ``` 123 | 124 | ### Visible tabs 125 | 126 | If you don't want to show all available tabs, it is possible to specify which tabs should be shown in the addon panel via the `tabs` setting. 127 | 128 | ```javascript 129 | export default { 130 | parameters: { 131 | designToken: { 132 | tabs: ['Colors'] 133 | } 134 | } 135 | }; 136 | ``` 137 | 138 | ### Style injection 139 | 140 | To inject styles needed by your design token documentation, use the `styleInjection` parameter. A typical usecase are web fonts needed by your font family tokens. Please note that the styleInjection parameter only works with valid css. 141 | 142 | `.storybook/preview.js` 143 | 144 | ```javascript 145 | export default { 146 | parameters: { 147 | designToken: { 148 | styleInjection: 149 | '@import url("https://fonts.googleapis.com/css2?family=Open+Sans&display=swap");' 150 | } 151 | } 152 | }; 153 | ``` 154 | 155 | ### Disable the addon panel 156 | 157 | In some cases you might only want to use the Doc Blocks and hide the addon panel. You can do so by the setting the `disable` parameter: 158 | 159 | ```javascript 160 | export default { 161 | parameters: { 162 | designToken: { 163 | disable: true 164 | } 165 | } 166 | }; 167 | ``` 168 | 169 | ### Token search visibility 170 | 171 | In some cases you might not need the search field to be visible. You can control its visibility by the setting the `showSearch` parameter: 172 | 173 | ```javascript 174 | export default { 175 | parameters: { 176 | designToken: { 177 | showSearch: false 178 | } 179 | } 180 | }; 181 | ``` 182 | 183 | ### Pagination 184 | 185 | By default `pageSize` of the card view is 50 items. You can configure it by setting the `pageSize` parameter: 186 | 187 | ```javascript 188 | export default { 189 | parameters: { 190 | designToken: { 191 | pageSize: 10 192 | } 193 | } 194 | }; 195 | ``` 196 | 197 | You can disable pagination in the following way: 198 | 199 | ```javascript 200 | export default { 201 | parameters: { 202 | designToken: { 203 | // specify max value to disable pagination 204 | pageSize: Number.MAX_VALUE 205 | } 206 | } 207 | }; 208 | ``` 209 | 210 | ### Specify a custom glob for your token files 211 | 212 | By default, the addon parses all `.css`, `.scss`, `.less`, `.svg`, `.jpeg`, `.png` and `.gif` files of your code base for annotated design tokens. If you only want to parse specific files, you can pass a [glob](https://github.com/isaacs/node-glob) via the `DESIGN_TOKEN_GLOB` environment variable or via an option in your `main.js`. 213 | 214 | For example: 215 | 216 | ``` 217 | DESIGN_TOKEN_GLOB=**/*.tokens.{css,scss,less,svg} 218 | ``` 219 | 220 | ## Design Token Doc Block 221 | 222 | This addon comes with a custom Storybook Doc Block allowing you to display your design token documentation inside docs pages. 223 | 224 | ```tsx 225 | // colors.stories.mdx 226 | 227 | import { DesignTokenDocBlock } from 'storybook-design-token'; 228 | 229 | ; 230 | ``` 231 | 232 | The `categoryName` parameter references your token category name (the part after `@tokens` in your stylesheet annotations). The `viewType` parameter can be set to `card` or `table` to switch between both presentations. In some cases you might want to hide the token values. You can do that by passing `showValueColumn={false}`. 233 | Check the [demo file](https://github.com/UX-and-I/storybook-design-token/blob/v1/demo/src/design-tokens/colors.stories.mdx) for usage examples. 234 | 235 | ### Custom Presenters 236 | 237 | `DesignTokenDocBlock` component allows you to use custom presenters. You can either create a new presenter or override an existing one. 238 | 239 | Example of overriding the existing Color presenter: 240 | 241 | ```tsx 242 | import React from 'react'; 243 | 244 | export function CircleColorPresenter({ token }) { 245 | return ( 246 |
254 | ); 255 | } 256 | ``` 257 | 258 | ```tsx 259 | import { DesignTokenDocBlock } from 'storybook-design-token'; 260 | import { CircleColorPresenter } from './CircleColorPresenter'; 261 | 262 | ; 267 | ``` 268 | 269 | ### Custom filters 270 | 271 | The `filterNames` prop allows you to filter the design tokens displayed in the `DesignTokenDocBlock` by variable names. Use this to focus on a subset of tokens in your Storybook documentation. 272 | 273 | ```tsx 274 | // colors.stories.mdx 275 | 276 | import { DesignTokenDocBlock } from 'storybook-design-token'; 277 | 278 | ; 283 | ``` 284 | 285 | You can also pass a theme to the `DesignTokenDocBlock` component. This is useful when you have two themes with the same variable names but only want to display the variables for the current theme. Just pass the `theme` property to do this. 286 | 287 | ### Token Usage Map 288 | 289 | You can provide a `usageMap` to the `DesignTokenDocBlock` component to display where each token is used across your components. 290 | 291 | This is especially helpful for design teams who want to trace Figma tokens to component usage. When a `usageMap` is provided, users can **right-click a token row** in the table view to open a modal displaying the list of components where the token is used. 292 | 293 | **Usage Example:** 294 | 295 | ```tsx 296 | 303 | ``` 304 | 305 | > [!NOTE] 306 | > The usageMap is an object where keys are token names (e.g., --b100) and values are arrays of component names. 307 | > Tokens not found in the usage map or mapped to an > empty array will display a fallback message: `This token appears to be global or unused.` 308 | 309 | > 💡 Typically, this map is generated in a prebuild step 310 | > (e.g., using a script that scans your component codebase for token usage). 311 | 312 | ## Browser support 313 | 314 | - All modern browsers 315 | - ~~Internet Explorer 11~~ 316 | -------------------------------------------------------------------------------- /addon/src/components/TokenTable.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { transparentize } from "polished"; 3 | import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; 4 | import { useVirtualizer } from "@tanstack/react-virtual"; 5 | import { CopyIcon, InfoIcon } from "@storybook/icons"; 6 | import { 7 | TooltipMessage, 8 | TooltipNote, 9 | WithTooltip, 10 | } from "storybook/internal/components"; 11 | import { styled } from "storybook/theming"; 12 | import { useFilteredTokens } from "../hooks/useFilteredTokens"; 13 | import { createPortal } from "react-dom"; 14 | 15 | import { Category } from "../types/category.types"; 16 | import { ClipboardButton } from "./ClipboardButton"; 17 | import { PresenterMapType, TokenPreview } from "./TokenPreview"; 18 | import { TokenValue } from "./TokenValue"; 19 | import { ToolButton } from "./ToolButton"; 20 | import { Popup } from "./Popup"; 21 | import { usePopup } from "../hooks/usePopup"; 22 | import { Token } from "src/types/token.types"; 23 | 24 | interface TokenTableProps { 25 | categories: Category[]; 26 | maxHeight?: number; 27 | readonly?: boolean; 28 | showValueColumn?: boolean; 29 | presenters?: PresenterMapType; 30 | filterNames?: string[]; 31 | theme?: string; 32 | usageMap?: Record; 33 | } 34 | 35 | export const TokenTable = ({ 36 | categories, 37 | maxHeight, 38 | readonly, 39 | showValueColumn = true, 40 | presenters, 41 | filterNames, 42 | theme, 43 | usageMap, 44 | }: TokenTableProps) => { 45 | const [tokenValueOverwrites, setTokenValueOverwrites] = useState<{ 46 | [tokenName: string]: any; 47 | }>({}); 48 | const [panelHeight, setPanelHeight] = useState(maxHeight || 100); 49 | const rowRefs = useRef>(new Map()); 50 | 51 | const { popup, popupRef, handleContextMenu } = usePopup({ 52 | usageMap, 53 | getElementRef: (item: { token: Token; index: number }) => 54 | rowRefs.current.get(item.index) || null, 55 | getTokenName: (item: { token: Token; index: number }) => item.token.name, 56 | }); 57 | 58 | const parentRef = useRef(null); 59 | const theadRef = useRef(null); 60 | 61 | const tokens = useFilteredTokens(categories, filterNames, theme); 62 | 63 | const rowVirtualizer = useVirtualizer({ 64 | count: tokens.length, 65 | getScrollElement: () => parentRef.current, 66 | estimateSize: useCallback(() => 49, []), 67 | }); 68 | 69 | const ScrollContainer = useMemo( 70 | () => 71 | styled.div(() => ({ 72 | maxHeight: panelHeight ? `${panelHeight + 30}px` : "none", 73 | overflow: "auto", 74 | padding: "15px", 75 | })), 76 | [panelHeight] 77 | ); 78 | 79 | const Table = useMemo( 80 | () => 81 | styled.table(({ theme }) => ({ 82 | borderCollapse: "collapse", 83 | borderSpacing: 0, 84 | color: theme.color.defaultText, 85 | fontFamily: theme.typography.fonts.base, 86 | fontSize: theme.typography.size.s1, 87 | minWidth: 700, 88 | tableLayout: "fixed", 89 | textAlign: "left", 90 | width: "100%", 91 | 92 | "thead > tr": { 93 | display: "flex", 94 | }, 95 | 96 | "tbody > tr": { 97 | borderTop: `1px solid ${theme.color.mediumlight}`, 98 | display: "flex", 99 | 100 | ":first-of-type": { 101 | borderTopColor: theme.color.medium, 102 | }, 103 | 104 | ":last-of-type": { 105 | borderBottom: `1px solid ${theme.color.mediumlight}`, 106 | }, 107 | 108 | ":hover": { 109 | backgroundColor: theme.background.hoverable, 110 | }, 111 | }, 112 | 113 | tr: { 114 | ":hover": { 115 | backgroundColor: "rgba(0,0,0, 0.1)", 116 | }, 117 | }, 118 | "td, th": { 119 | border: "none !important", 120 | textOverflow: "ellipsis", 121 | verticalAlign: "middle", 122 | 123 | ":nth-of-type(1)": { 124 | flexBasis: "50%", 125 | flexGrow: 1, 126 | flexShrink: 0, 127 | }, 128 | 129 | ":nth-of-type(2)": { 130 | flexBasis: "25%", 131 | flexGrow: 0, 132 | flexShrink: 0, 133 | }, 134 | 135 | ":nth-of-type(3)": { 136 | flexBasis: "25%", 137 | flexGrow: 0, 138 | flexShrink: 0, 139 | }, 140 | }, 141 | 142 | th: { 143 | color: 144 | theme.base === "light" 145 | ? transparentize(0.25, theme.color.defaultText) 146 | : transparentize(0.45, theme.color.defaultText), 147 | paddingBottom: 12, 148 | 149 | ":not(:first-of-type)": { 150 | paddingLeft: 15, 151 | }, 152 | 153 | ":not(:last-of-type)": { 154 | paddingRight: 15, 155 | }, 156 | 157 | ":last-of-type": { 158 | width: 200, 159 | }, 160 | }, 161 | 162 | td: { 163 | overflow: "hidden", 164 | paddingBottom: 8, 165 | paddingTop: 8, 166 | alignItems: "center", 167 | 168 | ":not(:first-of-type)": { 169 | paddingLeft: 15, 170 | }, 171 | 172 | ":not(:last-of-type)": { 173 | paddingRight: 15, 174 | }, 175 | 176 | svg: { 177 | maxWidth: "100%", 178 | maxHeight: "100%", 179 | }, 180 | 181 | span: { 182 | alignItems: "center", 183 | display: "flex", 184 | height: "100%", 185 | }, 186 | }, 187 | })), 188 | [] 189 | ); 190 | 191 | const TokenName = styled.span(({ theme }) => ({ 192 | fontFamily: theme.typography.fonts.mono, 193 | fontWeight: theme.typography.weight.bold, 194 | fontSize: theme.typography.size.s1, 195 | })); 196 | 197 | useLayoutEffect(() => { 198 | const container = document.querySelector("#storybook-panel-root"); 199 | if (!container) { 200 | return; 201 | } 202 | 203 | const resizeHandler = () => { 204 | if (maxHeight !== undefined || !container) { 205 | return; 206 | } 207 | 208 | const vpHeight = window.innerHeight; 209 | const tableTop = parentRef.current?.getBoundingClientRect().top || 0; 210 | 211 | const height = vpHeight - tableTop - 40; 212 | 213 | setPanelHeight(height); 214 | }; 215 | 216 | setTimeout(() => { 217 | resizeHandler(); 218 | }); 219 | 220 | const resizeObserver = new ResizeObserver(resizeHandler); 221 | resizeObserver.observe(container); 222 | 223 | return () => resizeObserver.disconnect(); 224 | }, []); 225 | 226 | // Force virtualizer to recalculate after mount - fixes Firefox rendering issue 227 | useEffect(() => { 228 | const frame = requestAnimationFrame(() => { 229 | rowVirtualizer.measure(); 230 | }); 231 | return () => cancelAnimationFrame(frame); 232 | }, [tokens]); 233 | 234 | return ( 235 | 236 | 245 | 250 | 251 | 252 | {showValueColumn && } 253 | 254 | 255 | 256 | 257 | {rowVirtualizer.getVirtualItems().map((virtualRow) => { 258 | const token = tokens[virtualRow.index]; 259 | 260 | return ( 261 | { 264 | if (el) rowRefs.current.set(virtualRow.index, el); 265 | }} 266 | onContextMenu={(e) => 267 | handleContextMenu(e, { token, index: virtualRow.index }) 268 | } 269 | style={{ 270 | position: "absolute", 271 | top: 0, 272 | left: 0, 273 | width: "100%", 274 | height: `${virtualRow.size}px`, 275 | transform: `translateY(${ 276 | virtualRow.start + 277 | (theadRef.current?.getBoundingClientRect().height || 0) 278 | }px)`, 279 | }} 280 | > 281 | 310 | {showValueColumn && ( 311 | 325 | )} 326 | 335 | 336 | ); 337 | })} 338 | 339 |
NameValuePreview
282 | 283 | {token.name} 284 | 285 | } 288 | > 289 | 292 | 293 | 294 | } 295 | value={token.name} 296 | /> 297 | 298 | 299 | {token.description && ( 300 | } 302 | > 303 | 304 | 305 | 306 | 307 | )} 308 | 309 | 312 | { 315 | setTokenValueOverwrites((tokenValueOverwrites) => ({ 316 | ...tokenValueOverwrites, 317 | [token.name]: 318 | newValue === token.rawValue ? undefined : newValue, 319 | })); 320 | }} 321 | readonly={readonly} 322 | token={token} 323 | /> 324 | 327 | 334 |
340 | {popup && 341 | usageMap && 342 | createPortal( 343 | 344 | {usageMap[popup.tokenName] ? ( 345 | <> 346 |
Used in:
347 |
    348 | {usageMap[popup.tokenName].map((usage, index) => ( 349 |
  • {usage}
  • 350 | ))} 351 |
352 | 353 | ) : ( 354 |
Token might be unused or global, please check
355 | )} 356 |
, 357 | document.body 358 | )} 359 |
360 | ); 361 | }; 362 | -------------------------------------------------------------------------------- /addon/src/demo/tokens.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /** 3 | * @tokens Animations 4 | * @presenter Animation 5 | */ 6 | 7 | --animation-rotate: rotate 1.2s infinite cubic-bezier(0.55, 0, 0.1, 1); 8 | 9 | /** 10 | * @tokens Border Radius 11 | * @presenter BorderRadius 12 | */ 13 | 14 | --border-radius-s: 4px; 15 | --border-radius-m: 8px; 16 | --border-radius-l: 16px; 17 | 18 | /** 19 | * @tokens Borders 20 | * @presenter Border 21 | */ 22 | 23 | --border: 3px dashed var(--brand); 24 | 25 | /** 26 | * @tokens Colors 27 | * @presenter Color 28 | */ 29 | 30 | --b100: hsl(240, 100%, 90%); /* Token Description Example */ 31 | --b200: hsl(240, 100%, 80%); 32 | --b300: hsl(240, 100%, 70%); 33 | --b400: hsl(240, 100%, 60%); 34 | --b500: hsl(240, 100%, 50%); 35 | --b600: hsl(240, 100%, 40%); 36 | --b700: hsl(240, 100%, 30%); 37 | --b800: hsl(240, 100%, 20%); 38 | --b900: hsl(240, 100%, 10%); 39 | 40 | --n100: hsl(0, 0%, 90%); 41 | --n200: hsl(0, 0%, 80%); 42 | --n300: hsl(0, 0%, 70%); 43 | --n400: hsl(0, 0%, 60%); 44 | --n500: hsl(0, 0%, 50%); 45 | --n600: hsl(0, 0%, 40%); 46 | --n700: hsl(0, 0%, 30%); 47 | --n800: hsl(0, 0%, 20%); 48 | --n900: hsl(0, 0%, 10%); 49 | 50 | --brand: blue; 51 | 52 | /* --n1001: red; 53 | --n1002: red; 54 | --n1003: red; 55 | --n1004: red; 56 | --n1005: red; 57 | --n1006: red; 58 | --n1007: red; 59 | --n1008: red; 60 | --n1009: red; 61 | --n1010: red; 62 | --n1011: red; 63 | --n1012: red; 64 | --n1013: red; 65 | --n1014: red; 66 | --n1015: red; 67 | --n1016: red; 68 | --n1017: red; 69 | --n1018: red; 70 | --n1019: red; 71 | --n1020: red; 72 | --n1021: red; 73 | --n1022: red; 74 | --n1023: red; 75 | --n1024: red; 76 | --n1025: red; 77 | --n1026: red; 78 | --n1027: red; 79 | --n1028: red; 80 | --n1029: red; 81 | --n1030: red; 82 | --n1031: red; 83 | --n1032: red; 84 | --n1033: red; 85 | --n1034: red; 86 | --n1035: red; 87 | --n1036: red; 88 | --n1037: red; 89 | --n1038: red; 90 | --n1039: red; 91 | --n1040: red; 92 | --n1041: red; 93 | --n1042: red; 94 | --n1043: red; 95 | --n1044: red; 96 | --n1045: red; 97 | --n1046: red; 98 | --n1047: red; 99 | --n1048: red; 100 | --n1049: red; 101 | --n1050: red; 102 | --n1051: red; 103 | --n1052: red; 104 | --n1053: red; 105 | --n1054: red; 106 | --n1055: red; 107 | --n1056: red; 108 | --n1057: red; 109 | --n1058: red; 110 | --n1059: red; 111 | --n1060: red; 112 | --n1061: red; 113 | --n1062: red; 114 | --n1063: red; 115 | --n1064: red; 116 | --n1065: red; 117 | --n1066: red; 118 | --n1067: red; 119 | --n1068: red; 120 | --n1069: red; 121 | --n1070: red; 122 | --n1071: red; 123 | --n1072: red; 124 | --n1073: red; 125 | --n1074: red; 126 | --n1075: red; 127 | --n1076: red; 128 | --n1077: red; 129 | --n1078: red; 130 | --n1079: red; 131 | --n1080: red; 132 | --n1081: red; 133 | --n1082: red; 134 | --n1083: red; 135 | --n1084: red; 136 | --n1085: red; 137 | --n1086: red; 138 | --n1087: red; 139 | --n1088: red; 140 | --n1089: red; 141 | --n1090: red; 142 | --n1091: red; 143 | --n1092: red; 144 | --n1093: red; 145 | --n1094: red; 146 | --n1095: red; 147 | --n1096: red; 148 | --n1097: red; 149 | --n1098: red; 150 | --n1099: red; 151 | --n1100: red; 152 | --n1101: red; 153 | --n1102: red; 154 | --n1103: red; 155 | --n1104: red; 156 | --n1105: red; 157 | --n1106: red; 158 | --n1107: red; 159 | --n1108: red; 160 | --n1109: red; 161 | --n1110: red; 162 | --n1111: red; 163 | --n1112: red; 164 | --n1113: red; 165 | --n1114: red; 166 | --n1115: red; 167 | --n1116: red; 168 | --n1117: red; 169 | --n1118: red; 170 | --n1119: red; 171 | --n1120: red; 172 | --n1121: red; 173 | --n1122: red; 174 | --n1123: red; 175 | --n1124: red; 176 | --n1125: red; 177 | --n1126: red; 178 | --n1127: red; 179 | --n1128: red; 180 | --n1129: red; 181 | --n1130: red; 182 | --n1131: red; 183 | --n1132: red; 184 | --n1133: red; 185 | --n1134: red; 186 | --n1135: red; 187 | --n1136: red; 188 | --n1137: red; 189 | --n1138: red; 190 | --n1139: red; 191 | --n1140: red; 192 | --n1141: red; 193 | --n1142: red; 194 | --n1143: red; 195 | --n1144: red; 196 | --n1145: red; 197 | --n1146: red; 198 | --n1147: red; 199 | --n1148: red; 200 | --n1149: red; 201 | --n1150: red; 202 | --n1151: red; 203 | --n1152: red; 204 | --n1153: red; 205 | --n1154: red; 206 | --n1155: red; 207 | --n1156: red; 208 | --n1157: red; 209 | --n1158: red; 210 | --n1159: red; 211 | --n1160: red; 212 | --n1161: red; 213 | --n1162: red; 214 | --n1163: red; 215 | --n1164: red; 216 | --n1165: red; 217 | --n1166: red; 218 | --n1167: red; 219 | --n1168: red; 220 | --n1169: red; 221 | --n1170: red; 222 | --n1171: red; 223 | --n1172: red; 224 | --n1173: red; 225 | --n1174: red; 226 | --n1175: red; 227 | --n1176: red; 228 | --n1177: red; 229 | --n1178: red; 230 | --n1179: red; 231 | --n1180: red; 232 | --n1181: red; 233 | --n1182: red; 234 | --n1183: red; 235 | --n1184: red; 236 | --n1185: red; 237 | --n1186: red; 238 | --n1187: red; 239 | --n1188: red; 240 | --n1189: red; 241 | --n1190: red; 242 | --n1191: red; 243 | --n1192: red; 244 | --n1193: red; 245 | --n1194: red; 246 | --n1195: red; 247 | --n1196: red; 248 | --n1197: red; 249 | --n1198: red; 250 | --n1199: red; 251 | --n1200: red; 252 | --n1201: red; 253 | --n1202: red; 254 | --n1203: red; 255 | --n1204: red; 256 | --n1205: red; 257 | --n1206: red; 258 | --n1207: red; 259 | --n1208: red; 260 | --n1209: red; 261 | --n1210: red; 262 | --n1211: red; 263 | --n1212: red; 264 | --n1213: red; 265 | --n1214: red; 266 | --n1215: red; 267 | --n1216: red; 268 | --n1217: red; 269 | --n1218: red; 270 | --n1219: red; 271 | --n1220: red; 272 | --n1221: red; 273 | --n1222: red; 274 | --n1223: red; 275 | --n1224: red; 276 | --n1225: red; 277 | --n1226: red; 278 | --n1227: red; 279 | --n1228: red; 280 | --n1229: red; 281 | --n1230: red; 282 | --n1231: red; 283 | --n1232: red; 284 | --n1233: red; 285 | --n1234: red; 286 | --n1235: red; 287 | --n1236: red; 288 | --n1237: red; 289 | --n1238: red; 290 | --n1239: red; 291 | --n1240: red; 292 | --n1241: red; 293 | --n1242: red; 294 | --n1243: red; 295 | --n1244: red; 296 | --n1245: red; 297 | --n1246: red; 298 | --n1247: red; 299 | --n1248: red; 300 | --n1249: red; 301 | --n1250: red; 302 | --n1251: red; 303 | --n1252: red; 304 | --n1253: red; 305 | --n1254: red; 306 | --n1255: red; 307 | --n1256: red; 308 | --n1257: red; 309 | --n1258: red; 310 | --n1259: red; 311 | --n1260: red; 312 | --n1261: red; 313 | --n1262: red; 314 | --n1263: red; 315 | --n1264: red; 316 | --n1265: red; 317 | --n1266: red; 318 | --n1267: red; 319 | --n1268: red; 320 | --n1269: red; 321 | --n1270: red; 322 | --n1271: red; 323 | --n1272: red; 324 | --n1273: red; 325 | --n1274: red; 326 | --n1275: red; 327 | --n1276: red; 328 | --n1277: red; 329 | --n1278: red; 330 | --n1279: red; 331 | --n1280: red; 332 | --n1281: red; 333 | --n1282: red; 334 | --n1283: red; 335 | --n1284: red; 336 | --n1285: red; 337 | --n1286: red; 338 | --n1287: red; 339 | --n1288: red; 340 | --n1289: red; 341 | --n1290: red; 342 | --n1291: red; 343 | --n1292: red; 344 | --n1293: red; 345 | --n1294: red; 346 | --n1295: red; 347 | --n1296: red; 348 | --n1297: red; 349 | --n1298: red; 350 | --n1299: red; 351 | --n1300: red; 352 | --n1301: red; 353 | --n1302: red; 354 | --n1303: red; 355 | --n1304: red; 356 | --n1305: red; 357 | --n1306: red; 358 | --n1307: red; 359 | --n1308: red; 360 | --n1309: red; 361 | --n1310: red; 362 | --n1311: red; 363 | --n1312: red; 364 | --n1313: red; 365 | --n1314: red; 366 | --n1315: red; 367 | --n1316: red; 368 | --n1317: red; 369 | --n1318: red; 370 | --n1319: red; 371 | --n1320: red; 372 | --n1321: red; 373 | --n1322: red; 374 | --n1323: red; 375 | --n1324: red; 376 | --n1325: red; 377 | --n1326: red; 378 | --n1327: red; 379 | --n1328: red; 380 | --n1329: red; 381 | --n1330: red; 382 | --n1331: red; 383 | --n1332: red; 384 | --n1333: red; 385 | --n1334: red; 386 | --n1335: red; 387 | --n1336: red; 388 | --n1337: red; 389 | --n1338: red; 390 | --n1339: red; 391 | --n1340: red; 392 | --n1341: red; 393 | --n1342: red; 394 | --n1343: red; 395 | --n1344: red; 396 | --n1345: red; 397 | --n1346: red; 398 | --n1347: red; 399 | --n1348: red; 400 | --n1349: red; 401 | --n1350: red; 402 | --n1351: red; 403 | --n1352: red; 404 | --n1353: red; 405 | --n1354: red; 406 | --n1355: red; 407 | --n1356: red; 408 | --n1357: red; 409 | --n1358: red; 410 | --n1359: red; 411 | --n1360: red; 412 | --n1361: red; 413 | --n1362: red; 414 | --n1363: red; 415 | --n1364: red; 416 | --n1365: red; 417 | --n1366: red; 418 | --n1367: red; 419 | --n1368: red; 420 | --n1369: red; 421 | --n1370: red; 422 | --n1371: red; 423 | --n1372: red; 424 | --n1373: red; 425 | --n1374: red; 426 | --n1375: red; 427 | --n1376: red; 428 | --n1377: red; 429 | --n1378: red; 430 | --n1379: red; 431 | --n1380: red; 432 | --n1381: red; 433 | --n1382: red; 434 | --n1383: red; 435 | --n1384: red; 436 | --n1385: red; 437 | --n1386: red; 438 | --n1387: red; 439 | --n1388: red; 440 | --n1389: red; 441 | --n1390: red; 442 | --n1391: red; 443 | --n1392: red; 444 | --n1393: red; 445 | --n1394: red; 446 | --n1395: red; 447 | --n1396: red; 448 | --n1397: red; 449 | --n1398: red; 450 | --n1399: red; 451 | --n1400: red; 452 | --n1401: red; 453 | --n1402: red; 454 | --n1403: red; 455 | --n1404: red; 456 | --n1405: red; 457 | --n1406: red; 458 | --n1407: red; 459 | --n1408: red; 460 | --n1409: red; 461 | --n1410: red; 462 | --n1411: red; 463 | --n1412: red; 464 | --n1413: red; 465 | --n1414: red; 466 | --n1415: red; 467 | --n1416: red; 468 | --n1417: red; 469 | --n1418: red; 470 | --n1419: red; 471 | --n1420: red; 472 | --n1421: red; 473 | --n1422: red; 474 | --n1423: red; 475 | --n1424: red; 476 | --n1425: red; 477 | --n1426: red; 478 | --n1427: red; 479 | --n1428: red; 480 | --n1429: red; 481 | --n1430: red; 482 | --n1431: red; 483 | --n1432: red; 484 | --n1433: red; 485 | --n1434: red; 486 | --n1435: red; 487 | --n1436: red; 488 | --n1437: red; 489 | --n1438: red; 490 | --n1439: red; 491 | --n1440: red; 492 | --n1441: red; 493 | --n1442: red; 494 | --n1443: red; 495 | --n1444: red; 496 | --n1445: red; 497 | --n1446: red; 498 | --n1447: red; 499 | --n1448: red; 500 | --n1449: red; 501 | --n1450: red; 502 | --n1451: red; 503 | --n1452: red; 504 | --n1453: red; 505 | --n1454: red; 506 | --n1455: red; 507 | --n1456: red; 508 | --n1457: red; 509 | --n1458: red; 510 | --n1459: red; 511 | --n1460: red; 512 | --n1461: red; 513 | --n1462: red; 514 | --n1463: red; 515 | --n1464: red; 516 | --n1465: red; 517 | --n1466: red; 518 | --n1467: red; 519 | --n1468: red; 520 | --n1469: red; 521 | --n1470: red; 522 | --n1471: red; 523 | --n1472: red; 524 | --n1473: red; 525 | --n1474: red; 526 | --n1475: red; 527 | --n1476: red; 528 | --n1477: red; 529 | --n1478: red; 530 | --n1479: red; 531 | --n1480: red; 532 | --n1481: red; 533 | --n1482: red; 534 | --n1483: red; 535 | --n1484: red; 536 | --n1485: red; 537 | --n1486: red; 538 | --n1487: red; 539 | --n1488: red; 540 | --n1489: red; 541 | --n1490: red; 542 | --n1491: red; 543 | --n1492: red; 544 | --n1493: red; 545 | --n1494: red; 546 | --n1495: red; 547 | --n1496: red; 548 | --n1497: red; 549 | --n1498: red; 550 | --n1499: red; 551 | --n1500: red; 552 | --n1501: red; 553 | --n1502: red; 554 | --n1503: red; 555 | --n1504: red; 556 | --n1505: red; 557 | --n1506: red; 558 | --n1507: red; 559 | --n1508: red; 560 | --n1509: red; 561 | --n1510: red; 562 | --n1511: red; 563 | --n1512: red; 564 | --n1513: red; 565 | --n1514: red; 566 | --n1515: red; 567 | --n1516: red; 568 | --n1517: red; 569 | --n1518: red; 570 | --n1519: red; 571 | --n1520: red; 572 | --n1521: red; 573 | --n1522: red; 574 | --n1523: red; 575 | --n1524: red; 576 | --n1525: red; 577 | --n1526: red; 578 | --n1527: red; 579 | --n1528: red; 580 | --n1529: red; 581 | --n1530: red; 582 | --n1531: red; 583 | --n1532: red; 584 | --n1533: red; 585 | --n1534: red; 586 | --n1535: red; 587 | --n1536: red; 588 | --n1537: red; 589 | --n1538: red; 590 | --n1539: red; 591 | --n1540: red; 592 | --n1541: red; 593 | --n1542: red; 594 | --n1543: red; 595 | --n1544: red; 596 | --n1545: red; 597 | --n1546: red; 598 | --n1547: red; 599 | --n1548: red; 600 | --n1549: red; 601 | --n1550: red; 602 | --n1551: red; 603 | --n1552: red; 604 | --n1553: red; 605 | --n1554: red; 606 | --n1555: red; 607 | --n1556: red; 608 | --n1557: red; 609 | --n1558: red; 610 | --n1559: red; 611 | --n1560: red; 612 | --n1561: red; 613 | --n1562: red; 614 | --n1563: red; 615 | --n1564: red; 616 | --n1565: red; 617 | --n1566: red; 618 | --n1567: red; 619 | --n1568: red; 620 | --n1569: red; 621 | --n1570: red; 622 | --n1571: red; 623 | --n1572: red; 624 | --n1573: red; 625 | --n1574: red; 626 | --n1575: red; 627 | --n1576: red; 628 | --n1577: red; 629 | --n1578: red; 630 | --n1579: red; 631 | --n1580: red; 632 | --n1581: red; 633 | --n1582: red; 634 | --n1583: red; 635 | --n1584: red; 636 | --n1585: red; 637 | --n1586: red; 638 | --n1587: red; 639 | --n1588: red; 640 | --n1589: red; 641 | --n1590: red; */ 642 | 643 | --hex: #37dd35; 644 | --gradient: linear-gradient(90deg, #f00 0%, #0f0 50%, #f00 100%); 645 | --gradient-with-var: linear-gradient(90deg, var(--brand) 0%, #f00 100%); 646 | 647 | /** 648 | * @tokens Easings 649 | * @presenter Easing 650 | */ 651 | 652 | --easing-swift: cubic-bezier(0.55, 0, 0.1, 1); 653 | 654 | /** 655 | * @tokens Font Families 656 | * @presenter FontFamily 657 | */ 658 | --base-font-family: system-ui, sans-serif; 659 | --mono-font-family: Monaco, monospace; 660 | --web-font-family: "Open Sans"; 661 | 662 | /** 663 | * @tokens Font Sizes 664 | * @presenter FontSize 665 | */ 666 | 667 | --font-size-s: 0.75rem; 668 | --font-size-m: 0.875rem; 669 | --font-size-l: 1rem; 670 | 671 | /** 672 | * @tokens Font Weights 673 | * @presenter FontWeight 674 | */ 675 | 676 | --font-weight-regular: 400; 677 | --font-weight-bold: 600; 678 | 679 | /** 680 | * @tokens Line Heights 681 | * @presenter LineHeight 682 | */ 683 | 684 | --line-height: 1.5; 685 | 686 | /** 687 | * @tokens Letter Spacings 688 | * @presenter LetterSpacing 689 | */ 690 | 691 | --letter-spacing-none: 0; 692 | --letter-spacing-s: 1px; 693 | --letter-spacing-m: 2px; 694 | 695 | /** 696 | * @tokens Opacities 697 | * @presenter Opacity 698 | */ 699 | 700 | --opacity: 0.5; 701 | 702 | /** 703 | * @tokens Shadows 704 | * @presenter Shadow 705 | */ 706 | 707 | --shadow-m: 0 1px 3px rgba(0, 0, 0, 0.2); 708 | --shadow-l: 0 1px 5px rgba(0, 0, 0, 0.3); 709 | 710 | /** 711 | * @tokens Spacings 712 | * @presenter Spacing 713 | */ 714 | 715 | --spacing-s: 8px; 716 | --spacing-m: 12px; 717 | --spacing-l: 16px; 718 | 719 | /** 720 | * @tokens Z-Index 721 | */ 722 | 723 | --z-index: 1000; 724 | 725 | /** 726 | * @tokens Others 727 | */ 728 | --border-normal: 3px dashed red; /* Token Description Example @presenter Border */ 729 | } 730 | 731 | @keyframes rotate { 732 | from { 733 | transform: rotate(0deg); 734 | } 735 | to { 736 | transform: rotate(360deg); 737 | } 738 | } 739 | --------------------------------------------------------------------------------