From 14b252ef8d4428869be3fba7a5e26e474a88b1bf Mon Sep 17 00:00:00 2001 From: Sean Kinread Date: Fri, 6 Dec 2024 10:22:59 +1100 Subject: [PATCH] Feat: Document tokens in Storybook (#1008) * feat: elevations, space scale, colour palette and themed colour stories * feat: prototype new Box and Stack with sprinkles and intrinsic attributes --- .prettierignore | 1 + .storybook/main.js | 2 +- .storybook/preview.jsx | 2 +- lib/stories/borders.stories.tsx | 105 ++++++++++++++++++++++ lib/stories/helpers/index.tsx | 70 +++++++++++++++ lib/stories/helpers/styles.css.ts | 54 ++++++++++++ lib/stories/palette.stories.tsx | 87 ++++++++++++++++++ lib/stories/space.stories.tsx | 70 +++++++++++++++ lib/stories/theme.stories.tsx | 126 +++++++++++++++++++++++++++ {stories => lib/stories}/welcome.mdx | 2 +- lib/styles/sprinkles.css.ts | 79 +++++++++++++++++ lib/styles/stack.css.ts | 41 +++++++++ lib/themes/base/tokens.ts | 2 +- lib/themes/tokens.ts | 22 +++-- package.json | 4 +- yarn.lock | 20 +++++ 16 files changed, 676 insertions(+), 11 deletions(-) create mode 100644 lib/stories/borders.stories.tsx create mode 100644 lib/stories/helpers/index.tsx create mode 100644 lib/stories/helpers/styles.css.ts create mode 100644 lib/stories/palette.stories.tsx create mode 100644 lib/stories/space.stories.tsx create mode 100644 lib/stories/theme.stories.tsx rename {stories => lib/stories}/welcome.mdx (72%) create mode 100644 lib/styles/sprinkles.css.ts create mode 100644 lib/styles/stack.css.ts diff --git a/.prettierignore b/.prettierignore index a50cf9583..0f82310b9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,6 +3,7 @@ !/jest/ !/lib/ !/scripts/ +!/stories/ !/.github/ !/.storybook/ !/.changeset diff --git a/.storybook/main.js b/.storybook/main.js index 7a262ce22..02b48c652 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,7 +1,7 @@ /** @type { import('@storybook/react-vite').StorybookConfig } */ const config = { stories: [ - '../stories/**/*.mdx', + '../lib/stories/*.@(mdx|jsx|tsx)', '../lib/**/*.stories.@(js|jsx|mjs|ts|tsx)', ], addons: [ diff --git a/.storybook/preview.jsx b/.storybook/preview.jsx index 42e5771c3..f38ccbd1b 100644 --- a/.storybook/preview.jsx +++ b/.storybook/preview.jsx @@ -179,7 +179,7 @@ const preview = { }, options: { storySort: { - order: ['Overdrive'], + order: ['Overdrive', 'Foundation'], }, }, }, diff --git a/lib/stories/borders.stories.tsx b/lib/stories/borders.stories.tsx new file mode 100644 index 000000000..5dffe3dc0 --- /dev/null +++ b/lib/stories/borders.stories.tsx @@ -0,0 +1,105 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import { Heading } from '../components/Heading'; +import { tokens } from '../themes/base/tokens'; + +import { Box, Stack, type Sprinkles } from './helpers'; +import { labels, titles } from './helpers/styles.css'; + +const { border, elevation } = tokens; +const elevationItems = Object.keys(elevation); +const widthItems = Object.keys(border.width); +const radiusItems = Object.keys(border.radius); + +const Elevation = () => ( + + + Elevation + + + {elevationItems.map((elevation) => ( + + +

{elevation}

+
+ ))} +
+); + +const Widths = () => { + return ( + + + Width + + + {widthItems.map((width) => ( + + +

{width}

+
+ ))} +
+ ); +}; + +const Radius = () => { + return ( + + + Radius + + + {radiusItems.sort().map((radius) => ( + + +

{radius}

+
+ ))} +
+ ); +}; + +const meta: Meta = { + title: 'Foundation/Borders', + tags: ['!autodocs'], +}; +export default meta; + +type Story = StoryObj; + +export const Borders: Story = { + render: () => ( + + + Borders & +
+ Elevation +
+ + + +
+ ), +}; diff --git a/lib/stories/helpers/index.tsx b/lib/stories/helpers/index.tsx new file mode 100644 index 000000000..9fe32833b --- /dev/null +++ b/lib/stories/helpers/index.tsx @@ -0,0 +1,70 @@ +import clsx from 'clsx'; +import React from 'react'; + +import { sprinkles, type Sprinkles } from '../../styles/sprinkles.css'; +import { stack, type RecipeStackProps } from '../../styles/stack.css'; + +import { + variantColourSwatch, + type VariantColourSwatchProps, +} from './styles.css'; + +type ElementAttributes = React.ComponentPropsWithoutRef<'div'>; +type FilteredAttributes = Pick; +type ComponentProps

= React.PropsWithChildren

; + +export const Box = ({ + children, + className, + style, + ...props +}: ComponentProps) => ( +

+ {children} +
+); + +type StackSprinkles = Pick< + Sprinkles, + 'alignItems' | 'flexDirection' | 'flexWrap' | 'gap' | 'justifyContent' +>; +export const Stack = ({ + alignItems, + children, + className, + flexDirection, + flexWrap, + gap, + horizontal, + justifyContent, + space, + style, + ...props +}: ComponentProps) => ( +
+ {children} +
+); + +type ColourSwatchProps = ComponentProps< + Omit & VariantColourSwatchProps +>; +export const ColourSwatch = ({ shape, size, ...props }: ColourSwatchProps) => ( + +); + +export { type Sprinkles } from '../../styles/sprinkles.css'; diff --git a/lib/stories/helpers/styles.css.ts b/lib/stories/helpers/styles.css.ts new file mode 100644 index 000000000..6b1065566 --- /dev/null +++ b/lib/stories/helpers/styles.css.ts @@ -0,0 +1,54 @@ +import { style } from '@vanilla-extract/css'; +import { recipe, type RecipeVariants } from '@vanilla-extract/recipes'; + +import { themeContractVars as vars } from '../../themes/theme.css'; + +export const titles = style({ + marginTop: '11px', +}); + +export const labels = style({ + textTransform: 'capitalize', +}); + +export const small = style({ + fontSize: 'small', +}); + +export const hexPill = style({ + backgroundColor: 'hsl(0 0 100 / 75%)', + borderRadius: '100px', + display: 'inline-block', + fontSize: '11px', + padding: '1px 8px', + position: 'absolute', + textTransform: 'uppercase', + top: '5px', +}); + +export const variantColourSwatch = recipe({ + base: { + position: 'relative', + borderColor: vars.border.colours.light, + borderStyle: 'solid', + borderWidth: vars.border.width[1], + }, + variants: { + size: { + sm: { height: vars.space[7], width: vars.space[7] }, + md: { height: vars.space[8], width: vars.space[8] }, + lg: { height: vars.space[9], width: vars.space[9] }, + }, + shape: { + circle: { borderRadius: '100%' }, + }, + }, + defaultVariants: { + size: 'md', + shape: 'circle', + }, +}); + +export type VariantColourSwatchProps = NonNullable< + RecipeVariants +>; diff --git a/lib/stories/palette.stories.tsx b/lib/stories/palette.stories.tsx new file mode 100644 index 000000000..6de77ae8a --- /dev/null +++ b/lib/stories/palette.stories.tsx @@ -0,0 +1,87 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import clsx from 'clsx'; +import React from 'react'; + +import { Heading } from '../components/Heading'; +import { sprinkles } from '../styles/sprinkles.css'; +import { baseThemeColours } from '../themes/base/tokens'; +import type { ColourGamut, ColourValue } from '../themes/tokens'; + +import { ColourSwatch, Stack } from './helpers'; +import { labels, hexPill } from './helpers/styles.css'; + +interface SwatchProps { + colour: ColourGamut; + hex?: string; + hue?: string; +} +const Swatch = ({ colour, hex, hue }: SwatchProps) => ( + + +
{hex}
+
+ {hue && colour.replace(hue, '')} +
+); + +interface PaletteSwatchesProps { + hue: string; + shades: ColourValue; +} +const PaletteSwatches = ({ hue, shades }: PaletteSwatchesProps) => ( + + {Object.entries(shades) + .reverse() + .map(([colour, hex]) => ( + + ))} + +); + +const Palettes = () => ( + + {['green', 'blue', 'yellow', 'red', 'gray', 'black'].map((hue) => ( +
+ + {hue} + + +
+ ))} +
+); + +const meta: Meta = { + title: 'Foundation/Palette', + tags: ['!autodocs'], +}; +export default meta; + +type Story = StoryObj; + +export const Palette: Story = { + render: () => { + return ( + + Palette + Colours + + + ); + }, +}; diff --git a/lib/stories/space.stories.tsx b/lib/stories/space.stories.tsx new file mode 100644 index 000000000..555ef52f2 --- /dev/null +++ b/lib/stories/space.stories.tsx @@ -0,0 +1,70 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import { Heading } from '../components/Heading'; +import { tokens } from '../themes/base/tokens'; +import { breakpoints } from '../themes/makeTheme'; + +import { Box, Stack, type Sprinkles } from './helpers'; +import { labels, small, titles } from './helpers/styles.css'; + +const { space } = tokens; +const spaceItems = Object.keys(space).filter((val) => val !== 'none'); + +const SpaceScale = () => ( + + + Space scale + + + {spaceItems.map((space) => ( + +

{space}

+ +

{tokens.space[space]}

+
+ ))} +
+); + +const breakpointItems = Object.entries(breakpoints); +const Breakpoints = () => { + return ( + + + Breakpoints + + + {breakpointItems.map(([name, width], idx) => ( +
+ {name}: {width} + {idx < breakpointItems.length - 1 + ? ` to ${breakpointItems[idx + 1][1]}` + : ' and up'} +
+ ))} +
+ ); +}; + +const meta: Meta = { + title: 'Foundation/Space', + tags: ['!autodocs'], +}; +export default meta; + +type Story = StoryObj; + +export const Space: Story = { + render: () => ( + + Space + + + + ), +}; diff --git a/lib/stories/theme.stories.tsx b/lib/stories/theme.stories.tsx new file mode 100644 index 000000000..1125dd216 --- /dev/null +++ b/lib/stories/theme.stories.tsx @@ -0,0 +1,126 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import { Heading } from '../components/Heading'; +import { themeContractVars } from '../themes/theme.css'; + +import { Stack } from './helpers'; +import { labels, variantColourSwatch } from './helpers/styles.css'; + +const ThemeSwatch = ({ label, cssVar }) => ( + +
+ {label} +
+); + +const SemanticSwatches = ({ vars }: { vars: Record }) => ( + + {Object.entries(vars).map(([colour, cssVar]) => ( + +
+ {colour} +
+ ))} +
+); + +const IntentionalSwatches = ({ + vars, +}: { + vars: { + background: { mild: string; standard: string; strong: string }; + foreground: string; + border: string; + }; +}) => ( + + + + + + + + + +); + +const meta: Meta = { + title: 'Foundation/Theme Colours', + tags: ['!autodocs'], +}; +export default meta; + +type Story = StoryObj; + +export const ThemeColours: Story = { + render: () => { + return ( + +
+ Theme Colours +

+ Use the theme selection menu options in the top bar to + view alternate colour mappings. +

+
+ + Body + + + + Foreground + + + + Background + + + + Border + + + + + Intentional colour sets + + {Object.entries(themeContractVars.colours.intent).map( + ([title, vars]) => ( + <> + + {title} + + + + ), + )} + + +
+ ); + }, +}; diff --git a/stories/welcome.mdx b/lib/stories/welcome.mdx similarity index 72% rename from stories/welcome.mdx rename to lib/stories/welcome.mdx index 0620dc403..605ec17b8 100644 --- a/stories/welcome.mdx +++ b/lib/stories/welcome.mdx @@ -1,6 +1,6 @@ import { Markdown, Meta } from '@storybook/blocks'; -import Readme from '../readme.md?raw'; +import Readme from '../../readme.md?raw'; diff --git a/lib/styles/sprinkles.css.ts b/lib/styles/sprinkles.css.ts new file mode 100644 index 000000000..3068eef02 --- /dev/null +++ b/lib/styles/sprinkles.css.ts @@ -0,0 +1,79 @@ +import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles'; + +import { tokens } from '../themes/base/tokens'; +const { border, colours, elevation, space } = tokens; +const { none, ...spaceWithoutNone } = space; + +const responsiveProperties = defineProperties({ + properties: { + gap: space, + marginBottom: space, + marginLeft: space, + marginRight: space, + marginTop: space, + paddingBottom: space, + paddingLeft: space, + paddingRight: space, + paddingTop: space, + }, + shorthands: { + margin: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'], + marginX: ['marginLeft', 'marginRight'], + marginY: ['marginTop', 'marginBottom'], + padding: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], + paddingX: ['paddingLeft', 'paddingRight'], + paddingY: ['paddingTop', 'paddingBottom'], + }, +}); + +const borderProperties = defineProperties({ + properties: { + borderColor: { ...border.colours }, + borderRadius: { ...border.radius }, + borderStyle: ['solid', 'dotted', 'dashed'], + borderWidth: { ...border.width }, + boxShadow: { ...elevation }, + }, +}); + +const displayProperties = defineProperties({ + properties: { + display: ['none', 'block', 'flex'], + flexDirection: ['row', 'column'], + flexWrap: ['nowrap', 'wrap', 'wrap-reverse'], + alignItems: ['stretch', 'flex-start', 'center', 'flex-end'], + justifyContent: ['stretch', 'flex-start', 'center', 'flex-end'], + height: { + ...spaceWithoutNone, + '100%': '100%', + auto: 'auto', + }, + width: { + ...spaceWithoutNone, + '100%': '100%', + auto: 'auto', + }, + }, + shorthands: { + placeItems: ['justifyContent', 'alignItems'], + size: ['width', 'height'], + }, +}); + +const gamutProperties = defineProperties({ + properties: { + color: colours.gamut, + background: colours.gamut, + fill: colours.gamut, + stroke: colours.gamut, + }, +}); + +export const sprinkles = createSprinkles( + responsiveProperties, + borderProperties, + displayProperties, + gamutProperties, +); + +export type Sprinkles = Parameters[0]; diff --git a/lib/styles/stack.css.ts b/lib/styles/stack.css.ts new file mode 100644 index 000000000..704f0d41c --- /dev/null +++ b/lib/styles/stack.css.ts @@ -0,0 +1,41 @@ +import { recipe, type RecipeVariants } from '@vanilla-extract/recipes'; + +import { sprinkles } from './sprinkles.css'; + +export const stack = recipe({ + base: sprinkles({ display: 'flex', flexWrap: 'wrap' }), + variants: { + space: { + sm: sprinkles({ gap: '5' }), + md: sprinkles({ gap: '8' }), + lg: sprinkles({ gap: '9' }), + }, + // space: Object.fromEntries( + // Object.entries(tokens.space).map(([key, val]) => [ + // key, + // { gap: val }, + // ]), + // ), + horizontal: { + false: sprinkles({ flexDirection: 'column' }), + true: sprinkles({ flexDirection: 'row' }), + }, + }, + defaultVariants: { + horizontal: false, + space: 'md', + }, +}); + +type Variants = NonNullable>; + +export interface RecipeStackProps { + /** + * Control the gap spacing between items + */ + space?: Variants['space']; + /** + * Change the orientation + */ + horizontal?: Variants['horizontal']; +} diff --git a/lib/themes/base/tokens.ts b/lib/themes/base/tokens.ts index f3d94212a..ceda94cd2 100644 --- a/lib/themes/base/tokens.ts +++ b/lib/themes/base/tokens.ts @@ -1,7 +1,7 @@ import { buildColourGamut } from '../makeTheme'; import { ColourMap, Tokens } from '../tokens'; -const baseThemeColours: ColourMap = { +export const baseThemeColours: ColourMap = { black: { '900': '#222222', '800': '#2A2C2A', diff --git a/lib/themes/tokens.ts b/lib/themes/tokens.ts index 48c8c601a..2c2b6acf9 100644 --- a/lib/themes/tokens.ts +++ b/lib/themes/tokens.ts @@ -1,14 +1,24 @@ type VanillaTokens = { [key: string]: string | VanillaTokens; }; -type SpaceScale = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'none'; -type TextSizeScale = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; -type BorderWidthScale = '1' | '2' | '3' | 'none'; -type IconSizeScale = 'small' | 'medium' | 'large'; +export type SpaceScale = + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9' + | 'none'; +export type TextSizeScale = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; +export type BorderWidthScale = '1' | '2' | '3' | 'none'; +export type IconSizeScale = 'small' | 'medium' | 'large'; -type DeviceSize = 'mobile' | 'tablet' | 'desktop' | 'largeDesktop'; +export type DeviceSize = 'mobile' | 'tablet' | 'desktop' | 'largeDesktop'; -type ColourValue = Record; +export type ColourValue = Record; interface ColourIntensityMap extends VanillaTokens { standard: string; diff --git a/package.json b/package.json index 9d9645a0b..af39959df 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "access": "public" }, "scripts": { - "build": "babel lib --out-dir dist --extensions '.ts,.tsx,.css' --ignore 'lib/**/*.stories.tsx'", + "build": "babel lib --out-dir dist --extensions '.ts,.tsx,.css' --ignore 'lib/**/*.stories.tsx,lib/stories'", "chromatic": "chromatic test --exit-zero-on-changes --build-script-name storybook:build", "copy:public": "node scripts/copyPublic.js", "check-deps": "npx npm-check-updates@latest --interactive --format group", @@ -108,6 +108,8 @@ "@vanilla-extract/babel-plugin": "^1.2.0", "@vanilla-extract/css": "^1.16.1", "@vanilla-extract/dynamic": "^2.1.2", + "@vanilla-extract/recipes": "^0.5.5", + "@vanilla-extract/sprinkles": "^1.6.3", "@vanilla-extract/vite-plugin": "^4.0.18", "@vanilla-extract/webpack-plugin": "^2.3.15", "babel-plugin-add-import-extension": "^1.6.0", diff --git a/yarn.lock b/yarn.lock index e74ccc45d..1842b2f5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -147,6 +147,8 @@ __metadata: "@vanilla-extract/babel-plugin": "npm:^1.2.0" "@vanilla-extract/css": "npm:^1.16.1" "@vanilla-extract/dynamic": "npm:^2.1.2" + "@vanilla-extract/recipes": "npm:^0.5.5" + "@vanilla-extract/sprinkles": "npm:^1.6.3" "@vanilla-extract/vite-plugin": "npm:^4.0.18" "@vanilla-extract/webpack-plugin": "npm:^2.3.15" babel-plugin-add-import-extension: "npm:^1.6.0" @@ -6013,6 +6015,24 @@ __metadata: languageName: node linkType: hard +"@vanilla-extract/recipes@npm:^0.5.5": + version: 0.5.5 + resolution: "@vanilla-extract/recipes@npm:0.5.5" + peerDependencies: + "@vanilla-extract/css": ^1.0.0 + checksum: 10/8d2b4f8163369424226ec9a47e754002b8a095bcf86c1a60a91b2183f59508519bd31ed41baefc950ad7ca225d75b3184c3b84d3c741c5c60d91618dd70452aa + languageName: node + linkType: hard + +"@vanilla-extract/sprinkles@npm:^1.6.3": + version: 1.6.3 + resolution: "@vanilla-extract/sprinkles@npm:1.6.3" + peerDependencies: + "@vanilla-extract/css": ^1.0.0 + checksum: 10/74f8e2b189c0d48e279f1b85b5fedebf1f615ab31839964cf3861f2c5cf574567c0caeddf9c8b11327d81213f81d195efc79f136b725e6013b6d645d238d5c2b + languageName: node + linkType: hard + "@vanilla-extract/vite-plugin@npm:^4.0.18": version: 4.0.18 resolution: "@vanilla-extract/vite-plugin@npm:4.0.18"