[READ-ONLY] Mirror of https://github.com/lukebennett88/luke-ui. luke-ui.netlify.app/
0

Configure Feed

Select the types of activity you want to include in your feed.

Add token stories and increase print width

Luke Bennett (Mar 4, 2026, 8:49 PM +1100) 55f0900a 5a08591f

+1155 -526
+2 -9
.oxfmtrc.json
··· 20 20 "protocol", 21 21 ["builtin", "external", "type-builtin", "type-external"], 22 22 ["subpath", "internal", "type-subpath", "type-internal"], 23 - [ 24 - "parent", 25 - "sibling", 26 - "index", 27 - "type-parent", 28 - "type-sibling", 29 - "type-index" 30 - ], 23 + ["parent", "sibling", "index", "type-parent", "type-sibling", "type-index"], 31 24 "unknown" 32 25 ], 33 26 "ignoreCase": false, ··· 49 42 "packages/@luke-ui/react/src/styles/composed.css.ts" 50 43 ], 51 44 "jsxSingleQuote": false, 52 - "printWidth": 80, 45 + "printWidth": 100, 53 46 "quoteProps": "as-needed", 54 47 "semi": true, 55 48 "singleAttributePerLine": false,
+2 -9
.oxfmtrc.root.json
··· 14 14 "protocol", 15 15 ["builtin", "external", "type-builtin", "type-external"], 16 16 ["subpath", "internal", "type-subpath", "type-internal"], 17 - [ 18 - "parent", 19 - "sibling", 20 - "index", 21 - "type-parent", 22 - "type-sibling", 23 - "type-index" 24 - ], 17 + ["parent", "sibling", "index", "type-parent", "type-sibling", "type-index"], 25 18 "unknown" 26 19 ], 27 20 "ignoreCase": false, ··· 42 35 "packages/@luke-ui" 43 36 ], 44 37 "jsxSingleQuote": false, 45 - "printWidth": 80, 38 + "printWidth": 100, 46 39 "quoteProps": "as-needed", 47 40 "semi": true, 48 41 "singleAttributePerLine": false,
+1 -4
knip.config.ts
··· 24 24 }, 25 25 'packages/@luke-ui/react': { 26 26 entry: ['src/**/*.stories.tsx', 'src/**/*.docs.mdx'], 27 - ignoreDependencies: [ 28 - '@vitest/coverage-v8', 29 - 'babel-plugin-react-compiler', 30 - ], 27 + ignoreDependencies: ['@vitest/coverage-v8', 'babel-plugin-react-compiler'], 31 28 project: ['src/**/*.{ts,tsx,mdx}'], 32 29 }, 33 30 },
+1 -5
turbo.json
··· 30 30 }, 31 31 "generate": { 32 32 "dependsOn": ["^generate"], 33 - "outputs": [ 34 - ".generated/**", 35 - "dist/spritesheet.svg", 36 - "src/routeTree.gen.ts" 37 - ] 33 + "outputs": [".generated/**", "dist/spritesheet.svg", "src/routeTree.gen.ts"] 38 34 }, 39 35 "dev": { 40 36 "cache": false,
+1 -5
.vscode/extensions.json
··· 1 1 { 2 - "recommendations": [ 3 - "oxc.oxc-vscode", 4 - "unifiedjs.vscode-mdx", 5 - "vitest.explorer" 6 - ] 2 + "recommendations": ["oxc.oxc-vscode", "unifiedjs.vscode-mdx", "vitest.explorer"] 7 3 }
+2 -9
packages/turbo-generators/.oxfmtrc.json
··· 14 14 "protocol", 15 15 ["builtin", "external", "type-builtin", "type-external"], 16 16 ["subpath", "internal", "type-subpath", "type-internal"], 17 - [ 18 - "parent", 19 - "sibling", 20 - "index", 21 - "type-parent", 22 - "type-sibling", 23 - "type-index" 24 - ], 17 + ["parent", "sibling", "index", "type-parent", "type-sibling", "type-index"], 25 18 "unknown" 26 19 ], 27 20 "ignoreCase": false, ··· 40 33 "node_modules" 41 34 ], 42 35 "jsxSingleQuote": false, 43 - "printWidth": 80, 36 + "printWidth": 100, 44 37 "quoteProps": "as-needed", 45 38 "semi": true, 46 39 "singleAttributePerLine": false,
+8 -33
packages/turbo-generators/config.ts
··· 3 3 import { join } from 'node:path'; 4 4 import type { PlopTypes } from '@turbo/gen'; 5 5 6 - const COMPONENT_GROUPS = [ 7 - 'actions', 8 - 'feedback', 9 - 'typography', 10 - 'visuals', 11 - ] as const; 6 + const COMPONENT_GROUPS = ['actions', 'feedback', 'typography', 'visuals'] as const; 12 7 13 8 type GeneratorAnswers = { 14 9 group?: string; ··· 56 51 } 57 52 return parts 58 53 .map((part, index) => { 59 - return index === 0 60 - ? part 61 - : (part[0]?.toUpperCase() ?? '') + part.slice(1); 54 + return index === 0 ? part : (part[0]?.toUpperCase() ?? '') + part.slice(1); 62 55 }) 63 56 .join(''); 64 57 } ··· 83 76 function addComponentToDocsMeta(answers: GeneratorAnswers): string { 84 77 const group = resolveGroup(answers); 85 78 const componentDir = resolveComponentDir(answers); 86 - const groupDir = join( 87 - process.cwd(), 88 - 'apps/docs/content/docs/components', 89 - group, 90 - ); 79 + const groupDir = join(process.cwd(), 'apps/docs/content/docs/components', group); 91 80 const groupMetaPath = join(groupDir, 'meta.json'); 92 81 93 82 mkdirSync(groupDir, { recursive: true }); ··· 108 97 meta.pages = [...pages, componentDir].sort(); 109 98 writeFileSync(groupMetaPath, `${JSON.stringify(meta, null, '\t')}\n`); 110 99 111 - const parentMetaPath = join( 112 - process.cwd(), 113 - 'apps/docs/content/docs/components/meta.json', 114 - ); 100 + const parentMetaPath = join(process.cwd(), 'apps/docs/content/docs/components/meta.json'); 115 101 const parentContent = readFileSync(parentMetaPath, 'utf8'); 116 102 const parentMeta = JSON.parse(parentContent) as { 117 103 pages?: Array<string>; ··· 120 106 const parentPages = Array.isArray(parentMeta.pages) ? parentMeta.pages : []; 121 107 if (!parentPages.includes(group)) { 122 108 parentMeta.pages = [...parentPages, group].sort(); 123 - writeFileSync( 124 - parentMetaPath, 125 - `${JSON.stringify(parentMeta, null, '\t')}\n`, 126 - ); 109 + writeFileSync(parentMetaPath, `${JSON.stringify(parentMeta, null, '\t')}\n`); 127 110 } 128 111 129 112 return `Added ${componentDir} to docs components ${group} meta`; ··· 154 137 function addComponentToGettingStarted(answers: GeneratorAnswers): string { 155 138 const group = resolveGroup(answers); 156 139 const componentDir = resolveComponentDir(answers); 157 - const filePath = join( 158 - process.cwd(), 159 - 'apps/docs/content/docs/getting-started.mdx', 160 - ); 140 + const filePath = join(process.cwd(), 'apps/docs/content/docs/getting-started.mdx'); 161 141 const displayName = toDisplayName(componentDir); 162 142 const nextStepLine = `- Read the [${displayName}](/docs/components/${group}/${componentDir}) docs.`; 163 143 ··· 167 147 } 168 148 169 149 const lines = content.split('\n'); 170 - const nextStepsIndex = lines.findIndex( 171 - (line) => line.trim() === '## Next Steps', 172 - ); 150 + const nextStepsIndex = lines.findIndex((line) => line.trim() === '## Next Steps'); 173 151 174 152 if (nextStepsIndex === -1) { 175 153 const appended = `${content.trimEnd()}\n\n## Next Steps\n\n${nextStepLine}\n`; ··· 263 241 const firstImportIndex = content.search(/^import\s/m); 264 242 if (firstImportIndex === -1) { 265 243 const separator = content.endsWith('\n') ? '' : '\n'; 266 - writeFileSync( 267 - RECIPES_PRIMITIVES_PATH, 268 - `${content}${separator}${exportLine}\n`, 269 - ); 244 + writeFileSync(RECIPES_PRIMITIVES_PATH, `${content}${separator}${exportLine}\n`); 270 245 return `Added ${componentDir} to recipes barrel`; 271 246 } 272 247 const updated = `${content.slice(0, firstImportIndex)}${exportLine}\n${content.slice(firstImportIndex)}`;
+1 -5
packages/@luke-ui/react/tsconfig.storybook.json
··· 1 1 { 2 2 "extends": "./tsconfig.json", 3 - "include": [ 4 - "src/**/*.stories.tsx", 5 - ".storybook/**/*.ts", 6 - ".storybook/**/*.tsx" 7 - ] 3 + "include": ["src/**/*.stories.tsx", ".storybook/**/*.ts", ".storybook/**/*.tsx"] 8 4 }
+1 -3
packages/@luke-ui/react/vitest.config.ts
··· 8 8 import { devEntries } from './.generated/entries.js'; 9 9 10 10 const dirname = 11 - typeof __dirname !== 'undefined' 12 - ? __dirname 13 - : path.dirname(fileURLToPath(import.meta.url)); 11 + typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); 14 12 const configDir = path.join(dirname, '.storybook'); 15 13 16 14 const packageRoot = path.resolve(dirname);
+1 -4
apps/docs/content/docs/index.mdx
··· 11 11 <Card title="Link component" href="/docs/components/actions/link" /> 12 12 <Card title="Button component" href="/docs/components/actions/button" /> 13 13 <Card title="Icon component" href="/docs/components/visuals/icon" /> 14 - <Card 15 - title="Loading Spinner component" 16 - href="/docs/components/feedback/loading-spinner" 17 - /> 14 + <Card title="Loading Spinner component" href="/docs/components/feedback/loading-spinner" /> 18 15 </Cards>
+2 -2
apps/docs/src/components/not-found.tsx
··· 13 13 <h1 className="font-bold text-6xl text-fd-muted-foreground">404</h1> 14 14 <h2 className="font-semibold text-2xl">Page Not Found</h2> 15 15 <p className="max-w-md text-fd-muted-foreground"> 16 - The page you are looking for might have been removed, had its name 17 - changed, or is temporarily unavailable. 16 + The page you are looking for might have been removed, had its name changed, or is 17 + temporarily unavailable. 18 18 </p> 19 19 <Link 20 20 className="mt-4 rounded-lg bg-fd-primary px-4 py-2 font-medium text-fd-primary-foreground text-sm transition-opacity hover:opacity-90"
+1 -6
apps/docs/src/components/search.tsx
··· 32 32 }); 33 33 34 34 return ( 35 - <SearchDialog 36 - isLoading={query.isLoading} 37 - onSearchChange={setSearch} 38 - search={search} 39 - {...props} 40 - > 35 + <SearchDialog isLoading={query.isLoading} onSearchChange={setSearch} search={search} {...props}> 41 36 <SearchDialogOverlay /> 42 37 <SearchDialogContent> 43 38 <SearchDialogHeader>
+1 -6
apps/docs/src/routes/__root.tsx
··· 1 1 import lukeUiStyles from '@luke-ui/react/stylesheet.css?url'; 2 2 import { themeRootClassName } from '@luke-ui/react/theme'; 3 3 import { cx } from '@luke-ui/react/utils'; 4 - import { 5 - createRootRoute, 6 - HeadContent, 7 - Outlet, 8 - Scripts, 9 - } from '@tanstack/react-router'; 4 + import { createRootRoute, HeadContent, Outlet, Scripts } from '@tanstack/react-router'; 10 5 import { RootProvider } from 'fumadocs-ui/provider/tanstack'; 11 6 import type * as React from 'react'; 12 7 import SearchDialog from '../components/search';
+1 -4
packages/@luke-ui/react/.storybook/main.ts
··· 55 55 ]; 56 56 57 57 config.optimizeDeps ??= {}; 58 - config.optimizeDeps.exclude = [ 59 - ...(config.optimizeDeps.exclude ?? []), 60 - ...exportSpecifiers, 61 - ]; 58 + config.optimizeDeps.exclude = [...(config.optimizeDeps.exclude ?? []), ...exportSpecifiers]; 62 59 63 60 return config; 64 61 },
+5 -15
packages/@luke-ui/react/scripts/build-icons.ts
··· 11 11 const GENERATED_DIR = path.resolve(process.cwd(), '.generated'); 12 12 const ICON_DATA_TS_PATH = path.resolve(GENERATED_DIR, 'icon-data.ts'); 13 13 14 - async function writeIfChanged( 15 - filepath: string, 16 - content: string, 17 - ): Promise<boolean> { 14 + async function writeIfChanged(filepath: string, content: string): Promise<boolean> { 18 15 try { 19 16 const current = await fs.promises.readFile(filepath, 'utf8'); 20 17 if (current === content) return false; ··· 64 61 } 65 62 66 63 if (seenIconNames.has(iconName)) { 67 - process.stdout.write( 68 - `Skipping "${file}": duplicate icon id "${iconName}"\n`, 69 - ); 64 + process.stdout.write(`Skipping "${file}": duplicate icon id "${iconName}"\n`); 70 65 continue; 71 66 } 72 67 ··· 124 119 125 120 const changed = spriteChanged || iconDataChanged; 126 121 if (changed) { 127 - process.stdout.write( 128 - `Generated spritesheet with ${iconNames.length} icons\n`, 129 - ); 122 + process.stdout.write(`Generated spritesheet with ${iconNames.length} icons\n`); 130 123 } else { 131 - process.stdout.write( 132 - `Spritesheet up to date (${iconNames.length} icons)\n`, 133 - ); 124 + process.stdout.write(`Spritesheet up to date (${iconNames.length} icons)\n`); 134 125 } 135 126 } 136 127 137 128 main().catch((err) => { 138 - const message = 139 - err instanceof Error ? (err.stack ?? err.message) : String(err); 129 + const message = err instanceof Error ? (err.stack ?? err.message) : String(err); 140 130 process.stderr.write(`Failed to build icons: ${message}\n`); 141 131 process.exit(1); 142 132 });
+9 -14
packages/@luke-ui/react/scripts/generate-color-tokens.ts
··· 14 14 100: '#e9f3fb', 15 15 200: '#d4e7f7', 16 16 300: '#a9cfef', 17 - 400: '#185281', 18 - 500: '#194c73', 19 - 600: '#1a3f60', 17 + 400: '#2888d7', 18 + 500: '#185281', 19 + 600: '#103656', 20 20 } as const; 21 21 22 22 const paletteThemeAccent = { ··· 76 76 } as const; 77 77 78 78 const themeColorValues = { 79 - buttonBackgroundColor: paletteThemePrimary['400'], 80 - buttonBackgroundColorHover: paletteThemePrimary['500'], 79 + buttonBackgroundColor: paletteThemePrimary['500'], 80 + buttonBackgroundColorHover: paletteThemePrimary['600'], 81 81 buttonBackgroundColorActive: paletteThemePrimary['600'], 82 - buttonBorderColor: paletteThemePrimary['400'], 83 - buttonBorderColorHover: paletteThemePrimary['500'], 82 + buttonBorderColor: paletteThemePrimary['500'], 83 + buttonBorderColorHover: paletteThemePrimary['600'], 84 84 buttonBorderColorActive: paletteThemePrimary['600'], 85 85 buttonColor: paletteThemePrimary['100'], 86 86 focusRingColor: paletteThemePrimary['300'], ··· 138 138 139 139 const components = parsedColor.coords.map((component, index) => { 140 140 if (component === null) { 141 - throw new Error( 142 - `Color token "${inputColor}" has a missing component at index ${index}`, 143 - ); 141 + throw new Error(`Color token "${inputColor}" has a missing component at index ${index}`); 144 142 } 145 143 146 144 return roundNumber(component); ··· 159 157 160 158 const generatedGroups = Object.entries(colorGroups).map(([name, values]) => { 161 159 const transformed = Object.fromEntries( 162 - Object.entries(values).map(([key, inputColor]) => [ 163 - key, 164 - toColorTokenValue(inputColor), 165 - ]), 160 + Object.entries(values).map(([key, inputColor]) => [key, toColorTokenValue(inputColor)]), 166 161 ); 167 162 168 163 return toGeneratedConst(name, transformed);
+7 -21
packages/@luke-ui/react/scripts/generate-entries.ts
··· 16 16 utils: 'src/utils.ts', 17 17 }; 18 18 19 - async function writeIfChanged( 20 - filePath: string, 21 - content: string, 22 - ): Promise<boolean> { 19 + async function writeIfChanged(filePath: string, content: string): Promise<boolean> { 23 20 try { 24 21 const current = await fs.readFile(filePath, 'utf8'); 25 22 if (current === content) return false; ··· 59 56 if (hasPrimitives) { 60 57 entries.push([ 61 58 groupName, 62 - path 63 - .relative(PACKAGE_ROOT, primitivesPath) 64 - .replaceAll(path.sep, '/'), 59 + path.relative(PACKAGE_ROOT, primitivesPath).replaceAll(path.sep, '/'), 65 60 ]); 66 61 } 67 62 if (hasComposed) { ··· 78 73 return Object.fromEntries(groupEntries); 79 74 } 80 75 81 - async function assertEntryFilesExist( 82 - entries: Record<string, string>, 83 - ): Promise<void> { 76 + async function assertEntryFilesExist(entries: Record<string, string>): Promise<void> { 84 77 await Promise.all( 85 78 Object.entries(entries).map(async ([subpath, relativePath]) => { 86 79 const absolutePath = path.resolve(PACKAGE_ROOT, relativePath); 87 80 try { 88 81 await fs.access(absolutePath); 89 82 } catch { 90 - throw new Error( 91 - `Entry "${subpath}" points to missing file: ${relativePath}`, 92 - ); 83 + throw new Error(`Entry "${subpath}" points to missing file: ${relativePath}`); 93 84 } 94 85 }), 95 86 ); ··· 133 124 const tsChanged = await writeIfChanged(OUTPUT_TS, createTsModule(entries)); 134 125 135 126 if (tsChanged) { 136 - process.stdout.write( 137 - `Generated entries (${Object.keys(entries).length})\n`, 138 - ); 127 + process.stdout.write(`Generated entries (${Object.keys(entries).length})\n`); 139 128 } else { 140 - process.stdout.write( 141 - `Entries up to date (${Object.keys(entries).length})\n`, 142 - ); 129 + process.stdout.write(`Entries up to date (${Object.keys(entries).length})\n`); 143 130 } 144 131 } 145 132 146 133 main().catch((err) => { 147 - const errorMessage = 148 - err instanceof Error ? (err.stack ?? err.message) : String(err); 134 + const errorMessage = err instanceof Error ? (err.stack ?? err.message) : String(err); 149 135 process.stderr.write(`Failed to generate entries: ${errorMessage}\n`); 150 136 process.exit(1); 151 137 });
+5 -8
packages/@luke-ui/react/src/style-helpers.ts
··· 1 1 type StylePrimitive = string | number; 2 2 3 - export function createVariants< 4 - Key extends string, 5 - Style extends Record<string, StylePrimitive>, 6 - >(keys: ReadonlyArray<Key>, getStyle: (key: Key) => Style): Record<Key, Style> { 3 + export function createVariants<Key extends string, Style extends Record<string, StylePrimitive>>( 4 + keys: ReadonlyArray<Key>, 5 + getStyle: (key: Key) => Style, 6 + ): Record<Key, Style> { 7 7 const styles = {} as Record<Key, Style>; 8 8 for (const key of keys) { 9 9 styles[key] = getStyle(key); ··· 11 11 return styles; 12 12 } 13 13 14 - export function createPropertyVariants< 15 - Key extends string, 16 - Property extends string, 17 - >( 14 + export function createPropertyVariants<Key extends string, Property extends string>( 18 15 keys: ReadonlyArray<Key>, 19 16 property: Property, 20 17 values: Record<Key, string>,
+11 -33
packages/@luke-ui/react/src/tokens.ts
··· 28 28 $value: TValue; 29 29 }; 30 30 31 - export type DesignTokenGroup< 32 - TType extends string, 33 - TValues extends Record<string, unknown>, 34 - > = { 31 + export type DesignTokenGroup<TType extends string, TValues extends Record<string, unknown>> = { 35 32 $type: TType; 36 33 } & { 37 34 [Key in keyof TValues]: DesignToken<TValues[Key]>; ··· 42 39 string 43 40 >; 44 41 45 - function toTokenGroup< 46 - TType extends string, 47 - TValues extends Record<string, unknown>, 48 - >(type: TType, values: TValues): DesignTokenGroup<TType, TValues> { 42 + function toTokenGroup<TType extends string, TValues extends Record<string, unknown>>( 43 + type: TType, 44 + values: TValues, 45 + ): DesignTokenGroup<TType, TValues> { 49 46 const group = { $type: type } as DesignTokenGroup<TType, TValues>; 50 47 51 48 for (const key in values) { ··· 65 62 ); 66 63 } 67 64 68 - export function dimensionToRemString( 69 - value: DimensionTokenValue, 70 - base: number = 16, 71 - ): string { 72 - return value.unit === 'rem' 73 - ? `${value.value}rem` 74 - : pxToRem(value.value, base); 65 + export function dimensionToRemString(value: DimensionTokenValue, base: number = 16): string { 66 + return value.unit === 'rem' ? `${value.value}rem` : pxToRem(value.value, base); 75 67 } 76 68 77 - export function dimensionToPxNumber( 78 - value: DimensionTokenValue, 79 - base: number = 16, 80 - ): number { 69 + export function dimensionToPxNumber(value: DimensionTokenValue, base: number = 16): number { 81 70 return value.unit === 'px' ? value.value : value.value * base; 82 71 } 83 72 ··· 98 87 return `${normalized}`; 99 88 } 100 89 101 - const functionLikeColorSpaces = new Set([ 102 - 'hsl', 103 - 'hwb', 104 - 'lab', 105 - 'lch', 106 - 'oklab', 107 - 'oklch', 108 - ]); 90 + const functionLikeColorSpaces = new Set(['hsl', 'hwb', 'lab', 'lch', 'oklab', 'oklch']); 109 91 110 92 export function colorToCssString(value: ColorTokenValue): string { 111 93 if (value.components.length === 0) { 112 94 throw new Error(`Color token "${value.colorSpace}" has no components`); 113 95 } 114 96 115 - const components = value.components.map((component) => 116 - formatNumber(component), 117 - ); 97 + const components = value.components.map((component) => formatNumber(component)); 118 98 const alphaSuffix = 119 - value.alpha === undefined || value.alpha === 1 120 - ? '' 121 - : ` / ${formatNumber(value.alpha)}`; 99 + value.alpha === undefined || value.alpha === 1 ? '' : ` / ${formatNumber(value.alpha)}`; 122 100 123 101 if (functionLikeColorSpaces.has(value.colorSpace)) { 124 102 return `${value.colorSpace}(${components.join(' ')}${alphaSuffix})`;
+1 -4
packages/@luke-ui/react/src/typography.ts
··· 8 8 lineHeight: LineHeightToken; 9 9 } 10 10 11 - export function getTypographyClass( 12 - input: GetTypographyInput, 13 - debugId?: string, 14 - ) { 11 + export function getTypographyClass(input: GetTypographyInput, debugId?: string) { 15 12 const fontSize = dimensionToPxNumber(tokens.fontSize[input.fontSize].$value); 16 13 const lineHeight = tokens.lineHeight[input.lineHeight].$value; 17 14
+1 -3
packages/@luke-ui/react/src/utils.ts
··· 47 47 * const rebuilt2 = typedObjectFromEntries(typedEntries(obj)); 48 48 * // ^? { name: string, age: number } 49 49 */ 50 - export function typedObjectFromEntries<T extends object>( 51 - entries: Array<ObjectEntry<T>>, 52 - ) { 50 + export function typedObjectFromEntries<T extends object>(entries: Array<ObjectEntry<T>>) { 53 51 return Object.fromEntries(entries) as T; 54 52 }
+1 -6
apps/docs/src/routes/docs/$.tsx
··· 3 3 import { staticFunctionMiddleware } from '@tanstack/start-static-server-functions'; 4 4 import { useFumadocsLoader } from 'fumadocs-core/source/client'; 5 5 import { DocsLayout } from 'fumadocs-ui/layouts/docs'; 6 - import { 7 - DocsBody, 8 - DocsDescription, 9 - DocsPage, 10 - DocsTitle, 11 - } from 'fumadocs-ui/layouts/docs/page'; 6 + import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page'; 12 7 import defaultMdxComponents from 'fumadocs-ui/mdx'; 13 8 import { Suspense } from 'react'; 14 9 import browserCollections from '../../../.source/browser';
+1 -2
packages/@luke-ui/react/src/lib/icon.ts
··· 13 13 export const SPINNER_STROKE_WIDTH = 2; 14 14 15 15 /** Radius inset by half stroke so the stroke's outer edge aligns with icon content (19px). */ 16 - export const SPINNER_CIRCLE_RADIUS = 17 - (ICON_CONTENT_SIZE - SPINNER_STROKE_WIDTH) / 2; 16 + export const SPINNER_CIRCLE_RADIUS = (ICON_CONTENT_SIZE - SPINNER_STROKE_WIDTH) / 2;
+1 -3
packages/@luke-ui/react/src/recipes/loading-spinner.css.ts
··· 7 7 8 8 const colorKeys = tokenKeys(tokens.foregroundColor); 9 9 10 - const colorVariants = Object.fromEntries( 11 - colorKeys.map((key) => [key, { color: vars.color[key] }]), 12 - ); 10 + const colorVariants = Object.fromEntries(colorKeys.map((key) => [key, { color: vars.color[key] }])); 13 11 14 12 const base = styleInLayer('recipes', { 15 13 color: 'currentColor',
+25 -41
packages/@luke-ui/react/src/recipes/text.css.ts
··· 19 19 const fontWeightKeys = tokenKeys(tokens.fontWeight); 20 20 const lineHeightKeys = tokenKeys(tokens.lineHeight); 21 21 22 - const textDecorationKeys = [ 23 - 'none', 24 - 'underline', 25 - 'line-through', 26 - 'inherit', 27 - ] as const; 22 + const textDecorationKeys = ['none', 'underline', 'line-through', 'inherit'] as const; 28 23 export type TextDecoration = (typeof textDecorationKeys)[number]; 29 24 30 - const textTransformKeys = [ 31 - 'none', 32 - 'capitalize', 33 - 'uppercase', 34 - 'lowercase', 35 - 'inherit', 36 - ] as const; 25 + const textTransformKeys = ['none', 'capitalize', 'uppercase', 'lowercase', 'inherit'] as const; 37 26 export type TextTransform = (typeof textTransformKeys)[number]; 38 27 39 28 const textAlignKeys = ['start', 'center', 'end'] as const; ··· 87 76 }); 88 77 89 78 const colorVariants: Record<TextColor, { color: TextColor }> = (() => { 90 - const variants = createPropertyVariants( 91 - colorKeys, 92 - 'color', 93 - vars.color, 94 - ) as Record<TextColor, { color: TextColor }>; 79 + const variants = createPropertyVariants(colorKeys, 'color', vars.color) as Record< 80 + TextColor, 81 + { color: TextColor } 82 + >; 95 83 variants.inherit = { color: 'inherit' }; 96 84 return variants; 97 85 })(); ··· 102 90 vars.fontFamily, 103 91 ) as Record<TextFontFamily, { fontFamily: TextFontFamily }>; 104 92 105 - const fontWeightVariants: Record< 106 - TextFontWeight, 107 - { fontWeight: TextFontWeight } 108 - > = (() => { 109 - const variants = createPropertyVariants( 110 - fontWeightKeys, 111 - 'fontWeight', 112 - vars.fontWeight, 113 - ) as Record<TextFontWeight, { fontWeight: TextFontWeight }>; 93 + const fontWeightVariants: Record<TextFontWeight, { fontWeight: TextFontWeight }> = (() => { 94 + const variants = createPropertyVariants(fontWeightKeys, 'fontWeight', vars.fontWeight) as Record< 95 + TextFontWeight, 96 + { fontWeight: TextFontWeight } 97 + >; 114 98 variants.inherit = { fontWeight: 'inherit' }; 115 99 return variants; 116 100 })(); 117 101 118 - const textDecorationVariants = createVariants( 119 - textDecorationKeys, 120 - (textDecoration) => ({ textDecoration }), 121 - ); 102 + const textDecorationVariants = createVariants(textDecorationKeys, (textDecoration) => ({ 103 + textDecoration, 104 + })); 122 105 123 - const textTransformVariants = createVariants( 124 - textTransformKeys, 125 - (textTransform) => ({ textTransform }), 126 - ); 106 + const textTransformVariants = createVariants(textTransformKeys, (textTransform) => ({ 107 + textTransform, 108 + })); 127 109 128 110 const textAlignVariants = { 129 111 start: { textAlign: 'start' }, ··· 135 117 fontVariantNumeric: variant === 'unset' ? 'normal' : variant, 136 118 })); 137 119 138 - const fontSizeVariants = Object.fromEntries( 139 - fontSizeKeys.map((key) => [key, {}]), 140 - ) as Record<FontSizeToken, {}>; 120 + const fontSizeVariants = Object.fromEntries(fontSizeKeys.map((key) => [key, {}])) as Record< 121 + FontSizeToken, 122 + {} 123 + >; 141 124 142 - const lineHeightVariants = Object.fromEntries( 143 - lineHeightKeys.map((key) => [key, {}]), 144 - ) as Record<LineHeightToken, {}>; 125 + const lineHeightVariants = Object.fromEntries(lineHeightKeys.map((key) => [key, {}])) as Record< 126 + LineHeightToken, 127 + {} 128 + >; 145 129 146 130 const typographyCompoundVariants = (() => { 147 131 const variants: Array<{
+1 -3
packages/@luke-ui/react/src/styles/class-names.ts
··· 3 3 themeRoot: 'luke-ui-theme', 4 4 } as const; 5 5 6 - export function classSelector<TClassName extends string>( 7 - className: TClassName, 8 - ): `.${TClassName}` { 6 + export function classSelector<TClassName extends string>(className: TClassName): `.${TClassName}` { 9 7 return `.${className}`; 10 8 }
+16 -39
packages/@luke-ui/react/src/styles/layered-style.css.ts
··· 1 1 import type { GlobalStyleRule, StyleRule } from '@vanilla-extract/css'; 2 - import { 3 - globalStyle as vanillaGlobalStyle, 4 - style as vanillaStyle, 5 - } from '@vanilla-extract/css'; 2 + import { globalStyle as vanillaGlobalStyle, style as vanillaStyle } from '@vanilla-extract/css'; 6 3 import { recipe as vanillaRecipe } from '@vanilla-extract/recipes'; 7 4 import type { DistributiveOmit } from '../types.js'; 8 5 import type { LayerName } from './layers.css.js'; ··· 15 12 type RecipeVariantGroups = Record<string, RecipeVariantDefinitions>; 16 13 type BooleanMap<T> = T extends 'true' | 'false' ? boolean : T; 17 14 type RecipeVariantSelection<Variants extends RecipeVariantGroups> = { 18 - [VariantGroup in keyof Variants]?: 19 - | BooleanMap<keyof Variants[VariantGroup]> 20 - | undefined; 15 + [VariantGroup in keyof Variants]?: BooleanMap<keyof Variants[VariantGroup]> | undefined; 21 16 }; 22 17 type RecipeCompoundVariant<Variants extends RecipeVariantGroups> = { 23 18 variants: RecipeVariantSelection<Variants>; ··· 38 33 }; 39 34 } 40 35 41 - function withLayerGlobal( 42 - layer: LayerName, 43 - rule: LayeredGlobalStyleRule, 44 - ): GlobalStyleRule { 36 + function withLayerGlobal(layer: LayerName, rule: LayeredGlobalStyleRule): GlobalStyleRule { 45 37 return { 46 38 '@layer': { 47 39 [layers[layer]]: rule, ··· 49 41 }; 50 42 } 51 43 52 - function withLayerIfStyleRule( 53 - layer: LayerName, 54 - styleRule: RecipeStyleRule, 55 - ): RecipeStyleRule { 56 - return typeof styleRule === 'string' 57 - ? styleRule 58 - : withLayer(layer, styleRule); 44 + function withLayerIfStyleRule(layer: LayerName, styleRule: RecipeStyleRule): RecipeStyleRule { 45 + return typeof styleRule === 'string' ? styleRule : withLayer(layer, styleRule); 59 46 } 60 47 61 48 export function globalStyleInLayer( ··· 66 53 vanillaGlobalStyle(selector, withLayerGlobal(layer, rule)); 67 54 } 68 55 69 - export function styleInLayer( 70 - layer: LayerName, 71 - rule: LayeredStyleRule, 72 - debugId?: string, 73 - ): string { 56 + export function styleInLayer(layer: LayerName, rule: LayeredStyleRule, debugId?: string): string { 74 57 return vanillaStyle(withLayer(layer, rule), debugId); 75 58 } 76 59 ··· 83 66 options.variants === undefined 84 67 ? undefined 85 68 : (Object.fromEntries( 86 - Object.entries(options.variants).map( 87 - ([variantName, variantValues]) => [ 88 - variantName, 89 - Object.fromEntries( 90 - Object.entries(variantValues).map( 91 - ([variantValue, styleRule]) => [ 92 - variantValue, 93 - withLayerIfStyleRule(layer, styleRule), 94 - ], 95 - ), 96 - ), 97 - ], 98 - ), 69 + Object.entries(options.variants).map(([variantName, variantValues]) => [ 70 + variantName, 71 + Object.fromEntries( 72 + Object.entries(variantValues).map(([variantValue, styleRule]) => [ 73 + variantValue, 74 + withLayerIfStyleRule(layer, styleRule), 75 + ]), 76 + ), 77 + ]), 99 78 ) as Variants); 100 79 101 80 const layeredCompoundVariants = ··· 109 88 return vanillaRecipe<Variants>( 110 89 { 111 90 ...options, 112 - ...(options.base === undefined 113 - ? {} 114 - : { base: withLayerIfStyleRule(layer, options.base) }), 91 + ...(options.base === undefined ? {} : { base: withLayerIfStyleRule(layer, options.base) }), 115 92 ...(layeredVariants === undefined ? {} : { variants: layeredVariants }), 116 93 ...(layeredCompoundVariants === undefined 117 94 ? {}
+3 -7
packages/@luke-ui/react/src/styles/reset.css.ts
··· 4 4 5 5 const root = classSelector(lukeUiClassNames.resetRoot); 6 6 7 - globalStyleInLayer( 8 - 'reset', 9 - `${root}, ${root} *, ${root} *::before, ${root} *::after`, 10 - { 11 - boxSizing: 'border-box', 12 - }, 13 - ); 7 + globalStyleInLayer('reset', `${root}, ${root} *, ${root} *::before, ${root} *::after`, { 8 + boxSizing: 'border-box', 9 + }); 14 10 15 11 globalStyleInLayer('reset', `${root} :where(blockquote, dl, dd, figure, p)`, { 16 12 margin: 0,
+3 -10
packages/@luke-ui/react/src/styles/vars.css.ts
··· 19 19 20 20 type TokenGroupKey<T extends TokenGroup> = Exclude<keyof T, '$type'>; 21 21 22 - function toContract<T extends TokenGroup>( 23 - group: T, 24 - ): Record<TokenGroupKey<T>, null> { 22 + function toContract<T extends TokenGroup>(group: T): Record<TokenGroupKey<T>, null> { 25 23 const contract = {} as Record<TokenGroupKey<T>, null>; 26 24 for (const key of tokenKeys(group)) { 27 25 contract[key] = null; ··· 49 47 return String(value); 50 48 } 51 49 52 - function toStringValues<T extends TokenGroup>( 53 - group: T, 54 - ): Record<TokenGroupKey<T>, string> { 50 + function toStringValues<T extends TokenGroup>(group: T): Record<TokenGroupKey<T>, string> { 55 51 const values = {} as Record<TokenGroupKey<T>, string>; 56 52 for (const key of tokenKeys(group)) { 57 - values[key] = toCssValue( 58 - group.$type, 59 - (group[key] as { $value: unknown }).$value, 60 - ); 53 + values[key] = toCssValue(group.$type, (group[key] as { $value: unknown }).$value); 61 54 } 62 55 return values; 63 56 }
+974
packages/@luke-ui/react/src/theme/tokens-vars.stories.tsx
··· 1 + import { vars } from '@luke-ui/react/theme'; 2 + import { 3 + colorToCssString, 4 + cubicBezierToString, 5 + dimensionToRemString, 6 + durationToString, 7 + tokenKeys, 8 + tokens, 9 + } from '@luke-ui/react/tokens'; 10 + import type { 11 + ColorTokenValue, 12 + CubicBezierTokenValue, 13 + DimensionTokenValue, 14 + DurationTokenValue, 15 + } from '@luke-ui/react/tokens'; 16 + import { Text } from '@luke-ui/react/typography'; 17 + import { Heading } from '@luke-ui/react/typography/composed'; 18 + import ColorJs from 'colorjs.io'; 19 + import type { CSSProperties } from 'react'; 20 + import { expect } from 'storybook/test'; 21 + import preview from '../../.storybook/preview.js'; 22 + 23 + type StoryTokenGroup = { 24 + $type: string; 25 + [key: string]: { $value: unknown } | string; 26 + }; 27 + 28 + type TokenRow = { 29 + cssValue: string; 30 + groupName: string; 31 + path: string; 32 + type: string; 33 + value: string; 34 + }; 35 + 36 + type ExampleRow = { 37 + cssValue: string; 38 + displayValue: string; 39 + path: string; 40 + tokenName: string; 41 + }; 42 + 43 + type VarRow = { 44 + cssVar: string; 45 + path: string; 46 + }; 47 + 48 + type ExampleMode = 49 + | 'background' 50 + | 'border' 51 + | 'borderWidth' 52 + | 'breakpoint' 53 + | 'controlSize' 54 + | 'duration' 55 + | 'easing' 56 + | 'fontFamily' 57 + | 'fontSize' 58 + | 'fontWeight' 59 + | 'foreground' 60 + | 'iconSize' 61 + | 'lineHeight' 62 + | 'mediaQuery' 63 + | 'radius' 64 + | 'shadow' 65 + | 'space'; 66 + 67 + const tokenGroups = [ 68 + ['backgroundColor', tokens.backgroundColor], 69 + ['borderColor', tokens.borderColor], 70 + ['borderRadius', tokens.borderRadius], 71 + ['borderWidth', tokens.borderWidth], 72 + ['boxShadow', tokens.boxShadow], 73 + ['breakpoints', tokens.breakpoints], 74 + ['controlSize', tokens.controlSize], 75 + ['fontFamily', tokens.fontFamily], 76 + ['fontSize', tokens.fontSize], 77 + ['fontWeight', tokens.fontWeight], 78 + ['foregroundColor', tokens.foregroundColor], 79 + ['iconSize', tokens.iconSize], 80 + ['lineHeight', tokens.lineHeight], 81 + ['mediaQueries.min', tokens.mediaQueries.min], 82 + ['motionDuration', tokens.motionDuration], 83 + ['motionEasing', tokens.motionEasing], 84 + ['space', tokens.space], 85 + ['themeColor', tokens.themeColor], 86 + ] as const; 87 + 88 + const tokenRows = tokenGroups 89 + .flatMap(([groupName, group]) => getTokenRows(groupName, group)) 90 + .sort((a, b) => a.path.localeCompare(b.path)); 91 + 92 + const varRows = getVarRows(vars).sort((a, b) => a.path.localeCompare(b.path)); 93 + 94 + const backgroundColorRows = getColorRows('backgroundColor', tokens.backgroundColor); 95 + const foregroundColorRows = getColorRows('foregroundColor', tokens.foregroundColor); 96 + const borderColorRows = getColorRows('borderColor', tokens.borderColor); 97 + const themeColorRows = getColorRows('themeColor', tokens.themeColor); 98 + 99 + const spaceRows = getDimensionRows('space', tokens.space); 100 + const shadowRows = getStringRows('boxShadow', tokens.boxShadow); 101 + const borderRadiusRows = getDimensionRows('borderRadius', tokens.borderRadius); 102 + const borderWidthRows = getDimensionRows('borderWidth', tokens.borderWidth); 103 + const fontSizeRows = getDimensionRows('fontSize', tokens.fontSize); 104 + const lineHeightRows = getNumberRows('lineHeight', tokens.lineHeight); 105 + const fontWeightRows = getNumberRows('fontWeight', tokens.fontWeight); 106 + const fontFamilyRows = getStringRows('fontFamily', tokens.fontFamily); 107 + const controlSizeRows = getDimensionRows('controlSize', tokens.controlSize); 108 + const iconSizeRows = getDimensionRows('iconSize', tokens.iconSize); 109 + const motionDurationRows = getDurationRows('motionDuration', tokens.motionDuration); 110 + const motionEasingRows = getCubicBezierRows('motionEasing', tokens.motionEasing); 111 + const breakpointRows = getDimensionRows('breakpoints', tokens.breakpoints); 112 + const mediaQueryRows = getStringRows('mediaQueries.min', tokens.mediaQueries.min); 113 + 114 + const meta = preview.meta({ 115 + component: TokensVarsReference, 116 + title: 'Theme/Tokens & Vars', 117 + }); 118 + 119 + function css<TStyle extends CSSProperties>(style: TStyle): TStyle { 120 + return style; 121 + } 122 + 123 + function columnStyle(inlineSize: string): CSSProperties { 124 + return css({ inlineSize }); 125 + } 126 + 127 + const stackStyle = css({ 128 + display: 'flex', 129 + flexDirection: 'column', 130 + gap: '1.5rem', 131 + maxInlineSize: '100%', 132 + }); 133 + 134 + const sectionStyle = css({ 135 + display: 'flex', 136 + flexDirection: 'column', 137 + gap: '0.75rem', 138 + }); 139 + 140 + const tableWrapStyle = css({ 141 + borderColor: vars.border.default, 142 + borderStyle: 'solid', 143 + borderWidth: 1, 144 + borderRadius: vars.borderRadius.medium, 145 + inlineSize: '100%', 146 + maxInlineSize: '100%', 147 + overflow: 'auto', 148 + }); 149 + 150 + const tokenReferenceTableStyle = css({ 151 + borderCollapse: 'collapse', 152 + fontFamily: vars.font.family.mono, 153 + fontSize: vars.font.size.standard, 154 + inlineSize: '100%', 155 + minInlineSize: '82rem', 156 + tableLayout: 'fixed', 157 + }); 158 + 159 + const varsReferenceTableStyle = css({ 160 + borderCollapse: 'collapse', 161 + fontFamily: vars.font.family.mono, 162 + fontSize: vars.font.size.standard, 163 + inlineSize: '100%', 164 + minInlineSize: '64rem', 165 + tableLayout: 'fixed', 166 + }); 167 + 168 + const headerCellStyle = css({ 169 + backgroundColor: vars.backgroundColor.subtle, 170 + borderBlockEndColor: vars.border.default, 171 + borderBlockEndStyle: 'solid', 172 + borderBlockEndWidth: 1, 173 + fontWeight: vars.font.weight.bold, 174 + paddingBlock: vars.space.xsmall, 175 + paddingInline: vars.space.small, 176 + textAlign: 'start', 177 + whiteSpace: 'nowrap', 178 + }); 179 + 180 + const cellStyle = css({ 181 + borderBlockEndColor: vars.border.default, 182 + borderBlockEndStyle: 'solid', 183 + borderBlockEndWidth: 1, 184 + overflowWrap: 'anywhere', 185 + paddingBlock: vars.space.xsmall, 186 + paddingInline: vars.space.small, 187 + verticalAlign: 'top', 188 + }); 189 + 190 + const headingStyle = css({ 191 + fontFamily: vars.font.family.body, 192 + fontSize: vars.font.size.medium, 193 + fontWeight: vars.font.weight.bold, 194 + lineHeight: vars.font.lineHeight.tight, 195 + margin: 0, 196 + }); 197 + 198 + const descriptionStyle = css({ 199 + color: vars.foregroundColor.secondary, 200 + fontFamily: vars.font.family.body, 201 + margin: 0, 202 + }); 203 + 204 + const previewSwatchStyle = css({ 205 + blockSize: '1.5rem', 206 + borderColor: vars.border.default, 207 + borderStyle: 'solid', 208 + borderWidth: 1, 209 + borderRadius: vars.borderRadius.small, 210 + display: 'inline-block', 211 + inlineSize: '4rem', 212 + }); 213 + 214 + const previewBoxStyle = css({ 215 + backgroundColor: vars.backgroundColor.default, 216 + blockSize: '1.5rem', 217 + borderColor: vars.border.default, 218 + borderStyle: 'solid', 219 + borderWidth: 1, 220 + display: 'inline-block', 221 + inlineSize: '4rem', 222 + }); 223 + 224 + const previewTextStyle = css({ 225 + fontFamily: vars.font.family.body, 226 + fontSize: vars.font.size.standard, 227 + fontWeight: vars.font.weight.bold, 228 + }); 229 + 230 + const mutedPreviewStyle = css({ 231 + color: vars.foregroundColor.secondary, 232 + }); 233 + 234 + const docsPageStyle = css({ 235 + backgroundColor: vars.backgroundColor.default, 236 + borderColor: vars.border.default, 237 + borderStyle: 'solid', 238 + borderWidth: 1, 239 + borderRadius: vars.borderRadius.large, 240 + display: 'flex', 241 + flexDirection: 'column', 242 + gap: vars.space.large, 243 + paddingBlock: vars.space.large, 244 + paddingInline: vars.space.large, 245 + }); 246 + 247 + const docsMainStyle = css({ 248 + display: 'flex', 249 + flexDirection: 'column', 250 + gap: vars.space.large, 251 + minInlineSize: 0, 252 + }); 253 + 254 + const tokenSectionStyle = css({ 255 + display: 'flex', 256 + flexDirection: 'column', 257 + gap: vars.space.small, 258 + }); 259 + 260 + const tokenTableWrapStyle = css({ 261 + backgroundColor: vars.backgroundColor.default, 262 + borderColor: vars.border.default, 263 + borderStyle: 'solid', 264 + borderWidth: 1, 265 + borderRadius: vars.borderRadius.medium, 266 + overflow: 'auto', 267 + }); 268 + 269 + const tokenTableStyle = css({ 270 + borderCollapse: 'collapse', 271 + fontFamily: vars.font.family.body, 272 + fontSize: vars.font.size.standard, 273 + inlineSize: '100%', 274 + minInlineSize: '50rem', 275 + tableLayout: 'fixed', 276 + }); 277 + 278 + const tokenTableHeaderCellStyle = css({ 279 + backgroundColor: vars.backgroundColor.subtle, 280 + borderBlockEndColor: vars.border.default, 281 + borderBlockEndStyle: 'solid', 282 + borderBlockEndWidth: 1, 283 + fontWeight: vars.font.weight.bold, 284 + paddingBlock: vars.space.small, 285 + paddingInline: vars.space.small, 286 + textAlign: 'start', 287 + }); 288 + 289 + const tokenTableCellStyle = css({ 290 + borderBlockEndColor: vars.border.default, 291 + borderBlockEndStyle: 'solid', 292 + borderBlockEndWidth: 1, 293 + overflowWrap: 'anywhere', 294 + paddingBlock: vars.space.small, 295 + paddingInline: vars.space.small, 296 + verticalAlign: 'middle', 297 + }); 298 + 299 + const examplesColExampleStyle = columnStyle('20rem'); 300 + const examplesColTokenKeyStyle = columnStyle('16rem'); 301 + const examplesColTokenValueStyle = columnStyle('14rem'); 302 + 303 + const tokenRefColPathStyle = columnStyle('24rem'); 304 + const tokenRefColTypeStyle = columnStyle('8rem'); 305 + const tokenRefColRawStyle = columnStyle('24rem'); 306 + const tokenRefColCssStyle = columnStyle('18rem'); 307 + const tokenRefColPreviewStyle = columnStyle('8rem'); 308 + 309 + const varsRefColPathStyle = columnStyle('24rem'); 310 + const varsRefColValueStyle = columnStyle('28rem'); 311 + const varsRefColPreviewStyle = columnStyle('10rem'); 312 + 313 + const tokenPillStyle = css({ 314 + backgroundColor: vars.backgroundColor.subtle, 315 + borderRadius: vars.borderRadius.small, 316 + display: 'inline-flex', 317 + fontFamily: vars.font.family.mono, 318 + fontSize: vars.font.size.standard, 319 + fontWeight: vars.font.weight.medium, 320 + paddingBlock: vars.space.xxsmall, 321 + paddingInline: vars.space.xsmall, 322 + }); 323 + 324 + const tokenValueStyle = css({ 325 + fontFamily: vars.font.family.mono, 326 + fontSize: vars.font.size.standard, 327 + }); 328 + 329 + const exampleBarStyle = css({ 330 + blockSize: '2.75rem', 331 + borderColor: vars.border.default, 332 + borderStyle: 'solid', 333 + borderWidth: 1, 334 + borderRadius: vars.borderRadius.small, 335 + inlineSize: '14rem', 336 + maxInlineSize: '100%', 337 + }); 338 + 339 + const sampleTextStyle = css({ 340 + color: vars.foregroundColor.primary, 341 + fontFamily: vars.font.family.body, 342 + fontSize: vars.font.size.standard, 343 + fontWeight: vars.font.weight.regular, 344 + }); 345 + 346 + const motionTrackStyle = css({ 347 + ...exampleBarStyle, 348 + alignItems: 'center', 349 + display: 'flex', 350 + paddingInline: vars.space.xsmall, 351 + }); 352 + 353 + const motionRailStyle = css({ 354 + backgroundColor: vars.backgroundColor.inputDisabled, 355 + blockSize: '1.25rem', 356 + borderColor: vars.border.default, 357 + borderStyle: 'solid', 358 + borderWidth: 1, 359 + borderRadius: vars.borderRadius.full, 360 + inlineSize: '100%', 361 + position: 'relative', 362 + }); 363 + 364 + const motionDotBaseStyle = css({ 365 + backgroundColor: vars.themeColor.paletteThemePrimary600, 366 + blockSize: '1rem', 367 + borderRadius: vars.borderRadius.full, 368 + boxShadow: `0 0 0 ${vars.space.xxsmall} ${vars.themeColor.paletteThemePrimary200}`, 369 + display: 'inline-block', 370 + insetBlockStart: '50%', 371 + position: 'absolute', 372 + transform: 'translateY(-50%)', 373 + }); 374 + 375 + const motionPreviewDefaultDuration = '1200ms'; 376 + const motionPreviewDefaultEasing = 'cubic-bezier(0.4, 0, 0.2, 1)'; 377 + const motionDotInset = '0.25rem'; 378 + const motionDotSize = '1rem'; 379 + const motionPreviewAnimationName = 'luke-ui-motion-token-preview'; 380 + 381 + function formatRawValue(value: unknown): string { 382 + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { 383 + return String(value); 384 + } 385 + 386 + return JSON.stringify(value); 387 + } 388 + 389 + function toCssValue(type: string, value: unknown): string { 390 + if (type === 'color') { 391 + return colorToCssString(value as ColorTokenValue); 392 + } 393 + 394 + if (type === 'cubicBezier') { 395 + return cubicBezierToString(value as CubicBezierTokenValue); 396 + } 397 + 398 + if (type === 'dimension') { 399 + return dimensionToRemString(value as DimensionTokenValue); 400 + } 401 + 402 + if (type === 'duration') { 403 + return durationToString(value as DurationTokenValue); 404 + } 405 + 406 + return String(value); 407 + } 408 + 409 + function colorToDisplayValue(value: ColorTokenValue): string { 410 + if (value.colorSpace === 'srgb' && value.alpha === undefined) { 411 + try { 412 + const color = new ColorJs(colorToCssString(value)); 413 + return color.toString({ collapse: false, format: 'hex' }).toUpperCase(); 414 + } catch { 415 + return colorToCssString(value); 416 + } 417 + } 418 + 419 + return colorToCssString(value); 420 + } 421 + 422 + function getTokenValue(group: StoryTokenGroup, tokenName: string, groupName: string): unknown { 423 + const token = group[tokenName]; 424 + if (token && typeof token === 'object' && '$value' in token) { 425 + return token.$value; 426 + } 427 + 428 + throw new TypeError(`Expected token value for ${groupName}.${tokenName}`); 429 + } 430 + 431 + function getTokenRows(groupName: string, group: StoryTokenGroup): Array<TokenRow> { 432 + return tokenKeys(group).map((tokenName) => { 433 + const tokenValue = getTokenValue(group, tokenName, groupName); 434 + return { 435 + cssValue: toCssValue(group.$type, tokenValue), 436 + groupName, 437 + path: `${groupName}.${tokenName}`, 438 + type: group.$type, 439 + value: formatRawValue(tokenValue), 440 + }; 441 + }); 442 + } 443 + 444 + function getRows( 445 + groupName: string, 446 + group: StoryTokenGroup, 447 + toValues: (value: unknown) => { cssValue: string; displayValue: string }, 448 + ): Array<ExampleRow> { 449 + return tokenKeys(group).map((tokenName) => { 450 + const tokenValue = getTokenValue(group, tokenName, groupName); 451 + const values = toValues(tokenValue); 452 + return { 453 + cssValue: values.cssValue, 454 + displayValue: values.displayValue, 455 + path: `${groupName}.${tokenName}`, 456 + tokenName, 457 + }; 458 + }); 459 + } 460 + 461 + function getColorRows(groupName: string, group: StoryTokenGroup): Array<ExampleRow> { 462 + if (group.$type !== 'color') { 463 + return []; 464 + } 465 + 466 + return getRows(groupName, group, (value) => { 467 + const colorValue = value as ColorTokenValue; 468 + return { 469 + cssValue: colorToCssString(colorValue), 470 + displayValue: colorToDisplayValue(colorValue), 471 + }; 472 + }); 473 + } 474 + 475 + function getDimensionRows(groupName: string, group: StoryTokenGroup): Array<ExampleRow> { 476 + if (group.$type !== 'dimension') { 477 + return []; 478 + } 479 + 480 + return getRows(groupName, group, (value) => { 481 + const cssValue = toCssValue('dimension', value); 482 + return { cssValue, displayValue: cssValue }; 483 + }); 484 + } 485 + 486 + function getDurationRows(groupName: string, group: StoryTokenGroup): Array<ExampleRow> { 487 + if (group.$type !== 'duration') { 488 + return []; 489 + } 490 + 491 + return getRows(groupName, group, (value) => { 492 + const cssValue = toCssValue('duration', value); 493 + return { cssValue, displayValue: cssValue }; 494 + }); 495 + } 496 + 497 + function getCubicBezierRows(groupName: string, group: StoryTokenGroup): Array<ExampleRow> { 498 + if (group.$type !== 'cubicBezier') { 499 + return []; 500 + } 501 + 502 + return getRows(groupName, group, (value) => { 503 + const cssValue = toCssValue('cubicBezier', value); 504 + return { cssValue, displayValue: cssValue }; 505 + }); 506 + } 507 + 508 + function getNumberRows(groupName: string, group: StoryTokenGroup): Array<ExampleRow> { 509 + if (group.$type !== 'number' && group.$type !== 'fontWeight') { 510 + return []; 511 + } 512 + 513 + return getRows(groupName, group, (value) => { 514 + const stringValue = String(value); 515 + return { cssValue: stringValue, displayValue: stringValue }; 516 + }); 517 + } 518 + 519 + function getStringRows(groupName: string, group: StoryTokenGroup): Array<ExampleRow> { 520 + return getRows(groupName, group, (value) => { 521 + const stringValue = String(value); 522 + return { cssValue: stringValue, displayValue: stringValue }; 523 + }); 524 + } 525 + 526 + function getVarRows(node: unknown, path: Array<string> = []): Array<VarRow> { 527 + if (typeof node === 'string') { 528 + return node.startsWith('var(--') ? [{ cssVar: node, path: path.join('.') }] : []; 529 + } 530 + 531 + if (!node || typeof node !== 'object') { 532 + return []; 533 + } 534 + 535 + return Object.entries(node).flatMap(([key, value]) => getVarRows(value, [...path, key])); 536 + } 537 + 538 + function renderTokenPreview(row: TokenRow) { 539 + if (row.type === 'color') { 540 + return <span style={{ ...previewSwatchStyle, backgroundColor: row.cssValue }} />; 541 + } 542 + 543 + if (row.groupName === 'borderRadius') { 544 + return ( 545 + <span 546 + style={{ 547 + ...previewBoxStyle, 548 + borderRadius: row.cssValue, 549 + }} 550 + /> 551 + ); 552 + } 553 + 554 + if (row.groupName === 'boxShadow') { 555 + return <span style={{ ...previewBoxStyle, boxShadow: row.cssValue }} />; 556 + } 557 + 558 + if (row.groupName === 'borderWidth') { 559 + return ( 560 + <span 561 + style={{ 562 + ...previewBoxStyle, 563 + borderWidth: row.cssValue, 564 + }} 565 + /> 566 + ); 567 + } 568 + 569 + return <span style={mutedPreviewStyle}>-</span>; 570 + } 571 + 572 + function renderVarPreview(row: VarRow) { 573 + if (row.path.includes('foregroundColor')) { 574 + return <span style={{ ...previewTextStyle, color: row.cssVar }}>Aa</span>; 575 + } 576 + 577 + if (row.path.includes('backgroundColor') || row.path.includes('themeColor')) { 578 + return <span style={{ ...previewSwatchStyle, backgroundColor: row.cssVar }} />; 579 + } 580 + 581 + if (row.path.includes('borderColor') || row.path.startsWith('border.')) { 582 + return ( 583 + <span 584 + style={{ 585 + ...previewBoxStyle, 586 + borderColor: row.cssVar, 587 + }} 588 + /> 589 + ); 590 + } 591 + 592 + if (row.path.includes('boxShadow')) { 593 + return <span style={{ ...previewBoxStyle, boxShadow: row.cssVar }} />; 594 + } 595 + 596 + if (row.path.includes('borderRadius')) { 597 + return ( 598 + <span 599 + style={{ 600 + ...previewBoxStyle, 601 + borderRadius: row.cssVar, 602 + }} 603 + /> 604 + ); 605 + } 606 + 607 + return <span style={mutedPreviewStyle}>-</span>; 608 + } 609 + 610 + function SharedPreviewBox({ size }: { size: string }) { 611 + return ( 612 + <span 613 + style={{ 614 + backgroundColor: vars.themeColor.paletteThemePrimary300, 615 + blockSize: size, 616 + display: 'inline-block', 617 + inlineSize: size, 618 + }} 619 + /> 620 + ); 621 + } 622 + 623 + function SizePreview({ size }: { size: string }) { 624 + return ( 625 + <div 626 + style={{ 627 + ...exampleBarStyle, 628 + display: 'grid', 629 + placeItems: 'center', 630 + }} 631 + > 632 + <SharedPreviewBox size={size} /> 633 + </div> 634 + ); 635 + } 636 + 637 + function MotionPreview({ duration, easing }: { duration: string; easing: string }) { 638 + return ( 639 + <div style={motionTrackStyle}> 640 + <style>{`@keyframes ${motionPreviewAnimationName} { from { inset-inline-start: ${motionDotInset}; } to { inset-inline-start: calc(100% - ${motionDotSize} - ${motionDotInset}); } }`}</style> 641 + <div style={motionRailStyle}> 642 + <span 643 + style={{ 644 + ...motionDotBaseStyle, 645 + animationDirection: 'alternate', 646 + animationDuration: duration, 647 + animationIterationCount: 'infinite', 648 + animationName: motionPreviewAnimationName, 649 + animationTimingFunction: easing, 650 + insetInlineStart: motionDotInset, 651 + }} 652 + /> 653 + </div> 654 + </div> 655 + ); 656 + } 657 + 658 + function RenderExamplePreview({ mode, row }: { mode: ExampleMode; row: ExampleRow }) { 659 + switch (mode) { 660 + case 'foreground': 661 + return ( 662 + <div 663 + style={{ 664 + ...exampleBarStyle, 665 + display: 'grid', 666 + placeItems: 'center', 667 + }} 668 + > 669 + <span style={{ ...previewTextStyle, color: row.cssValue }}>Aa</span> 670 + </div> 671 + ); 672 + case 'border': 673 + return <div style={{ ...exampleBarStyle, borderColor: row.cssValue }} />; 674 + case 'background': 675 + return <div style={{ ...exampleBarStyle, backgroundColor: row.cssValue }} />; 676 + case 'space': { 677 + const boxSize = vars.space.large; 678 + return ( 679 + <div 680 + style={{ 681 + ...exampleBarStyle, 682 + display: 'flex', 683 + alignItems: 'center', 684 + paddingInline: vars.space.xsmall, 685 + }} 686 + > 687 + <div 688 + style={{ 689 + display: 'flex', 690 + gap: row.cssValue, 691 + }} 692 + > 693 + <SharedPreviewBox size={boxSize} /> 694 + <SharedPreviewBox size={boxSize} /> 695 + <SharedPreviewBox size={boxSize} /> 696 + </div> 697 + </div> 698 + ); 699 + } 700 + case 'shadow': 701 + return <div style={{ ...exampleBarStyle, boxShadow: row.cssValue }} />; 702 + case 'radius': 703 + return <div style={{ ...exampleBarStyle, borderRadius: row.cssValue }} />; 704 + case 'borderWidth': 705 + return <div style={{ ...exampleBarStyle, borderWidth: row.cssValue }} />; 706 + case 'fontSize': 707 + return <span style={{ ...sampleTextStyle, fontSize: row.cssValue }}>Sample text</span>; 708 + case 'lineHeight': 709 + return ( 710 + <span 711 + style={{ 712 + ...sampleTextStyle, 713 + display: 'inline-block', 714 + lineHeight: row.cssValue, 715 + }} 716 + > 717 + Line one 718 + <br /> 719 + Line two 720 + </span> 721 + ); 722 + case 'fontWeight': 723 + return <span style={{ ...sampleTextStyle, fontWeight: row.cssValue }}>Weight</span>; 724 + case 'fontFamily': 725 + return ( 726 + <span style={{ ...sampleTextStyle, fontFamily: row.cssValue }}>Sphinx of black quartz</span> 727 + ); 728 + case 'controlSize': 729 + case 'iconSize': 730 + return <SizePreview size={row.cssValue} />; 731 + case 'breakpoint': 732 + return <span style={tokenValueStyle}>{`min-width >= ${row.displayValue}`}</span>; 733 + case 'duration': 734 + return <MotionPreview duration={row.cssValue} easing={motionPreviewDefaultEasing} />; 735 + case 'easing': 736 + return <MotionPreview duration={motionPreviewDefaultDuration} easing={row.cssValue} />; 737 + case 'mediaQuery': 738 + return <span style={tokenValueStyle}>{row.displayValue}</span>; 739 + default: 740 + return <span style={mutedPreviewStyle}>-</span>; 741 + } 742 + } 743 + 744 + function TokenExamplesTable({ mode, rows }: { mode: ExampleMode; rows: Array<ExampleRow> }) { 745 + return ( 746 + <section style={tokenSectionStyle}> 747 + <div style={tokenTableWrapStyle}> 748 + <table style={tokenTableStyle}> 749 + <colgroup> 750 + <col style={examplesColExampleStyle} /> 751 + <col style={examplesColTokenKeyStyle} /> 752 + <col style={examplesColTokenValueStyle} /> 753 + </colgroup> 754 + <thead> 755 + <tr> 756 + <th style={tokenTableHeaderCellStyle}>Example</th> 757 + <th style={tokenTableHeaderCellStyle}>Token key</th> 758 + <th style={tokenTableHeaderCellStyle}>Token value</th> 759 + </tr> 760 + </thead> 761 + <tbody> 762 + {rows.map((row) => ( 763 + <tr key={row.path}> 764 + <td style={tokenTableCellStyle}> 765 + <RenderExamplePreview mode={mode} row={row} /> 766 + </td> 767 + <td style={tokenTableCellStyle}> 768 + <span style={tokenPillStyle}>{row.tokenName}</span> 769 + </td> 770 + <td style={tokenTableCellStyle}> 771 + <span style={tokenValueStyle}>{row.displayValue}</span> 772 + </td> 773 + </tr> 774 + ))} 775 + </tbody> 776 + </table> 777 + </div> 778 + </section> 779 + ); 780 + } 781 + 782 + function TokensVarsReference() { 783 + return ( 784 + <div style={stackStyle}> 785 + <section style={sectionStyle}> 786 + <h2 style={headingStyle}>Tokens</h2> 787 + <p style={descriptionStyle}> 788 + Design-token source values, computed CSS value, and a visual preview. 789 + </p> 790 + <div style={tableWrapStyle}> 791 + <table style={tokenReferenceTableStyle}> 792 + <colgroup> 793 + <col style={tokenRefColPathStyle} /> 794 + <col style={tokenRefColTypeStyle} /> 795 + <col style={tokenRefColRawStyle} /> 796 + <col style={tokenRefColCssStyle} /> 797 + <col style={tokenRefColPreviewStyle} /> 798 + </colgroup> 799 + <thead> 800 + <tr> 801 + <th style={headerCellStyle}>Path</th> 802 + <th style={headerCellStyle}>Type</th> 803 + <th style={headerCellStyle}>$value</th> 804 + <th style={headerCellStyle}>CSS value</th> 805 + <th style={headerCellStyle}>Preview</th> 806 + </tr> 807 + </thead> 808 + <tbody> 809 + {tokenRows.map((row) => ( 810 + <tr key={row.path}> 811 + <td style={cellStyle}>{row.path}</td> 812 + <td style={cellStyle}>{row.type}</td> 813 + <td style={cellStyle}>{row.value}</td> 814 + <td style={cellStyle}>{row.cssValue}</td> 815 + <td style={cellStyle}>{renderTokenPreview(row)}</td> 816 + </tr> 817 + ))} 818 + </tbody> 819 + </table> 820 + </div> 821 + </section> 822 + <section style={sectionStyle}> 823 + <h2 style={headingStyle}>CSS Variables</h2> 824 + <p style={descriptionStyle}> 825 + Flattened paths from `theme.vars`, including a quick visual preview. 826 + </p> 827 + <div style={tableWrapStyle}> 828 + <table style={varsReferenceTableStyle}> 829 + <colgroup> 830 + <col style={varsRefColPathStyle} /> 831 + <col style={varsRefColValueStyle} /> 832 + <col style={varsRefColPreviewStyle} /> 833 + </colgroup> 834 + <thead> 835 + <tr> 836 + <th style={headerCellStyle}>Path</th> 837 + <th style={headerCellStyle}>Variable</th> 838 + <th style={headerCellStyle}>Preview</th> 839 + </tr> 840 + </thead> 841 + <tbody> 842 + {varRows.map((row) => ( 843 + <tr key={row.path}> 844 + <td style={cellStyle}>{row.path}</td> 845 + <td style={cellStyle}>{row.cssVar}</td> 846 + <td style={cellStyle}>{renderVarPreview(row)}</td> 847 + </tr> 848 + ))} 849 + </tbody> 850 + </table> 851 + </div> 852 + </section> 853 + </div> 854 + ); 855 + } 856 + 857 + type TokenTypeStoryConfig = { 858 + description?: string; 859 + mode: ExampleMode; 860 + rows: Array<ExampleRow>; 861 + title: string; 862 + }; 863 + 864 + function TokenTypeStory({ 865 + description = 'A visual reference for this token type.', 866 + mode, 867 + rows, 868 + title, 869 + }: TokenTypeStoryConfig) { 870 + return ( 871 + <div style={docsPageStyle}> 872 + <section style={sectionStyle}> 873 + <Heading elementType="h1" fontWeight="bold" lineHeight="tight"> 874 + {title} 875 + </Heading> 876 + <Text elementType="p" color="neutralSubtle" fontSize="medium" lineHeight="loose"> 877 + {description} 878 + </Text> 879 + </section> 880 + <main style={docsMainStyle}> 881 + <TokenExamplesTable mode={mode} rows={rows} /> 882 + </main> 883 + </div> 884 + ); 885 + } 886 + 887 + type StoryCanvas = { 888 + getByRole: (role: string, options: { name: string }) => unknown; 889 + }; 890 + 891 + const tokenTypeStoryConfigs = { 892 + backgroundColor: { mode: 'background', rows: backgroundColorRows, title: 'Background color' }, 893 + borderColor: { mode: 'border', rows: borderColorRows, title: 'Border color' }, 894 + borderRadius: { mode: 'radius', rows: borderRadiusRows, title: 'Border radius' }, 895 + borderWidth: { mode: 'borderWidth', rows: borderWidthRows, title: 'Border width' }, 896 + boxShadow: { mode: 'shadow', rows: shadowRows, title: 'Box shadow' }, 897 + breakpoints: { mode: 'breakpoint', rows: breakpointRows, title: 'Breakpoints' }, 898 + color: { mode: 'foreground', rows: foregroundColorRows, title: 'Color' }, 899 + controlSize: { mode: 'controlSize', rows: controlSizeRows, title: 'Control size' }, 900 + fontFamily: { mode: 'fontFamily', rows: fontFamilyRows, title: 'Font family' }, 901 + fontSize: { mode: 'fontSize', rows: fontSizeRows, title: 'Font size' }, 902 + fontWeight: { mode: 'fontWeight', rows: fontWeightRows, title: 'Font weight' }, 903 + iconSize: { mode: 'iconSize', rows: iconSizeRows, title: 'Icon size' }, 904 + lineHeight: { mode: 'lineHeight', rows: lineHeightRows, title: 'Line height' }, 905 + mediaQueries: { mode: 'mediaQuery', rows: mediaQueryRows, title: 'Media queries' }, 906 + motionDuration: { mode: 'duration', rows: motionDurationRows, title: 'Motion duration' }, 907 + motionEasing: { mode: 'easing', rows: motionEasingRows, title: 'Motion easing' }, 908 + space: { mode: 'space', rows: spaceRows, title: 'Space' }, 909 + themeColor: { mode: 'background', rows: themeColorRows, title: 'Theme color' }, 910 + } satisfies Record<string, TokenTypeStoryConfig>; 911 + 912 + async function expectHeadingInStory(canvas: StoryCanvas, heading: string) { 913 + await expect(canvas.getByRole('heading', { name: heading })).toBeInTheDocument(); 914 + } 915 + 916 + function RenderTokenTypeStory(props: TokenTypeStoryConfig) { 917 + return <TokenTypeStory {...props} />; 918 + } 919 + 920 + function tokenTypeStory(config: TokenTypeStoryConfig) { 921 + return { 922 + play: async ({ canvas }: { canvas: StoryCanvas }) => { 923 + await expectHeadingInStory(canvas, config.title); 924 + }, 925 + render: () => <RenderTokenTypeStory {...config} />, 926 + }; 927 + } 928 + 929 + export const BackgroundColor = meta.story(tokenTypeStory(tokenTypeStoryConfigs.backgroundColor)); 930 + 931 + export const Color = meta.story(tokenTypeStory(tokenTypeStoryConfigs.color)); 932 + 933 + export const BorderColor = meta.story(tokenTypeStory(tokenTypeStoryConfigs.borderColor)); 934 + 935 + export const ThemeColor = meta.story(tokenTypeStory(tokenTypeStoryConfigs.themeColor)); 936 + 937 + export const Space = meta.story(tokenTypeStory(tokenTypeStoryConfigs.space)); 938 + 939 + export const BoxShadow = meta.story(tokenTypeStory(tokenTypeStoryConfigs.boxShadow)); 940 + 941 + export const BorderRadius = meta.story(tokenTypeStory(tokenTypeStoryConfigs.borderRadius)); 942 + 943 + export const BorderWidth = meta.story(tokenTypeStory(tokenTypeStoryConfigs.borderWidth)); 944 + 945 + export const FontSize = meta.story(tokenTypeStory(tokenTypeStoryConfigs.fontSize)); 946 + 947 + export const LineHeight = meta.story(tokenTypeStory(tokenTypeStoryConfigs.lineHeight)); 948 + 949 + export const FontWeight = meta.story(tokenTypeStory(tokenTypeStoryConfigs.fontWeight)); 950 + 951 + export const FontFamily = meta.story(tokenTypeStory(tokenTypeStoryConfigs.fontFamily)); 952 + 953 + export const ControlSize = meta.story(tokenTypeStory(tokenTypeStoryConfigs.controlSize)); 954 + 955 + export const IconSize = meta.story(tokenTypeStory(tokenTypeStoryConfigs.iconSize)); 956 + 957 + export const MotionDuration = meta.story(tokenTypeStory(tokenTypeStoryConfigs.motionDuration)); 958 + 959 + export const MotionEasing = meta.story(tokenTypeStory(tokenTypeStoryConfigs.motionEasing)); 960 + 961 + export const Breakpoints = meta.story(tokenTypeStory(tokenTypeStoryConfigs.breakpoints)); 962 + 963 + export const MediaQueries = meta.story(tokenTypeStory(tokenTypeStoryConfigs.mediaQueries)); 964 + 965 + /** 966 + * Reference page for design tokens and the generated theme `vars` contract. 967 + */ 968 + export const Reference = meta.story({ 969 + play: async ({ canvas }) => { 970 + await expectHeadingInStory(canvas, 'Tokens'); 971 + await expectHeadingInStory(canvas, 'CSS Variables'); 972 + }, 973 + render: () => <TokensVarsReference />, 974 + });
+1 -5
packages/@luke-ui/react/src/typography/composed.ts
··· 1 1 export type { EmojiProps } from './text/composed/emoji.js'; 2 2 export { Emoji } from './text/composed/emoji.js'; 3 - export type { 4 - HeadingLevel, 5 - HeadingProps, 6 - HeadingTag, 7 - } from './text/composed/heading.js'; 3 + export type { HeadingLevel, HeadingProps, HeadingTag } from './text/composed/heading.js'; 8 4 export { Heading } from './text/composed/heading.js'; 9 5 export type { HeadingLevelsRenderProps } from './text/composed/heading-context.js'; 10 6 export {
+1 -3
packages/@luke-ui/react/src/actions/button/button.stories.tsx
··· 27 27 export const Default = meta.story({ 28 28 args: { children: 'Primitive button' }, 29 29 play: async ({ canvas }) => { 30 - await expect( 31 - canvas.getByRole('button', { name: 'Primitive button' }), 32 - ).toBeInTheDocument(); 30 + await expect(canvas.getByRole('button', { name: 'Primitive button' })).toBeInTheDocument(); 33 31 }, 34 32 }); 35 33
+2 -5
packages/@luke-ui/react/src/actions/link/link.stories.tsx
··· 35 35 export const Default = meta.story({ 36 36 args: baseArgs, 37 37 play: async ({ canvas }) => { 38 - await expect( 39 - canvas.getByRole('link', { name: 'Link' }), 40 - ).toBeInTheDocument(); 38 + await expect(canvas.getByRole('link', { name: 'Link' })).toBeInTheDocument(); 41 39 }, 42 40 }); 43 41 ··· 78 76 Standalone link 79 77 </Link> 80 78 <p> 81 - When part of a sentence, use the default{' '} 82 - <Link {...props}>inline link</Link> style. 79 + When part of a sentence, use the default <Link {...props}>inline link</Link> style. 83 80 </p> 84 81 </div> 85 82 ),
+2 -7
packages/@luke-ui/react/src/feedback/loading-spinner/loading-spinner.stories.tsx
··· 26 26 export const Default = meta.story({ 27 27 args: baseArgs, 28 28 play: async ({ canvas }) => { 29 - await expect( 30 - canvas.getByRole('progressbar', { name: 'pending' }), 31 - ).toBeInTheDocument(); 29 + await expect(canvas.getByRole('progressbar', { name: 'pending' })).toBeInTheDocument(); 32 30 }, 33 31 }); 34 32 35 - const sizes: Array<NonNullable<LoadingSpinnerProps['size']>> = [ 36 - 'small', 37 - 'medium', 38 - ]; 33 + const sizes: Array<NonNullable<LoadingSpinnerProps['size']>> = ['small', 'medium']; 39 34 40 35 /** 41 36 * Size adjusts the spinner footprint for compact and standard layouts.
+13 -42
packages/@luke-ui/react/src/typography/text/text.stories.tsx
··· 35 35 lineHeight: 'loose', 36 36 } as const satisfies Pick<TextProps, 'children' | 'fontSize' | 'lineHeight'>; 37 37 38 - const colors = [ 39 - ...tokenKeys(tokens.foregroundColor), 40 - 'inherit', 41 - ] satisfies Array<TextProps['color']>; 42 - 43 - const fontFamilies = tokenKeys(tokens.fontFamily) satisfies Array< 44 - TextProps['fontFamily'] 38 + const colors = [...tokenKeys(tokens.foregroundColor), 'inherit'] satisfies Array< 39 + TextProps['color'] 45 40 >; 46 41 47 - const fontSizes = tokenKeys(tokens.fontSize) satisfies Array< 48 - TextProps['fontSize'] 49 - >; 42 + const fontFamilies = tokenKeys(tokens.fontFamily) satisfies Array<TextProps['fontFamily']>; 43 + 44 + const fontSizes = tokenKeys(tokens.fontSize) satisfies Array<TextProps['fontSize']>; 50 45 51 46 const headingFontSizes = fontSizes.filter((fontSize) => 52 - ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'large', 'xlarge', 'xxlarge'].includes( 53 - fontSize, 54 - ), 47 + ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'large', 'xlarge', 'xxlarge'].includes(fontSize), 55 48 ); 56 49 57 - const fontWeights = [ 58 - ...tokenKeys(tokens.fontWeight), 59 - 'inherit', 60 - ] satisfies Array<TextProps['fontWeight']>; 61 - 62 - const lineHeights = tokenKeys(tokens.lineHeight) satisfies Array< 63 - TextProps['lineHeight'] 50 + const fontWeights = [...tokenKeys(tokens.fontWeight), 'inherit'] satisfies Array< 51 + TextProps['fontWeight'] 64 52 >; 65 53 54 + const lineHeights = tokenKeys(tokens.lineHeight) satisfies Array<TextProps['lineHeight']>; 55 + 66 56 export type LineClampOption = NonNullable<TextProps['lineClamp']>; 67 - const lineClampOptions: ReadonlyArray<LineClampOption> = [ 68 - false, 69 - true, 70 - 1, 71 - 2, 72 - 3, 73 - 4, 74 - 5, 75 - ]; 57 + const lineClampOptions: ReadonlyArray<LineClampOption> = [false, true, 1, 2, 3, 4, 5]; 76 58 77 59 /** 78 60 * `Text` is the base typography primitive for paragraph and inline copy. ··· 91 73 render: (props) => ( 92 74 <div style={stackContainerStyle}> 93 75 {fontSizes.map((fontSize) => ( 94 - <Text 95 - fontSize={fontSize} 96 - key={fontSize} 97 - lineHeight="tight" 98 - style={panelStyle} 99 - {...props} 100 - > 76 + <Text fontSize={fontSize} key={fontSize} lineHeight="tight" style={panelStyle} {...props}> 101 77 {fontSize}: {storyText} 102 78 </Text> 103 79 ))} ··· 161 137 render: (props) => ( 162 138 <div style={stackContainerStyle}> 163 139 {lineHeights.map((lineHeight) => ( 164 - <Text 165 - key={lineHeight} 166 - lineHeight={lineHeight} 167 - style={panelStyle} 168 - {...props} 169 - > 140 + <Text key={lineHeight} lineHeight={lineHeight} style={panelStyle} {...props}> 170 141 {lineHeight}: {loremIpsum} 171 142 </Text> 172 143 ))}
+1 -6
packages/@luke-ui/react/src/visuals/icon/icon.stories.tsx
··· 17 17 title: 'add', 18 18 } as const satisfies Partial<IconProps>; 19 19 20 - const iconSizes: Array<NonNullable<IconProps['size']>> = [ 21 - 'xsmall', 22 - 'small', 23 - 'medium', 24 - 'large', 25 - ]; 20 + const iconSizes: Array<NonNullable<IconProps['size']>> = ['xsmall', 'small', 'medium', 'large']; 26 21 const colors = tokenKeys(tokens.foregroundColor); 27 22 28 23 const wrapStyle = {
+2 -9
packages/@luke-ui/react/src/actions/button/composed/button.stories.tsx
··· 15 15 children: 'Button', 16 16 } satisfies Partial<ButtonProps>; 17 17 18 - const tones: Array<NonNullable<ButtonProps['tone']>> = [ 19 - 'primary', 20 - 'critical', 21 - 'ghost', 22 - 'neutral', 23 - ]; 18 + const tones: Array<NonNullable<ButtonProps['tone']>> = ['primary', 'critical', 'ghost', 'neutral']; 24 19 25 20 const sizes: Array<NonNullable<ButtonProps['size']>> = ['small', 'medium']; 26 21 ··· 62 57 export const Tone = meta.story({ 63 58 args: baseArgs, 64 59 play: async ({ canvas }) => { 65 - await expect( 66 - canvas.getByRole('button', { name: 'primary' }), 67 - ).toBeInTheDocument(); 60 + await expect(canvas.getByRole('button', { name: 'primary' })).toBeInTheDocument(); 68 61 }, 69 62 render: (props) => ( 70 63 <div style={rowStyle}>
+1 -3
packages/@luke-ui/react/src/actions/button/composed/close-button.stories.tsx
··· 18 18 19 19 export const Default = meta.story({ 20 20 play: async ({ canvas }) => { 21 - await expect( 22 - canvas.getByRole('button', { name: 'Close' }), 23 - ).toBeInTheDocument(); 21 + await expect(canvas.getByRole('button', { name: 'Close' })).toBeInTheDocument(); 24 22 }, 25 23 }); 26 24
+2 -12
packages/@luke-ui/react/src/actions/button/composed/close-button.tsx
··· 2 2 import type { IconButtonProps } from './icon-button.js'; 3 3 import { IconButton } from './icon-button.js'; 4 4 5 - export interface CloseButtonProps extends Omit< 6 - IconButtonProps, 7 - 'children' | 'icon' 8 - > {} 5 + export interface CloseButtonProps extends Omit<IconButtonProps, 'children' | 'icon'> {} 9 6 10 7 export function CloseButton(props: CloseButtonProps): JSX.Element { 11 8 const { tone = 'ghost', ...iconButtonProps } = props; 12 9 13 - return ( 14 - <IconButton 15 - {...iconButtonProps} 16 - aria-label="Close" 17 - icon="close" 18 - tone={tone} 19 - /> 20 - ); 10 + return <IconButton {...iconButtonProps} aria-label="Close" icon="close" tone={tone} />; 21 11 }
+1 -3
packages/@luke-ui/react/src/actions/button/composed/icon-button.stories.tsx
··· 25 25 export const Default = meta.story({ 26 26 args: { ...baseArgs, 'aria-label': 'Add' }, 27 27 play: async ({ canvas }) => { 28 - await expect( 29 - canvas.getByRole('button', { name: 'Add' }), 30 - ).toBeInTheDocument(); 28 + await expect(canvas.getByRole('button', { name: 'Add' })).toBeInTheDocument(); 31 29 }, 32 30 }); 33 31
+3 -12
packages/@luke-ui/react/src/feedback/loading-spinner/primitives/loading-spinner.tsx
··· 39 39 const normalizedMin = Math.min(minValue, maxValue); 40 40 const normalizedMax = Math.max(minValue, maxValue); 41 41 const clampedRange = Math.max(normalizedMax - normalizedMin, 1); 42 - const clampedValue = hasValue 43 - ? clamp(value, normalizedMin, normalizedMax) 44 - : 0; 42 + const clampedValue = hasValue ? clamp(value, normalizedMin, normalizedMax) : 0; 45 43 const progress = ((clampedValue - normalizedMin) / clampedRange) * 100; 46 44 const dashOffset = 100 - progress; 47 45 ··· 60 58 role="progressbar" 61 59 style={style} 62 60 > 63 - <svg 64 - aria-hidden="true" 65 - className={styles.svg} 66 - fill="none" 67 - viewBox={ICON_VIEWBOX} 68 - > 61 + <svg aria-hidden="true" className={styles.svg} fill="none" viewBox={ICON_VIEWBOX}> 69 62 <circle 70 63 className={cx( 71 64 styles.indicator, 72 - hasValue 73 - ? styles.indicatorDeterminate 74 - : styles.indicatorIndeterminate, 65 + hasValue ? styles.indicatorDeterminate : styles.indicatorIndeterminate, 75 66 )} 76 67 cx={ICON_VIEWBOX_SIZE / 2} 77 68 cy={ICON_VIEWBOX_SIZE / 2}
+1 -3
packages/@luke-ui/react/src/typography/text/composed/emoji.stories.tsx
··· 34 34 export const Default = meta.story({ 35 35 args: baseArgs, 36 36 play: async ({ canvas }) => { 37 - await expect( 38 - canvas.getByRole('img', { name: 'Celebration' }), 39 - ).toBeInTheDocument(); 37 + await expect(canvas.getByRole('img', { name: 'Celebration' })).toBeInTheDocument(); 40 38 }, 41 39 render: (props) => ( 42 40 <div style={rowStyle}>
+1 -4
packages/@luke-ui/react/src/typography/text/composed/emoji.tsx
··· 2 2 import type { TextProps } from '../primitives/text.js'; 3 3 import { Text } from '../primitives/text.js'; 4 4 5 - export interface EmojiProps extends DistributiveOmit< 6 - TextProps, 7 - 'children' | 'elementType' 8 - > { 5 + export interface EmojiProps extends DistributiveOmit<TextProps, 'children' | 'elementType'> { 9 6 emoji: string; 10 7 label: string; 11 8 }
+3 -10
packages/@luke-ui/react/src/typography/text/composed/heading-context.tsx
··· 6 6 const DEFAULT_LEVEL = 2; 7 7 8 8 /** Clamp a number to the valid heading range. */ 9 - const clampLevel = (n: number): HeadingLevel => 10 - Math.max(MIN_LEVEL, Math.min(6, n)) as HeadingLevel; 9 + const clampLevel = (n: number): HeadingLevel => Math.max(MIN_LEVEL, Math.min(6, n)) as HeadingLevel; 11 10 12 11 /** 13 12 * Context for heading levels (1–6). ··· 111 110 children: ReactNode; 112 111 }; 113 112 114 - export function HeadingPresenceProvider({ 115 - children, 116 - }: HeadingPresenceProviderProps) { 117 - return ( 118 - <WithinHeadingContext.Provider value={true}> 119 - {children} 120 - </WithinHeadingContext.Provider> 121 - ); 113 + export function HeadingPresenceProvider({ children }: HeadingPresenceProviderProps) { 114 + return <WithinHeadingContext.Provider value={true}>{children}</WithinHeadingContext.Provider>; 122 115 }
+3 -9
packages/@luke-ui/react/src/typography/text/composed/heading.stories.tsx
··· 16 16 maxInlineSize: '40rem', 17 17 } as const satisfies CSSProperties; 18 18 19 - const levels = [1, 2, 3, 4, 5, 6] as const satisfies Array< 20 - NonNullable<HeadingProps['level']> 21 - >; 19 + const levels = [1, 2, 3, 4, 5, 6] as const satisfies Array<NonNullable<HeadingProps['level']>>; 22 20 23 21 /** 24 22 * Use `level` to define heading hierarchy and default heading size. 25 23 */ 26 24 export const Level = meta.story({ 27 25 play: async ({ canvas }) => { 28 - await expect( 29 - canvas.getByRole('heading', { name: /Level 1/ }), 30 - ).toBeInTheDocument(); 26 + await expect(canvas.getByRole('heading', { name: /Level 1/ })).toBeInTheDocument(); 31 27 }, 32 28 render: (props) => ( 33 29 <div style={stackStyle}> ··· 90 86 }, 91 87 render: (props) => ( 92 88 <div style={{ inlineSize: '20rem' }}> 93 - <Heading {...props}> 94 - A flat-file CMS stores content in files rather than a database. 95 - </Heading> 89 + <Heading {...props}>A flat-file CMS stores content in files rather than a database.</Heading> 96 90 </div> 97 91 ), 98 92 });
+13 -22
packages/@luke-ui/react/src/typography/text/composed/numeral.stories.tsx
··· 62 62 */ 63 63 export const Units = meta.story({ 64 64 args: baseArgs, 65 - render: (props) => ( 66 - <Numeral {...props} unit="kilometer-per-hour" value={98} /> 67 - ), 65 + render: (props) => <Numeral {...props} unit="kilometer-per-hour" value={98} />, 68 66 }); 69 67 70 68 /** ··· 103 101 render: (props) => ( 104 102 <div style={stackStyle}> 105 103 <Heading level={2}> 106 - Acme Corporation shares hit{' '} 107 - <Numeral {...props} abbreviate value={1_456_789} /> today 104 + Acme Corporation shares hit <Numeral {...props} abbreviate value={1_456_789} /> today 108 105 </Heading> 109 106 <Text> 110 - We asked investors which private company&apos;s stock they would most 111 - like to own. More than{' '} 112 - <Numeral {...props} format="percent" value={0.80123} /> of respondents 113 - picked Acme Corp. 107 + We asked investors which private company&apos;s stock they would most like to own. More than{' '} 108 + <Numeral {...props} format="percent" value={0.80123} /> of respondents picked Acme Corp. 114 109 </Text> 115 110 <Text> 116 - Hooli, Acme&apos;s parent component, went public earlier in the year. 117 - The median commitment was{' '} 118 - <Numeral {...props} currency="AUD" precision={0} value={1_000} /> though 119 - the average was significantly higher. 111 + Hooli, Acme&apos;s parent component, went public earlier in the year. The median commitment 112 + was <Numeral {...props} currency="AUD" precision={0} value={1_000} /> though the average was 113 + significantly higher. 120 114 </Text> 121 115 </div> 122 116 ), ··· 130 124 render: () => ( 131 125 <div style={stackStyle}> 132 126 <Heading level={2}> 133 - Acme Corporation shares hit <Numeral value={1_456_789} abbreviate />{' '} 134 - today 127 + Acme Corporation shares hit <Numeral value={1_456_789} abbreviate /> today 135 128 </Heading> 136 129 <Text> 137 - We asked investors which private company’s stock they would most like to 138 - own. More than <Numeral value={0.80123} format="percent" /> of 139 - respondents picked Acme Corp. 130 + We asked investors which private company’s stock they would most like to own. More than{' '} 131 + <Numeral value={0.80123} format="percent" /> of respondents picked Acme Corp. 140 132 </Text> 141 133 <Text> 142 - Hooli, Acme’s parent component, went public earlier in the year. The 143 - median commitment was{' '} 144 - <Numeral value={1_000} currency="AUD" precision={0} /> though the 145 - average was significantly higher. 134 + Hooli, Acme’s parent component, went public earlier in the year. The median commitment was{' '} 135 + <Numeral value={1_000} currency="AUD" precision={0} /> though the average was significantly 136 + higher. 146 137 </Text> 147 138 </div> 148 139 ),
+9 -27
packages/@luke-ui/react/src/typography/text/composed/numeral.tsx
··· 7 7 export type NumeralAbbreviation = boolean | 'long'; 8 8 export type NumeralPrecision = number | readonly [number, number]; 9 9 10 - export interface NumeralProps extends Omit< 11 - TextProps, 12 - 'children' | 'textAlign' | 'variant' 13 - > { 10 + export interface NumeralProps extends Omit<TextProps, 'children' | 'textAlign' | 'variant'> { 14 11 abbreviate?: NumeralAbbreviation; 15 12 textAlign?: TextProps['textAlign']; 16 13 currency?: string; ··· 37 34 const hasUnit = unit ?? formatOptions?.unit; 38 35 39 36 if (hasCurrency && hasUnit) { 40 - throw new Error( 41 - 'Numeral cannot format both `currency` and `unit` at once.', 42 - ); 37 + throw new Error('Numeral cannot format both `currency` and `unit` at once.'); 43 38 } 44 39 45 40 if (format === 'currency' && hasCurrency === undefined) { 46 - throw new Error( 47 - 'Numeral with format="currency" requires a `currency` code.', 48 - ); 41 + throw new Error('Numeral with format="currency" requires a `currency` code.'); 49 42 } 50 43 51 44 if (format === 'unit' && hasUnit === undefined) { ··· 58 51 59 52 if (typeof precision === 'number') { 60 53 if (!isValidPrecisionValue(precision)) { 61 - throw new Error( 62 - 'Numeral `precision` must be a non-negative integer or tuple.', 63 - ); 54 + throw new Error('Numeral `precision` must be a non-negative integer or tuple.'); 64 55 } 65 56 return; 66 57 } ··· 70 61 !isValidPrecisionValue(precision[1]) || 71 62 precision[0] > precision[1] 72 63 ) { 73 - throw new Error( 74 - 'Numeral `precision` tuple must be [min, max] non-negative integers.', 75 - ); 64 + throw new Error('Numeral `precision` tuple must be [min, max] non-negative integers.'); 76 65 } 77 66 } 78 67 ··· 99 88 } = props; 100 89 const resolvedLocale = locale ?? localeFromContext; 101 90 102 - const resolvedFormat = 103 - format ?? (currency ? 'currency' : unit ? 'unit' : 'decimal'); 91 + const resolvedFormat = format ?? (currency ? 'currency' : unit ? 'unit' : 'decimal'); 104 92 105 93 const numeralFormatOptions: Intl.NumberFormatOptions = { 106 94 ...formatOptions, ··· 109 97 if (resolvedFormat === 'currency') { 110 98 const resolvedCurrency = currency ?? formatOptions?.currency; 111 99 if (resolvedCurrency === undefined) { 112 - throw new Error( 113 - 'Numeral with format="currency" requires a `currency` code.', 114 - ); 100 + throw new Error('Numeral with format="currency" requires a `currency` code.'); 115 101 } 116 102 numeralFormatOptions.currency = resolvedCurrency; 117 103 } ··· 126 112 } 127 113 128 114 if (abbreviate) { 129 - numeralFormatOptions.compactDisplay = 130 - abbreviate === 'long' ? 'long' : 'short'; 115 + numeralFormatOptions.compactDisplay = abbreviate === 'long' ? 'long' : 'short'; 131 116 numeralFormatOptions.notation = 'compact'; 132 117 } 133 118 ··· 141 126 } 142 127 } 143 128 144 - const content = new Intl.NumberFormat( 145 - resolvedLocale, 146 - numeralFormatOptions, 147 - ).format(value); 129 + const content = new Intl.NumberFormat(resolvedLocale, numeralFormatOptions).format(value); 148 130 const resolvedShouldDisableTrim = shouldDisableTrim ?? isWithinHeading; 149 131 const resolvedColor = color ?? (isWithinHeading ? 'inherit' : undefined); 150 132 const resolvedStyle =
+2 -7
packages/@luke-ui/react/src/visuals/icon/primitives/icon-size-context.tsx
··· 9 9 size: IconSizeToken; 10 10 } 11 11 12 - export function IconSizeProvider({ 13 - children, 14 - size, 15 - }: IconSizeProviderProps): JSX.Element { 16 - return ( 17 - <IconSizeContext.Provider value={size}>{children}</IconSizeContext.Provider> 18 - ); 12 + export function IconSizeProvider({ children, size }: IconSizeProviderProps): JSX.Element { 13 + return <IconSizeContext.Provider value={size}>{children}</IconSizeContext.Provider>; 19 14 } 20 15 21 16 export function useIconSizeContext(): IconSizeToken | null {
+5 -24
packages/@luke-ui/react/src/visuals/icon/primitives/icon.tsx
··· 22 22 children, 23 23 href, 24 24 }: IconSpritesheetProviderProps): JSX.Element { 25 - return ( 26 - <IconSpritesheetContext.Provider value={href}> 27 - {children} 28 - </IconSpritesheetContext.Provider> 29 - ); 25 + return <IconSpritesheetContext.Provider value={href}>{children}</IconSpritesheetContext.Provider>; 30 26 } 31 27 32 28 function useIconSpritesheetHref(): string { ··· 50 46 51 47 export type CustomIconProps = DistributiveOmit<IconProps, 'name'>; 52 48 53 - export interface CreateIconOptions< 54 - TProps extends CustomIconProps = CustomIconProps, 55 - > { 49 + export interface CreateIconOptions<TProps extends CustomIconProps = CustomIconProps> { 56 50 path: ReactNode | ((props: TProps) => ReactNode); 57 51 viewBox?: string | ((props: TProps) => string | undefined); 58 52 } ··· 62 56 viewBox: defaultViewBox = ICON_VIEWBOX, 63 57 }: CreateIconOptions<TProps>): (props: TProps) => JSX.Element { 64 58 return function Icon(props: TProps): JSX.Element { 65 - const { 66 - 'aria-hidden': ariaHiddenProp, 67 - className, 68 - id, 69 - size, 70 - style, 71 - title, 72 - viewBox, 73 - } = props; 59 + const { 'aria-hidden': ariaHiddenProp, className, id, size, style, title, viewBox } = props; 74 60 const ariaHidden = ariaHiddenProp ?? !title; 75 61 const role = ariaHidden ? undefined : 'img'; 76 62 const resolvedViewBox = 77 - viewBox ?? 78 - (typeof defaultViewBox === 'function' 79 - ? defaultViewBox(props) 80 - : defaultViewBox); 63 + viewBox ?? (typeof defaultViewBox === 'function' ? defaultViewBox(props) : defaultViewBox); 81 64 const resolvedPath = typeof path === 'function' ? path(props) : path; 82 65 const contextSize = useIconSizeContext(); 83 66 const resolvedSize = size ?? contextSize ?? 'medium'; ··· 108 91 }; 109 92 110 93 const SpritesheetIcon = createIcon<SpritesheetIconProps>({ 111 - path: ({ name, spritesheetHref }) => ( 112 - <use href={`${spritesheetHref}#${name}`} /> 113 - ), 94 + path: ({ name, spritesheetHref }) => <use href={`${spritesheetHref}#${name}`} />, 114 95 viewBox: ({ name }) => iconViewBoxes[name], 115 96 }); 116 97