diff --git a/packages/theme/bin/terrazzo-plugin-inline-alias-values/index.ts b/packages/theme/bin/terrazzo-plugin-inline-alias-values/index.ts index 12d145013a4092..33f4aad331fed2 100644 --- a/packages/theme/bin/terrazzo-plugin-inline-alias-values/index.ts +++ b/packages/theme/bin/terrazzo-plugin-inline-alias-values/index.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { type Plugin } from '@terrazzo/parser'; +import { type Plugin, type TokenNormalized } from '@terrazzo/parser'; interface InlineAliasValuesOptions { /** @@ -40,25 +40,54 @@ export default function inlineAliasValues( { return { name: '@wordpress/terrazzo-plugin-inline-alias-values', async transform( { tokens } ) { - Object.keys( tokens ) - .filter( ( id ) => pattern.test( id ) ) - .forEach( ( id ) => { - const token = tokens[ id ]; + // Map of primitive token ID -> array of references to inline + const inlineMap: Record< string, TokenNormalized[] > = {}; + + // Single pass: identify primitives and collect references + for ( const [ id, token ] of Object.entries( tokens ) ) { + const shouldInline = pattern.test( id ); + + if ( shouldInline ) { + // Track this token for inlining + inlineMap[ id ] = []; + + // Track aliased tokens for output file if ( token.aliasedBy ) { aliasedBy[ tokenId( id ) ] = token.aliasedBy.map( tokenId ); + } + } + + // Check if this token's main value references a primitive + if ( token.aliasOf && pattern.test( token.aliasOf ) ) { + inlineMap[ token.aliasOf ] ??= []; + inlineMap[ token.aliasOf ].push( token ); + } - for ( const aliasedId of token.aliasedBy ) { - Object.assign( tokens[ aliasedId ], { - $value: token.$value, - mode: token.mode, - originalValue: token.originalValue, - } ); - } + // Check if any mode values reference a primitive + for ( const modeValue of Object.values( token.mode ) ) { + const { aliasOf } = modeValue; + if ( aliasOf && pattern.test( aliasOf ) ) { + const primitiveId = aliasOf; + inlineMap[ primitiveId ] ??= []; + inlineMap[ primitiveId ].push( modeValue ); } + } + } + + // Inline values and delete primitives + for ( const [ id, references ] of Object.entries( inlineMap ) ) { + const token = tokens[ id ]; - delete tokens[ id ]; - } ); + for ( const target of references ) { + target.$value = token.$value; + target.originalValue = token.originalValue; + delete target.aliasOf; + delete target.aliasChain; + } + + delete tokens[ id ]; + } }, async build( { outputFile } ) { if ( ! filename ) { diff --git a/packages/theme/docs/ds-tokens.md b/packages/theme/docs/ds-tokens.md index 9148ba1bea01fa..e2d2464244d499 100644 --- a/packages/theme/docs/ds-tokens.md +++ b/packages/theme/docs/ds-tokens.md @@ -111,13 +111,14 @@ Do not edit directly. ### Dimension -| Variable name | Description | -| ------------------------------------------ | -------------------------------- | -| `--wpds-dimension-base` | Base dimension unit | -| `--wpds-dimension-padding-surface-x-small` | Extra small spacing for surfaces | -| `--wpds-dimension-padding-surface-small` | Small spacing for surfaces | -| `--wpds-dimension-padding-surface-medium` | Medium spacing for surfaces | -| `--wpds-dimension-padding-surface-large` | Large spacing for surfaces | +| Variable name | Description | +| -------------------------------------- | ----------------------------------- | +| `--wpds-dimension-base` | Base dimension unit | +| `--wpds-dimension-padding-surface-2xs` | 2x extra small spacing for surfaces | +| `--wpds-dimension-padding-surface-xs` | Extra small spacing for surfaces | +| `--wpds-dimension-padding-surface-sm` | Small spacing for surfaces | +| `--wpds-dimension-padding-surface-md` | Medium spacing for surfaces | +| `--wpds-dimension-padding-surface-lg` | Large spacing for surfaces | ### Elevation diff --git a/packages/theme/src/prebuilt/css/design-tokens.css b/packages/theme/src/prebuilt/css/design-tokens.css index b54fdaedf2ba40..9c575baa96f3e2 100644 --- a/packages/theme/src/prebuilt/css/design-tokens.css +++ b/packages/theme/src/prebuilt/css/design-tokens.css @@ -96,10 +96,11 @@ --wpds-color-stroke-surface-warning: #d2b581; /* Decorative stroke color used to define warning-toned surface boundaries with normal emphasis. */ --wpds-color-stroke-surface-warning-strong: #936400; /* Decorative stroke color used to define warning-toned surface boundaries with strong emphasis. */ --wpds-dimension-base: 4px; /* Base dimension unit */ - --wpds-dimension-padding-surface-large: 24px; /* Large spacing for surfaces */ - --wpds-dimension-padding-surface-medium: 16px; /* Medium spacing for surfaces */ - --wpds-dimension-padding-surface-small: 12px; /* Small spacing for surfaces */ - --wpds-dimension-padding-surface-x-small: 8px; /* Extra small spacing for surfaces */ + --wpds-dimension-padding-surface-2xs: 4px; /* 2x extra small spacing for surfaces */ + --wpds-dimension-padding-surface-lg: 32px; /* Large spacing for surfaces */ + --wpds-dimension-padding-surface-md: 24px; /* Medium spacing for surfaces */ + --wpds-dimension-padding-surface-sm: 16px; /* Small spacing for surfaces */ + --wpds-dimension-padding-surface-xs: 8px; /* Extra small spacing for surfaces */ --wpds-elevation-large: 0 5px 15px 0 #00000014, 0 15px 27px 0 #00000012, 0 30px 36px 0 #0000000a, 0 50px 43px 0 #00000005; /* For components that confirm decisions or handle necessary interruptions. Example: Modals. */ --wpds-elevation-medium: 0 2px 3px 0 #0000000d, 0 4px 5px 0 #0000000a, @@ -127,6 +128,31 @@ --wpds-font-size-x-small: 11px; /* Extra small font size */ } +[data-wpds-theme-provider-id][data-wpds-density='default'] { + --wpds-dimension-base: 4px; /* Base dimension unit */ + --wpds-dimension-padding-surface-2xs: 4px; /* 2x extra small spacing for surfaces */ + --wpds-dimension-padding-surface-lg: 32px; /* Large spacing for surfaces */ + --wpds-dimension-padding-surface-md: 24px; /* Medium spacing for surfaces */ + --wpds-dimension-padding-surface-sm: 16px; /* Small spacing for surfaces */ + --wpds-dimension-padding-surface-xs: 8px; /* Extra small spacing for surfaces */ +} + +[data-wpds-theme-provider-id][data-wpds-density='compact'] { + --wpds-dimension-padding-surface-2xs: 4px; /* 2x extra small spacing for surfaces */ + --wpds-dimension-padding-surface-lg: 24px; /* Large spacing for surfaces */ + --wpds-dimension-padding-surface-md: 20px; /* Medium spacing for surfaces */ + --wpds-dimension-padding-surface-sm: 12px; /* Small spacing for surfaces */ + --wpds-dimension-padding-surface-xs: 4px; /* Extra small spacing for surfaces */ +} + +[data-wpds-theme-provider-id][data-wpds-density='comfortable'] { + --wpds-dimension-padding-surface-2xs: 8px; /* 2x extra small spacing for surfaces */ + --wpds-dimension-padding-surface-lg: 40px; /* Large spacing for surfaces */ + --wpds-dimension-padding-surface-md: 32px; /* Medium spacing for surfaces */ + --wpds-dimension-padding-surface-sm: 20px; /* Small spacing for surfaces */ + --wpds-dimension-padding-surface-xs: 12px; /* Extra small spacing for surfaces */ +} + @media ( -webkit-min-device-pixel-ratio: 2 ), ( min-resolution: 192dpi ) { :root { --wpds-border-width-focus: 1.5px; /* Border width for focus ring */ diff --git a/packages/theme/src/prebuilt/js/design-tokens.js b/packages/theme/src/prebuilt/js/design-tokens.js index 8cf736c9760a6c..e5bb76c76005d1 100644 --- a/packages/theme/src/prebuilt/js/design-tokens.js +++ b/packages/theme/src/prebuilt/js/design-tokens.js @@ -97,10 +97,11 @@ export default [ '--wpds-color-stroke-interactive-error-strong', '--wpds-color-stroke-focus-brand', '--wpds-dimension-base', - '--wpds-dimension-padding-surface-x-small', - '--wpds-dimension-padding-surface-small', - '--wpds-dimension-padding-surface-medium', - '--wpds-dimension-padding-surface-large', + '--wpds-dimension-padding-surface-2xs', + '--wpds-dimension-padding-surface-xs', + '--wpds-dimension-padding-surface-sm', + '--wpds-dimension-padding-surface-md', + '--wpds-dimension-padding-surface-lg', '--wpds-elevation-x-small', '--wpds-elevation-small', '--wpds-elevation-medium', diff --git a/packages/theme/src/prebuilt/json/figma.json b/packages/theme/src/prebuilt/json/figma.json index 07325261569bdd..39b74a0d267213 100644 --- a/packages/theme/src/prebuilt/json/figma.json +++ b/packages/theme/src/prebuilt/json/figma.json @@ -558,27 +558,43 @@ }, "description": "Base dimension unit" }, - "Dimension/Semantic/padding-surface-x-small": { + "Dimension/Semantic/padding-surface-2xs": { "value": { - ".": "8px" + ".": "4px", + "compact": "4px", + "comfortable": "8px" + }, + "description": "2x extra small spacing for surfaces" + }, + "Dimension/Semantic/padding-surface-xs": { + "value": { + ".": "8px", + "compact": "4px", + "comfortable": "12px" }, "description": "Extra small spacing for surfaces" }, - "Dimension/Semantic/padding-surface-small": { + "Dimension/Semantic/padding-surface-sm": { "value": { - ".": "12px" + ".": "16px", + "compact": "12px", + "comfortable": "20px" }, "description": "Small spacing for surfaces" }, - "Dimension/Semantic/padding-surface-medium": { + "Dimension/Semantic/padding-surface-md": { "value": { - ".": "16px" + ".": "24px", + "compact": "20px", + "comfortable": "32px" }, "description": "Medium spacing for surfaces" }, - "Dimension/Semantic/padding-surface-large": { + "Dimension/Semantic/padding-surface-lg": { "value": { - ".": "24px" + ".": "32px", + "compact": "24px", + "comfortable": "40px" }, "description": "Large spacing for surfaces" }, diff --git a/packages/theme/src/stories/index.story.tsx b/packages/theme/src/stories/index.story.tsx index 68f1423f3e84f9..3118dff9efb6d5 100644 --- a/packages/theme/src/stories/index.story.tsx +++ b/packages/theme/src/stories/index.story.tsx @@ -18,6 +18,7 @@ import { * Internal dependencies */ import { ThemeProvider } from '../theme-provider'; +import '../prebuilt/css/design-tokens.css'; const meta: Meta< typeof ThemeProvider > = { title: 'Design System/Theme Provider', @@ -198,10 +199,10 @@ export const WithPicker: StoryObj< typeof ThemeProvider > = { }, }; -const NestingDebug = ( { bg = '', primary = '' } ) => ( +const NestingDebug = ( { bg = '', primary = '', density = '' } ) => (
( gap: '1rem', } } > -
-			bg: { bg } | primary: { primary }
+		
+			bg: { bg } | primary: { primary } | density: { density }
 		
( style={ { display: 'inline-block', marginInlineStart: '0.25rem', - padding: '0.25rem', + padding: 'var(--wpds-dimension-padding-surface-xs)', borderRadius: '0.25rem', backgroundColor: 'var(--wpds-color-bg-interactive-brand-weak-disabled)', @@ -245,27 +246,39 @@ export const NestingAndInheriting: StoryObj< typeof ThemeProvider > = { render: () => { return ( - +
- +
= {
@@ -397,7 +411,7 @@ export const AcrossIframes: StoryObj< typeof ThemeProvider > = { { const instanceId = useId(); @@ -76,6 +77,7 @@ export const ThemeProvider = ( {
diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index f5f20e07a64ef2..49bad28227afbf 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -23,6 +23,15 @@ export interface ThemeProviderSettings { */ bg?: string; }; + + /** + * The density of the theme. If left unspecified, the theme inherits from + * the density of the closest `ThemeProvider`, or uses the default density + * if there is no inherited density. + * + * @default undefined + */ + density?: undefined | 'default' | 'compact' | 'comfortable'; } export interface ThemeProviderProps extends ThemeProviderSettings { diff --git a/packages/theme/terrazzo.config.ts b/packages/theme/terrazzo.config.ts index f7b6829a921461..f2c111228a41b7 100644 --- a/packages/theme/terrazzo.config.ts +++ b/packages/theme/terrazzo.config.ts @@ -40,6 +40,27 @@ export default defineConfig( { makeCSSVar( `wpds.${ publicTokenId( token.id ) }` ), baseSelector: ':root', modeSelectors: [ + { + tokens: [ 'dimension.*' ], + mode: '.', + selectors: [ + "[data-wpds-theme-provider-id][data-wpds-density='default']", + ], + }, + { + tokens: [ 'dimension.*' ], + mode: 'compact', + selectors: [ + "[data-wpds-theme-provider-id][data-wpds-density='compact']", + ], + }, + { + tokens: [ 'dimension.*' ], + mode: 'comfortable', + selectors: [ + "[data-wpds-theme-provider-id][data-wpds-density='comfortable']", + ], + }, { mode: 'high-dpi', selectors: [ diff --git a/packages/theme/tokens/dimension.json b/packages/theme/tokens/dimension.json index 3a1f5edc1897af..37d10449f252a7 100644 --- a/packages/theme/tokens/dimension.json +++ b/packages/theme/tokens/dimension.json @@ -52,21 +52,55 @@ }, "padding": { "surface": { - "x-small": { - "$value": "{dimension.primitive.space.20}", - "$description": "Extra small spacing for surfaces" + "2xs": { + "$value": "{dimension.primitive.space.10}", + "$description": "2x extra small spacing for surfaces", + "$extensions": { + "mode": { + "compact": "{dimension.primitive.space.10}", + "comfortable": "{dimension.primitive.space.20}" + } + } }, - "small": { - "$value": "{dimension.primitive.space.30}", - "$description": "Small spacing for surfaces" + "xs": { + "$value": "{dimension.primitive.space.20}", + "$description": "Extra small spacing for surfaces", + "$extensions": { + "mode": { + "compact": "{dimension.primitive.space.10}", + "comfortable": "{dimension.primitive.space.30}" + } + } }, - "medium": { + "sm": { "$value": "{dimension.primitive.space.40}", - "$description": "Medium spacing for surfaces" + "$description": "Small spacing for surfaces", + "$extensions": { + "mode": { + "compact": "{dimension.primitive.space.30}", + "comfortable": "{dimension.primitive.space.50}" + } + } }, - "large": { + "md": { "$value": "{dimension.primitive.space.60}", - "$description": "Large spacing for surfaces" + "$description": "Medium spacing for surfaces", + "$extensions": { + "mode": { + "compact": "{dimension.primitive.space.50}", + "comfortable": "{dimension.primitive.space.70}" + } + } + }, + "lg": { + "$value": "{dimension.primitive.space.70}", + "$description": "Large spacing for surfaces", + "$extensions": { + "mode": { + "compact": "{dimension.primitive.space.60}", + "comfortable": "{dimension.primitive.space.80}" + } + } } } } diff --git a/packages/ui/src/box/stories/index.story.tsx b/packages/ui/src/box/stories/index.story.tsx index fbfcb421be2469..00ea84e8b33c2b 100644 --- a/packages/ui/src/box/stories/index.story.tsx +++ b/packages/ui/src/box/stories/index.story.tsx @@ -6,27 +6,16 @@ import { type Meta, type StoryObj } from '@storybook/react'; /** * WordPress dependencies */ -import { privateApis } from '@wordpress/theme'; import '@wordpress/theme/design-tokens.css'; // eslint-disable-line no-restricted-syntax /** * Internal dependencies */ import { Box } from '../box'; -import { unlock } from '../../lock-unlock'; - -const { ThemeProvider } = unlock( privateApis ); const meta: Meta< typeof Box > = { title: 'Design System/Components/Box', component: Box, - decorators: [ - ( Story ) => ( - - - - ), - ], tags: [ 'status-experimental' ], }; export default meta; @@ -38,29 +27,28 @@ export const Default: Story = { children: 'Box', backgroundColor: 'info', color: 'info', - padding: 4, + padding: 'sm', }, argTypes: { p: { control: 'select', - options: [ 'x-small', 'small', 'medium', 'large', 1, 2, 3, 4 ], + options: [ '2xs', 'xs', 'sm', 'md', 'lg', 1, 2, 3, 4 ], }, padding: { control: 'select', - options: [ 'x-small', 'small', 'medium', 'large', 1, 2, 3, 4 ], + options: [ '2xs', 'xs', 'sm', 'md', 'lg', 1, 2, 3, 4 ], }, }, }; export const DirectionalPadding: Story = { + ...Default, args: { - children: 'Box', - backgroundColor: 'info', - color: 'info', + ...Default.args, padding: { - blockStart: 'small', - inline: 'medium', - blockEnd: 'large', + blockStart: 'sm', + inline: 'md', + blockEnd: 'lg', }, }, }; diff --git a/packages/ui/src/box/types.ts b/packages/ui/src/box/types.ts index 96716a8330119d..f66226f7dbc217 100644 --- a/packages/ui/src/box/types.ts +++ b/packages/ui/src/box/types.ts @@ -3,7 +3,7 @@ */ import { type ComponentProps } from '../utils/types'; -type SizeToken = 'x-small' | 'small' | 'medium' | 'large'; +type SizeToken = '2xs' | 'xs' | 'sm' | 'md' | 'lg'; type Size = number | SizeToken; diff --git a/storybook/addons/design-system-theme/register.tsx b/storybook/addons/design-system-theme/register.tsx new file mode 100644 index 00000000000000..2d27b12f83ca19 --- /dev/null +++ b/storybook/addons/design-system-theme/register.tsx @@ -0,0 +1,99 @@ +/** + * External dependencies + */ +import { addons, types, useGlobals } from '@storybook/manager-api'; +import { MirrorIcon } from '@storybook/icons'; +import { + IconButton, + WithTooltip, + TooltipMessage, + TooltipLinkList, +} from '@storybook/components'; + +interface ThemeOption { + id: string; + title: string; +} + +interface ThemeTooltipMessageProps { + title: string; + globalName: string; + options: ThemeOption[]; +} + +const ADDON_ID = '@wordpress/storybook-addon-design-system-theme'; + +const COLOR_OPTIONS: ThemeOption[] = [ + { id: '', title: 'Default' }, + { id: 'dark', title: 'Dark' }, +]; + +const DENSITY_OPTIONS: ThemeOption[] = [ + { id: 'compact', title: 'Compact' }, + { id: '', title: 'Default' }, + { id: 'comfortable', title: 'Comfortable' }, +]; + +function ThemeTooltipMessage( { + title, + globalName, + options, +}: ThemeTooltipMessageProps ) { + const [ globals, updateGlobals ] = useGlobals(); + const currentGlobal = globals[ globalName ] ?? ''; + + const links = options.map( ( option ) => ( { + id: option.id, + title: option.title, + active: currentGlobal === option.id, + onClick: () => + updateGlobals( { [ globalName ]: option.id || undefined } ), + } ) ); + + return ( + } + /> + ); +} + +const ThemeTool = () => { + return ( + + + + + } + > + + + Theme + + + ); +}; + +addons.register( ADDON_ID, () => { + addons.add( `${ ADDON_ID }/tool`, { + type: types.TOOL, + title: 'Design System Theme', + match: ( { storyId, viewMode } ) => + !! storyId?.startsWith( 'design-system-components-' ) && + ( [ 'story', 'docs' ] as any[] ).includes( viewMode ), + render: ThemeTool, + } ); +} ); diff --git a/storybook/decorators/with-design-system-theme.tsx b/storybook/decorators/with-design-system-theme.tsx new file mode 100644 index 00000000000000..2d21023dd5dab4 --- /dev/null +++ b/storybook/decorators/with-design-system-theme.tsx @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import type { StoryContext } from '@storybook/types'; + +/** + * WordPress dependencies + */ +import { privateApis as themeApis } from '@wordpress/theme'; +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; + +const { unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', + '@wordpress/theme' +); + +const { ThemeProvider } = unlock( themeApis ); + +/** + * Decorator that applies Design System theme based on toolbar selections. + * + * @param Story - The story component to render + * @param context - The story context + * @return The wrapped story element + */ +export function WithDesignSystemTheme( + Story: React.ComponentType< any >, + context: StoryContext +) { + const isDesignSystemComponentsStory = context.id?.startsWith( + 'design-system-components-' + ); + if ( ! isDesignSystemComponentsStory ) { + return ; + } + + const colorTheme = context.globals.dsColorTheme; + const density = context.globals.dsDensity; + + let color; + if ( colorTheme === 'dark' ) { + color = { bg: '#1e1e1e', primary: '#3858e9' }; + } + + return ( + + + + ); +} diff --git a/storybook/main.js b/storybook/main.js index 3e8a342dce96a8..08e840d78c9984 100644 --- a/storybook/main.js +++ b/storybook/main.js @@ -62,6 +62,7 @@ module.exports = { '@storybook/addon-webpack5-compiler-babel', 'storybook-source-link', '@geometricpanda/storybook-addon-badges', + './addons/design-system-theme/register', ], framework: { name: '@storybook/react-webpack5', diff --git a/storybook/preview.jsx b/storybook/preview.jsx index 4b54beb8d3eba3..16420c5b6da275 100644 --- a/storybook/preview.jsx +++ b/storybook/preview.jsx @@ -18,6 +18,7 @@ import { WithMarginChecker } from './decorators/with-margin-checker'; import { WithMaxWidthWrapper } from './decorators/with-max-width-wrapper'; import { WithRTL } from './decorators/with-rtl'; import { WithTheme } from './decorators/with-theme'; +import { WithDesignSystemTheme } from './decorators/with-design-system-theme'; import badgesConfig from './badges'; export const globalTypes = { @@ -91,6 +92,8 @@ export const globalTypes = { ], }, }, + dsColorTheme: {}, + dsDensity: {}, }; export const decorators = [ @@ -99,6 +102,7 @@ export const decorators = [ WithRTL, WithMaxWidthWrapper, WithTheme, + WithDesignSystemTheme, ]; export const parameters = { diff --git a/storybook/tsconfig.json b/storybook/tsconfig.json new file mode 100644 index 00000000000000..ff8022c0b75067 --- /dev/null +++ b/storybook/tsconfig.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "rootDir": ".", + "noEmit": true + }, + "include": [ "**/*.tsx", "**/*.ts" ] +}