diff --git a/.changeset/config.json b/.changeset/config.json index 0533df50..bfce634e 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,10 +1,10 @@ { "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", + "access": "public", "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], + "ignore": ["docs"], "linked": [], - "access": "public", - "updateInternalDependencies": "patch", - "ignore": ["docs"] + "updateInternalDependencies": "patch" } diff --git a/.oxlintrc.json b/.oxlintrc.json index cfe7ee94..28e1d262 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -8,8 +8,8 @@ "**/storybook-static/**", "**/routeTree.gen.ts" ], - "plugins": ["import", "jsx-a11y", "react", "typescript", "vitest"], "jsPlugins": [{ "name": "react-hooks-js", "specifier": "eslint-plugin-react-hooks" }], + "plugins": ["import", "jsx-a11y", "react", "typescript", "vitest"], "rules": { "@typescript-eslint/array-type": ["error", { "default": "generic" }], "@typescript-eslint/consistent-type-exports": [ @@ -18,7 +18,7 @@ ], "@typescript-eslint/consistent-type-imports": [ "error", - { "prefer": "type-imports", "fixStyle": "separate-type-imports" } + { "fixStyle": "separate-type-imports", "prefer": "type-imports" } ], "curly": "off", "import/consistent-type-specifier-style": ["error", "prefer-top-level"], @@ -29,14 +29,12 @@ "jsdoc/empty-tags": "error", "no-console": "warn", "no-unused-vars": "error", - "react/no-array-index-key": "off", - "react/exhaustive-deps": "warn", - "react/rules-of-hooks": "error", "react-hooks-js/config": "error", "react-hooks-js/error-boundaries": "error", "react-hooks-js/gating": "error", "react-hooks-js/globals": "error", "react-hooks-js/immutability": "error", + "react-hooks-js/incompatible-library": "warn", "react-hooks-js/preserve-manual-memoization": "error", "react-hooks-js/purity": "error", "react-hooks-js/refs": "error", @@ -45,7 +43,9 @@ "react-hooks-js/static-components": "error", "react-hooks-js/unsupported-syntax": "warn", "react-hooks-js/use-memo": "error", - "react-hooks-js/incompatible-library": "warn", + "react/exhaustive-deps": "warn", + "react/no-array-index-key": "off", + "react/rules-of-hooks": "error", "sort-imports": "off" } } diff --git a/.vscode/settings.json b/.vscode/settings.json index c8a845af..7e20a07b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { - "editor.defaultFormatter": "oxc.oxc-vscode", "[css]": { "editor.defaultFormatter": "oxc.oxc-vscode" }, @@ -28,6 +27,7 @@ "source.fixAll.oxc": "explicit", "source.organizeImports.oxc": "explicit" }, + "editor.defaultFormatter": "oxc.oxc-vscode", "editor.formatOnSave": true, "files.readonlyInclude": { "**/routeTree.gen.ts": true diff --git a/CONTEXT.md b/CONTEXT.md index 6d2c4179..3f571f39 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -47,3 +47,4 @@ Don't document what isn't part of the public API. - [ADR-0001](docs/adr/0001-component-tier-taxonomy.md) — Three-tier taxonomy and docs rule - [ADR-0002](docs/adr/0002-primitive-package-path-convention.md) — Primitive kits exported at `[composed]/primitive` - [ADR-0003](docs/adr/0003-package-docs-surface.md) — Package docs are a separate AI-native surface +- [ADR-0004](docs/adr/0004-styling-utilities-public-api.md) — Styling utilities as public API diff --git a/apps/docs/source.config.ts b/apps/docs/source.config.ts index 1a228f59..60232132 100644 --- a/apps/docs/source.config.ts +++ b/apps/docs/source.config.ts @@ -15,9 +15,9 @@ export const docs = defineDocs({ // Safe because no hosted MDX file authors an inline `# H1` or leading // blockquote — titles and descriptions always come from frontmatter. interface MdastNode { - type?: string; - depth?: number; children?: Array; + depth?: number; + type?: string; } function stripIncludedHeader() { diff --git a/apps/docs/src/components/page-actions.tsx b/apps/docs/src/components/page-actions.tsx index 5b6d3c03..b1b08d43 100644 --- a/apps/docs/src/components/page-actions.tsx +++ b/apps/docs/src/components/page-actions.tsx @@ -3,8 +3,8 @@ import { CheckIcon, CopyIcon, ExternalLinkIcon, PencilIcon } from 'lucide-react' import { useState } from 'react'; interface PageActionsProps { - markdownUrl: string; githubUrl: string; + markdownUrl: string; } export function PageActions({ markdownUrl, githubUrl }: PageActionsProps) { diff --git a/apps/docs/src/components/search.tsx b/apps/docs/src/components/search.tsx index 6fbe2a2e..d64fb31e 100644 --- a/apps/docs/src/components/search.tsx +++ b/apps/docs/src/components/search.tsx @@ -16,9 +16,9 @@ import { useI18n } from 'fumadocs-ui/contexts/i18n'; function initOrama() { return create({ - schema: { _: 'string' }, // https://docs.orama.com/docs/orama-js/supported-languages language: 'english', + schema: { _: 'string' }, }); } diff --git a/apps/docs/src/icon-button/icon-button.story.tsx b/apps/docs/src/icon-button/icon-button.story.tsx index f30495ef..3c811041 100644 --- a/apps/docs/src/icon-button/icon-button.story.tsx +++ b/apps/docs/src/icon-button/icon-button.story.tsx @@ -2,6 +2,6 @@ import type { IconButton } from '@luke-ui/react/icon-button'; import { defineComponentStory } from '../lib/define-component-story'; export const story = defineComponentStory(import.meta.url, { - initial: { icon: 'add', 'aria-label': 'Add' }, + initial: { 'aria-label': 'Add', icon: 'add' }, priorities: ['icon', 'aria-label', 'tone', 'size', 'isDisabled'], }); diff --git a/apps/docs/src/lib/define-component-story.ts b/apps/docs/src/lib/define-component-story.ts index e5e1f322..f9597601 100644 --- a/apps/docs/src/lib/define-component-story.ts +++ b/apps/docs/src/lib/define-component-story.ts @@ -11,8 +11,8 @@ export function defineComponentStory>( ) { return defineStory(url, { args: { - initial: options.initial, controls: { transform: reorderProps(options.priorities) }, + initial: options.initial, }, }); } diff --git a/apps/docs/src/link/link.story.tsx b/apps/docs/src/link/link.story.tsx index c372c9b7..6d095df0 100644 --- a/apps/docs/src/link/link.story.tsx +++ b/apps/docs/src/link/link.story.tsx @@ -2,6 +2,6 @@ import type { Link } from '@luke-ui/react/link'; import { defineComponentStory } from '../lib/define-component-story'; export const story = defineComponentStory(import.meta.url, { - initial: { href: '#', children: 'Link' }, + initial: { children: 'Link', href: '#' }, priorities: ['children', 'href', 'tone', 'isStandalone'], }); diff --git a/apps/docs/src/routes/docs/$.tsx b/apps/docs/src/routes/docs/$.tsx index 013808c8..55c982d8 100644 --- a/apps/docs/src/routes/docs/$.tsx +++ b/apps/docs/src/routes/docs/$.tsx @@ -20,7 +20,9 @@ const GITHUB_DOCS_URL = 'https://github.com/lukebennett88/luke-ui/blob/main/apps const storyModules = import.meta.glob<{ story: Story }>('../../*/*.story.tsx', { eager: true }); const clientStoryModules = import.meta.glob<{ storyClient: StoryClient }>( '../../*/*.story.client.tsx', - { eager: true }, + { + eager: true, + }, ); const dirName = (globPath: string) => globPath.split('/').at(-2)!; @@ -90,7 +92,7 @@ function Page() { return ( - + {clientLoader.useContent(data.path, { className: '', diff --git a/apps/docs/src/routes/llms[.]txt.ts b/apps/docs/src/routes/llms[.]txt.ts index d1dd1cf7..0aab920f 100644 --- a/apps/docs/src/routes/llms[.]txt.ts +++ b/apps/docs/src/routes/llms[.]txt.ts @@ -23,9 +23,9 @@ export const Route = createFileRoute('/llms.txt')({ }); const txt = renderIndex({ - packageName: packageJson.name, entries: entriesWithHref, includeLibraryAuthors: false, + packageName: packageJson.name, }); return new Response(txt, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, diff --git a/apps/docs/src/text-field/text-field.story.tsx b/apps/docs/src/text-field/text-field.story.tsx index d8492ef2..25d014db 100644 --- a/apps/docs/src/text-field/text-field.story.tsx +++ b/apps/docs/src/text-field/text-field.story.tsx @@ -2,6 +2,6 @@ import type { TextField } from '@luke-ui/react/text-field'; import { defineComponentStory } from '../lib/define-component-story'; export const story = defineComponentStory(import.meta.url, { - initial: { name: 'email', label: 'Email' }, + initial: { label: 'Email', name: 'email' }, priorities: ['name', 'label', 'description', 'placeholder', 'size', 'isRequired'], }); diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index b5018418..130a07c4 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -1,5 +1,4 @@ { - "include": ["**/*.ts", "**/*.tsx"], "compilerOptions": { "allowJs": true, "forceConsistentCasingInFileNames": true, @@ -18,5 +17,6 @@ "target": "esnext", "types": ["vite/client"], "verbatimModuleSyntax": true - } + }, + "include": ["**/*.ts", "**/*.tsx"] } diff --git a/apps/docs/vite.config.ts b/apps/docs/vite.config.ts index d1f70366..863cb529 100644 --- a/apps/docs/vite.config.ts +++ b/apps/docs/vite.config.ts @@ -19,8 +19,8 @@ import { mapPublicToInternal, toInternal, toPublic } from './src/lib/markdown-ur // the original source paths. function storySourcePathPlugin(): Plugin { return { - name: 'story-source-path', enforce: 'pre', + name: 'story-source-path', transform(code, id) { const cleanId = id.split('?')[0] ?? ''; if (!cleanId.endsWith('.story.tsx') && !cleanId.endsWith('/lib/story.ts')) return null; @@ -52,21 +52,21 @@ function staticFunctionBasePathPlugin(): Plugin { function markdownRewritePlugin(): Plugin { return { - name: 'markdown-rewrite', - configureServer(server) { + configurePreviewServer(server) { server.middlewares.use((req, _res, next) => { const rewritten = mapPublicToInternal(req.url); if (rewritten) req.url = rewritten; next(); }); }, - configurePreviewServer(server) { + configureServer(server) { server.middlewares.use((req, _res, next) => { const rewritten = mapPublicToInternal(req.url); if (rewritten) req.url = rewritten; next(); }); }, + name: 'markdown-rewrite', }; } @@ -85,11 +85,21 @@ function packageDocsPlugin(): Plugin { const id = 'virtual:package-docs'; const resolved = `\0${id}`; return { - name: 'package-docs', - enforce: 'pre', - resolveId(source) { - return source === id ? resolved : null; + configureServer(server) { + server.watcher.add(packageDocsDir); + const handle = (filePath: string) => { + if (!filePath.endsWith('.mdx') || !filePath.startsWith(packageDocsDir)) return; + const mod = server.moduleGraph.getModuleById(resolved); + if (mod) { + server.moduleGraph.invalidateModule(mod); + server.ws.send({ type: 'full-reload' }); + } + }; + server.watcher.on('add', handle); + server.watcher.on('change', handle); + server.watcher.on('unlink', handle); }, + enforce: 'pre', async load(loadId) { if (loadId !== resolved) return null; const filenames = (await readdir(packageDocsDir)).filter((n) => n.endsWith('.mdx')); @@ -104,19 +114,9 @@ function packageDocsPlugin(): Plugin { ); return `export const packageDocs = ${JSON.stringify(Object.fromEntries(entries))};`; }, - configureServer(server) { - server.watcher.add(packageDocsDir); - const handle = (filePath: string) => { - if (!filePath.endsWith('.mdx') || !filePath.startsWith(packageDocsDir)) return; - const mod = server.moduleGraph.getModuleById(resolved); - if (mod) { - server.moduleGraph.invalidateModule(mod); - server.ws.send({ type: 'full-reload' }); - } - }; - server.watcher.on('add', handle); - server.watcher.on('change', handle); - server.watcher.on('unlink', handle); + name: 'package-docs', + resolveId(source) { + return source === id ? resolved : null; }, }; } @@ -193,8 +193,8 @@ function packageSourceWatcherPlugin(): Plugin { logger.info('[package-docs] regenerating @luke-ui/react docs...'); const child = spawn('pnpm', ['--filter', '@luke-ui/react', 'generate:docs'], { cwd: packageRootDir, - stdio: ['ignore', 'pipe', 'pipe'], shell: false, + stdio: ['ignore', 'pipe', 'pipe'], }); inFlight = child; @@ -234,13 +234,12 @@ function packageSourceWatcherPlugin(): Plugin { } return { - name: 'package-source-watcher', apply: 'serve', configureServer(server) { server.watcher.add(packageSrcDir); const logger = { - info: (msg: string) => server.config.logger.info(msg, { timestamp: true }), error: (msg: string) => server.config.logger.error(msg, { timestamp: true }), + info: (msg: string) => server.config.logger.info(msg, { timestamp: true }), }; const handleChange = (filePath: string) => { if (!shouldTrigger(filePath)) return; @@ -250,6 +249,7 @@ function packageSourceWatcherPlugin(): Plugin { server.watcher.on('change', handleChange); server.watcher.on('unlink', handleChange); }, + name: 'package-source-watcher', }; } @@ -284,10 +284,6 @@ export default defineConfig(async () => { optimizeDeps: { exclude: ['@luke-ui/react'], }, - resolve: { - external: ['ts-morph'], - tsconfigPaths: true, - }, plugins: [ storySourcePathPlugin(), staticFunctionBasePathPlugin(), @@ -297,21 +293,25 @@ export default defineConfig(async () => { mdx(await import('./source.config')), tailwindcss(), tanstackStart({ - prerender: { - enabled: true, - crawlLinks: true, - filter: (page) => !page.path.endsWith('.mdx') && !page.path.endsWith('.md'), - }, pages: [ { path: '/api/search' }, { path: '/llms.txt' }, { path: '/llms-full.txt' }, ...markdownPrerenderPages, ], + prerender: { + crawlLinks: true, + enabled: true, + filter: (page) => !page.path.endsWith('.mdx') && !page.path.endsWith('.md'), + }, }), react(), nitro(), ], + resolve: { + external: ['ts-morph'], + tsconfigPaths: true, + }, server: { port: 3000, }, diff --git a/docs/STYLING.md b/docs/STYLING.md index 6682840d..a86f5445 100644 --- a/docs/STYLING.md +++ b/docs/STYLING.md @@ -33,6 +33,118 @@ Recipes are public and can be imported from `@luke-ui/react/recipes`. import { button, link } from '@luke-ui/react/recipes'; ``` +## Styling Utilities + +Styling utilities are public API, exported from `@luke-ui/react/styles`. They +provide token-aware, type-safe layout and styling helpers for cases where +component props are too narrow. + +The current implementation is vendored `rainbow-sprinkles` in +`@luke-ui/rainbow-sprinkles`. It places all generated classes in the +`utilities` cascade layer and supports responsive conditions and pseudo-state +conditions out of the box. + +### `createSprinkles()` — layout utility + +`createSprinkles(props)` returns `{ className, style }`. Spread both onto any element: + +```tsx +import { createSprinkles } from '@luke-ui/react/styles'; + +const layout = createSprinkles({ + display: 'flex', + gap: 'medium', + padding: 'large', +}); + +return ( +
+ … +
+); +``` + +Token-backed properties use the design-token scale (e.g. `padding: 'large'`). +Enum-like properties use CSS-native values (e.g. `display: 'flex'`). Numeric +flex properties use string primitives (`flexGrow: '1'`). + +### Responsive values + +Use object notation keyed by breakpoint names. Values cascade from smaller to +larger breakpoints, so only overrides need to be specified: + +```tsx +const responsive = createSprinkles({ + display: 'flex', + flexDirection: { xsmall: 'column', medium: 'row' }, + gap: { xsmall: 'small', medium: 'large' }, +}); +``` + +### Pseudo-state conditions + +Use condition objects for `hover` and `focus-visible` on supported properties: + +```tsx +const interactive = createSprinkles({ + padding: 'medium', + backgroundColor: { default: 'neutral', hover: 'neutralHover', focusVisible: 'input' }, +}); +``` + +### React Aria Components `render` prop + +When you need to style the underlying DOM element directly, combine `createSprinkles` with +RAC's `render` prop. Use `mergeProps` from `@luke-ui/react/utils` to merge the +provided DOM props with `createSprinkles()` output so `className` and `style` are +concatenated correctly: + +```tsx +import { mergeProps } from '@luke-ui/react/utils'; + +const buttonBox = createSprinkles({ padding: 'medium' }); + + + )} +> + Save +; +``` + +### Shorthands + +`px` and `py` expand to both inline or block padding sides. `p` expands to all +four sides: + +```tsx +const padded = createSprinkles({ px: 'large', py: 'small' }); +``` + +### Available properties + +The v1 surface covers: + +- **Layout**: `display`, `flexDirection`, `justifyContent`, `alignItems` +- **Flex item**: `flexGrow`, `flexShrink`, `flexBasis` +- **Sizing**: `inlineSize`, `blockSize`, `minInlineSize`, `minBlockSize`, + `maxInlineSize`, `maxBlockSize` +- **Gaps**: `gap`, `rowGap`, `columnGap` +- **Padding**: `padding`, `paddingInline`, `paddingBlock`, + `paddingInlineStart`, `paddingInlineEnd`, `paddingBlockStart`, `paddingBlockEnd` +- **Overflow**: `overflow`, `overflowX`, `overflowY`, `textOverflow` +- **Pseudo-state**: `backgroundColor` (hover, focus-visible) +- **Shorthands**: `p`, `px`, `py` + +Margin utilities are excluded from the initial surface. CSS-native values are +used throughout (e.g. `flex-start` rather than `start`). + +Keep `@luke-ui/react/styles` as a separate export path from `@luke-ui/react/recipes`. +Recipes are for component-specific styles; styles are for general layout utilities. + ## Implementation - Use only CSS logical properties (e.g. `margin-inline-start`, `block-size`, `inset-inline`), not physical (`margin-left`, `height`, `left`/`right`). @@ -41,4 +153,4 @@ import { button, link } from '@luke-ui/react/recipes'; ## Limitations -No Sprinkles or runtime theme context yet. +No styling utility implementation or runtime theme context yet. diff --git a/docs/adr/0004-styling-utilities-public-api.md b/docs/adr/0004-styling-utilities-public-api.md new file mode 100644 index 00000000..c05e98bd --- /dev/null +++ b/docs/adr/0004-styling-utilities-public-api.md @@ -0,0 +1,41 @@ +# Styling utilities as public API + +Styling utilities are public API, exported from `@luke-ui/react/styles` as named +exports. They provide token-aware, type-safe layout helpers for cases where +component props are too narrow. + +## Decision + +Rainbow Sprinkles was chosen over vanilla-extract Sprinkles. Rainbow Sprinkles +uses `assignInlineVars` to emit dynamic CSS custom properties at runtime rather +than generating static classes for every token-value combination. This avoids +class-name bloat when the token scale grows and keeps the generated CSS bundle +small — the static utilities layer defines only property-level classes, not +value-level classes. The trade-off is that values are applied via inline +`style`, which raises specificity but is acceptable for the `utilities` cascade +layer (the highest-priority escape hatch). + +## Rejected alternatives + +- **Polymorphic `Box` component**: rejected because it introduces the same + polymorphism issues that make React Aria Components' `render` prop preferable. +- **Spreading style props onto every component**: rejected because it would bloat + component APIs and blur the boundary between component-specific recipes and + consumer layout utilities. +- **Library-author only API**: rejected because consumers repeatedly need safe + layout composition without inline styles, as shown by the middle-truncation + story example. + +## Consequences + +- `@luke-ui/react/styles` exports `createSprinkles()` as the public layout utility function. +- The initial surface covers layout, flex-item behavior, sizing, token-backed gaps + and padding, overflow, and text overflow. Margin utilities are excluded. +- Values are CSS-native, property names are logical where possible. +- Responsive values and pseudo-state conditions (`hover`, `focus-visible`) are in + scope for v1. +- Object notation is used for responsive values; array notation may be added later. +- Rainbow Sprinkles condition syntax is used for pseudo-states. +- Utilities live in the `utilities` cascade layer, below recipes. +- Layer helpers (`styleInLayer`, `recipeInLayer`) remain internal. +- No React render-prop helper is added until a real repeated need appears. diff --git a/knip.config.ts b/knip.config.ts index e922c2fd..b8a83e29 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -3,10 +3,6 @@ import type { KnipConfig } from 'knip'; export default { ignore: ['.claude/worktrees/**', '.worktrees/**'], workspaces: { - 'packages/turbo-generators': { - entry: ['config.ts'], - project: ['**/*.ts'], - }, 'apps/docs': { entry: [ 'source.config.ts', @@ -16,8 +12,12 @@ export default { 'src/styles/app.css', 'content/**/*.mdx', ], - project: ['src/**/*.{ts,tsx}'], ignoreDependencies: ['ts-morph'], + project: ['src/**/*.{ts,tsx}'], + }, + 'packages/@luke-ui/docs-tools': { + entry: ['src/**/__tests__/**/*.test.ts'], + project: ['src/**/*.ts'], }, 'packages/@luke-ui/react': { entry: [ @@ -33,9 +33,9 @@ export default { ], project: ['src/**/*.{ts,tsx}'], }, - 'packages/@luke-ui/docs-tools': { - entry: ['src/**/__tests__/**/*.test.ts'], - project: ['src/**/*.ts'], + 'packages/turbo-generators': { + entry: ['config.ts'], + project: ['**/*.ts'], }, }, } satisfies KnipConfig; diff --git a/packages/@luke-ui/docs-tools/package.json b/packages/@luke-ui/docs-tools/package.json index 1c58613f..6033c889 100644 --- a/packages/@luke-ui/docs-tools/package.json +++ b/packages/@luke-ui/docs-tools/package.json @@ -7,8 +7,8 @@ "./discover-exports": "./src/discover-exports.ts", "./parse-types": "./src/parse-types.ts", "./render-index": "./src/render-index.ts", - "./render-page": "./src/render-page.ts", - "./render-llms-full": "./src/render-llms-full.ts" + "./render-llms-full": "./src/render-llms-full.ts", + "./render-page": "./src/render-page.ts" }, "scripts": { "check:format": "pnpm exec oxfmt -c ../../../.oxfmtrc.json --check .", diff --git a/packages/@luke-ui/docs-tools/src/__tests__/discover-exports.test.ts b/packages/@luke-ui/docs-tools/src/__tests__/discover-exports.test.ts index 65950032..c5d19b18 100644 --- a/packages/@luke-ui/docs-tools/src/__tests__/discover-exports.test.ts +++ b/packages/@luke-ui/docs-tools/src/__tests__/discover-exports.test.ts @@ -58,9 +58,9 @@ describe('discoverExports', () => { it('classifies stylesheet/spritesheet/package.json as asset', () => { const exports = { - './stylesheet.css': './dist/stylesheet.css', - './spritesheet.svg': './dist/spritesheet.svg', './package.json': './package.json', + './spritesheet.svg': './dist/spritesheet.svg', + './stylesheet.css': './dist/stylesheet.css', }; const result = discoverExports(exports); expect(result.every((e) => e.shape === 'asset')).toBe(true); diff --git a/packages/@luke-ui/docs-tools/src/__tests__/fixtures/sample-wrapper.ts b/packages/@luke-ui/docs-tools/src/__tests__/fixtures/sample-wrapper.ts index 8dfbc6cd..fa45e1c6 100644 --- a/packages/@luke-ui/docs-tools/src/__tests__/fixtures/sample-wrapper.ts +++ b/packages/@luke-ui/docs-tools/src/__tests__/fixtures/sample-wrapper.ts @@ -1,10 +1,10 @@ interface InnerProps { - /** Inner size. @default 'inner' */ - size?: 'small' | 'medium'; /** Hidden implementation prop. */ icon: string; /** Inner press handler. */ onPress?: () => void; + /** Inner size. @default 'inner' */ + size?: 'small' | 'medium'; } interface WrapperStyleProps { diff --git a/packages/@luke-ui/docs-tools/src/__tests__/parse-types.test.ts b/packages/@luke-ui/docs-tools/src/__tests__/parse-types.test.ts index 09bb14f8..052afd7f 100644 --- a/packages/@luke-ui/docs-tools/src/__tests__/parse-types.test.ts +++ b/packages/@luke-ui/docs-tools/src/__tests__/parse-types.test.ts @@ -28,11 +28,11 @@ describe('parseComponent', () => { const parsed = parseComponent(fixturePath); const sizeProp = parsed.propsInterface?.members.find((p) => p.name === 'size'); expect(sizeProp).toMatchObject({ - name: 'size', - type: "'small' | 'medium'", - optional: true, default: "'medium'", description: expect.stringContaining('Controls the button size'), + name: 'size', + optional: true, + type: "'small' | 'medium'", }); }); @@ -50,14 +50,14 @@ describe('parseComponent', () => { expect(parsed.propsInterface?.members).toEqual( expect.arrayContaining([ expect.objectContaining({ - name: 'size', default: "'medium'", description: 'Sets the visual size.', + name: 'size', }), expect.objectContaining({ + description: 'Accessible label.', name: 'label', optional: false, - description: 'Accessible label.', }), ]), ); @@ -68,13 +68,13 @@ describe('parseComponent', () => { expect(parsed.propsInterface?.name).toBe('WrapperProps'); expect(parsed.propsInterface?.members.map((prop) => prop.name)).toEqual(['size', 'onPress']); expect(parsed.propsInterface?.members[0]).toMatchObject({ - name: 'size', default: "'wrapper'", description: 'Wrapper size.', + name: 'size', }); expect(parsed.propsInterface?.members[1]).toMatchObject({ - name: 'onPress', description: 'Wrapper press handler.', + name: 'onPress', }); }); }); diff --git a/packages/@luke-ui/docs-tools/src/__tests__/render-index.test.ts b/packages/@luke-ui/docs-tools/src/__tests__/render-index.test.ts index a3f48b7c..95af7cda 100644 --- a/packages/@luke-ui/docs-tools/src/__tests__/render-index.test.ts +++ b/packages/@luke-ui/docs-tools/src/__tests__/render-index.test.ts @@ -4,38 +4,38 @@ import { renderIndex } from '../render-index.js'; const sampleEntries: Array = [ { + description: 'Composed button.', + pageKind: 'component', path: './button', - target: '', - slug: 'button', shape: 'component', - pageKind: 'component', + slug: 'button', + target: '', tier: 'composed', - description: 'Composed button.', }, { + description: 'Bare button.', + pageKind: 'component', path: './button/primitive', - target: '', - slug: 'button-primitive', shape: 'component', - pageKind: 'component', + slug: 'button-primitive', + target: '', tier: 'primitive', - description: 'Bare button.', }, { + description: 'Design tokens.', + pageKind: 'barrel', path: './tokens', - target: '', - slug: 'tokens', shape: 'barrel', - pageKind: 'barrel', + slug: 'tokens', + target: '', tier: 'n/a', - description: 'Design tokens.', }, { + pageKind: 'asset', path: './stylesheet.css', - target: '', - slug: 'stylesheet', shape: 'asset', - pageKind: 'asset', + slug: 'stylesheet', + target: '', tier: 'n/a', }, ]; @@ -43,18 +43,18 @@ const sampleEntries: Array = [ describe('renderIndex', () => { it('lists composed components in the primary section', () => { const out = renderIndex({ - packageName: '@luke-ui/react', entries: sampleEntries, includeLibraryAuthors: false, + packageName: '@luke-ui/react', }); expect(out).toMatch(/- \[Button\]\(\.\/button\.md\)/); }); it('omits primitives when includeLibraryAuthors is false', () => { const out = renderIndex({ - packageName: '@luke-ui/react', entries: sampleEntries, includeLibraryAuthors: false, + packageName: '@luke-ui/react', }); expect(out).not.toMatch(/Button \(primitive\)/); expect(out).not.toMatch(/## Library authors/); @@ -62,9 +62,9 @@ describe('renderIndex', () => { it('includes primitives in a Library authors section when flag is true', () => { const out = renderIndex({ - packageName: '@luke-ui/react', entries: sampleEntries, includeLibraryAuthors: true, + packageName: '@luke-ui/react', }); expect(out).toMatch(/## Library authors/); expect(out).toMatch(/- \[Button \(primitive\)\]\(\.\/button-primitive\.md\)/); @@ -72,9 +72,9 @@ describe('renderIndex', () => { it('lists assets inline (no link) under Assets section', () => { const out = renderIndex({ - packageName: '@luke-ui/react', entries: sampleEntries, includeLibraryAuthors: true, + packageName: '@luke-ui/react', }); expect(out).toMatch(/## Assets/); expect(out).toMatch(/stylesheet\.css.*import '@luke-ui\/react\/stylesheet\.css'/); @@ -82,13 +82,13 @@ describe('renderIndex', () => { it('uses entry href when provided', () => { const out = renderIndex({ - packageName: '@luke-ui/react', entries: sampleEntries.map((entry) => entry.slug === 'button' ? Object.assign(entry, { href: './llms.mdx/docs/components/actions/button.md' }) : entry, ), includeLibraryAuthors: false, + packageName: '@luke-ui/react', }); expect(out).toMatch(/\[Button\]\(\.\/llms\.mdx\/docs\/components\/actions\/button\.md\)/); expect(out).toMatch(/\[Tokens\]\(\.\/tokens\.md\)/); diff --git a/packages/@luke-ui/docs-tools/src/__tests__/render-llms-full.test.ts b/packages/@luke-ui/docs-tools/src/__tests__/render-llms-full.test.ts index d148df6b..a3c88c1d 100644 --- a/packages/@luke-ui/docs-tools/src/__tests__/render-llms-full.test.ts +++ b/packages/@luke-ui/docs-tools/src/__tests__/render-llms-full.test.ts @@ -9,23 +9,23 @@ describe('sortLlmsFullEntries', () => { const entries: Array = [ // Deliberately mixed and non-alphabetic insertion order. { - slug: 'combobox-field-primitive', + md: md('combobox-field-primitive'), shape: 'component', + slug: 'combobox-field-primitive', tier: 'primitive', - md: md('combobox-field-primitive'), }, - { slug: 'tokens', shape: 'barrel', tier: 'n/a', md: md('tokens') }, - { slug: 'button', shape: 'component', tier: 'composed', md: md('button') }, - { slug: 'icon', shape: 'component', tier: 'atom', md: md('icon') }, + { md: md('tokens'), shape: 'barrel', slug: 'tokens', tier: 'n/a' }, + { md: md('button'), shape: 'component', slug: 'button', tier: 'composed' }, + { md: md('icon'), shape: 'component', slug: 'icon', tier: 'atom' }, { - slug: 'button-primitive', + md: md('button-primitive'), shape: 'component', + slug: 'button-primitive', tier: 'primitive', - md: md('button-primitive'), }, - { slug: 'recipes', shape: 'barrel', tier: 'n/a', md: md('recipes') }, - { slug: 'avatar', shape: 'component', tier: 'composed', md: md('avatar') }, - { slug: 'emoji', shape: 'component', tier: 'atom', md: md('emoji') }, + { md: md('recipes'), shape: 'barrel', slug: 'recipes', tier: 'n/a' }, + { md: md('avatar'), shape: 'component', slug: 'avatar', tier: 'composed' }, + { md: md('emoji'), shape: 'component', slug: 'emoji', tier: 'atom' }, ]; const sorted = sortLlmsFullEntries(entries).map((e) => e.slug); expect(sorted).toEqual([ @@ -46,10 +46,10 @@ describe('sortLlmsFullEntries', () => { it('drops entries that do not belong to a bucket (assets, unknown tiers)', () => { const entries: Array = [ - { slug: 'stylesheet', shape: 'asset', tier: 'n/a', md: md('stylesheet') }, - { slug: 'button', shape: 'component', tier: 'composed', md: md('button') }, + { md: md('stylesheet'), shape: 'asset', slug: 'stylesheet', tier: 'n/a' }, + { md: md('button'), shape: 'component', slug: 'button', tier: 'composed' }, // component with tier 'n/a' should not slip through as composed. - { slug: 'mystery', shape: 'component', tier: 'n/a', md: md('mystery') }, + { md: md('mystery'), shape: 'component', slug: 'mystery', tier: 'n/a' }, ]; const sorted = sortLlmsFullEntries(entries).map((e) => e.slug); expect(sorted).toEqual(['button']); @@ -57,8 +57,8 @@ describe('sortLlmsFullEntries', () => { it('treats atoms before composed regardless of slug ordering', () => { const entries: Array = [ - { slug: 'aardvark', shape: 'component', tier: 'composed', md: md('aardvark') }, - { slug: 'zebra', shape: 'component', tier: 'atom', md: md('zebra') }, + { md: md('aardvark'), shape: 'component', slug: 'aardvark', tier: 'composed' }, + { md: md('zebra'), shape: 'component', slug: 'zebra', tier: 'atom' }, ]; const sorted = sortLlmsFullEntries(entries).map((e) => e.slug); expect(sorted).toEqual(['zebra', 'aardvark']); @@ -67,12 +67,12 @@ describe('sortLlmsFullEntries', () => { it('places barrels before primitives even when primitive slugs sort earlier', () => { const entries: Array = [ { - slug: 'aardvark-primitive', + md: md('aardvark-primitive'), shape: 'component', + slug: 'aardvark-primitive', tier: 'primitive', - md: md('aardvark-primitive'), }, - { slug: 'tokens', shape: 'barrel', tier: 'n/a', md: md('tokens') }, + { md: md('tokens'), shape: 'barrel', slug: 'tokens', tier: 'n/a' }, ]; const sorted = sortLlmsFullEntries(entries).map((e) => e.slug); expect(sorted).toEqual(['tokens', 'aardvark-primitive']); @@ -80,8 +80,8 @@ describe('sortLlmsFullEntries', () => { it('does not mutate the input array', () => { const entries: Array = [ - { slug: 'b', shape: 'component', tier: 'composed', md: md('b') }, - { slug: 'a', shape: 'component', tier: 'composed', md: md('a') }, + { md: md('b'), shape: 'component', slug: 'b', tier: 'composed' }, + { md: md('a'), shape: 'component', slug: 'a', tier: 'composed' }, ]; const snapshot = entries.map((e) => e.slug); sortLlmsFullEntries(entries); @@ -94,23 +94,23 @@ describe('renderLlmsFull', () => { // Insertion order is intentionally jumbled. const entries: Array = [ { - slug: 'combobox-field-primitive', + md: md('combobox-field-primitive'), shape: 'component', + slug: 'combobox-field-primitive', tier: 'primitive', - md: md('combobox-field-primitive'), }, - { slug: 'tokens', shape: 'barrel', tier: 'n/a', md: md('tokens') }, - { slug: 'button', shape: 'component', tier: 'composed', md: md('button') }, - { slug: 'icon', shape: 'component', tier: 'atom', md: md('icon') }, + { md: md('tokens'), shape: 'barrel', slug: 'tokens', tier: 'n/a' }, + { md: md('button'), shape: 'component', slug: 'button', tier: 'composed' }, + { md: md('icon'), shape: 'component', slug: 'icon', tier: 'atom' }, { - slug: 'button-primitive', + md: md('button-primitive'), shape: 'component', + slug: 'button-primitive', tier: 'primitive', - md: md('button-primitive'), }, - { slug: 'recipes', shape: 'barrel', tier: 'n/a', md: md('recipes') }, - { slug: 'avatar', shape: 'component', tier: 'composed', md: md('avatar') }, - { slug: 'emoji', shape: 'component', tier: 'atom', md: md('emoji') }, + { md: md('recipes'), shape: 'barrel', slug: 'recipes', tier: 'n/a' }, + { md: md('avatar'), shape: 'component', slug: 'avatar', tier: 'composed' }, + { md: md('emoji'), shape: 'component', slug: 'emoji', tier: 'atom' }, ]; const output = renderLlmsFull(entries); @@ -136,7 +136,7 @@ describe('renderLlmsFull', () => { it('emits only the header when no entries fall into a bucket', () => { const output = renderLlmsFull([ - { slug: 'stylesheet', shape: 'asset', tier: 'n/a', md: md('stylesheet') }, + { md: md('stylesheet'), shape: 'asset', slug: 'stylesheet', tier: 'n/a' }, ]); expect(output).toMatch(/^# Luke UI — full documentation/); expect(output).not.toContain('---'); diff --git a/packages/@luke-ui/docs-tools/src/__tests__/render-page.test.ts b/packages/@luke-ui/docs-tools/src/__tests__/render-page.test.ts index a698fef8..bb97a22d 100644 --- a/packages/@luke-ui/docs-tools/src/__tests__/render-page.test.ts +++ b/packages/@luke-ui/docs-tools/src/__tests__/render-page.test.ts @@ -4,79 +4,79 @@ import { renderPage } from '../render-page.js'; const sampleParsed: ParsedComponent = { description: 'Sample composed button.', - tier: 'composed', propsInterface: { - name: 'SampleProps', extends: [{ from: 'external', module: 'react-aria-components', typeName: 'ButtonProps' }], members: [ { - name: 'size', - type: "'small' | 'medium'", - optional: true, default: "'medium'", description: 'Sets the size.', + name: 'size', + optional: true, + type: "'small' | 'medium'", }, ], + name: 'SampleProps', }, + tier: 'composed', }; describe('renderPage component body', () => { it('emits H1 from the export slug, titlecased', () => { const page = renderPage({ - kind: 'component', - slug: 'button', importPath: '@luke-ui/react/button', - tier: 'composed', + kind: 'component', parsed: sampleParsed, proseMarkdown: '', + slug: 'button', + tier: 'composed', }); expect(page).toMatch(/^# Button\n/); }); it('inserts the description as a blockquote', () => { const page = renderPage({ - kind: 'component', - slug: 'button', importPath: '@luke-ui/react/button', - tier: 'composed', + kind: 'component', parsed: sampleParsed, proseMarkdown: '', + slug: 'button', + tier: 'composed', }); expect(page).toContain('> Sample composed button.'); }); it('renders the import block', () => { const page = renderPage({ - kind: 'component', - slug: 'button', importPath: '@luke-ui/react/button', - tier: 'composed', + kind: 'component', parsed: sampleParsed, proseMarkdown: '', + slug: 'button', + tier: 'composed', }); expect(page).toContain("import { Button } from '@luke-ui/react/button';"); }); it('uses parsed.componentName for the import identifier when the export is renamed', () => { const page = renderPage({ - kind: 'component', - slug: 'text-field-primitive', importPath: '@luke-ui/react/text-field/primitive', - tier: 'primitive', + kind: 'component', parsed: { ...sampleParsed, componentName: 'TextInput' }, proseMarkdown: undefined, + slug: 'text-field-primitive', + tier: 'primitive', }); expect(page).toContain("import { TextInput } from '@luke-ui/react/text-field/primitive';"); }); it('renders the props table with own props only', () => { const page = renderPage({ - kind: 'component', - slug: 'button', importPath: '@luke-ui/react/button', - tier: 'composed', + kind: 'component', parsed: sampleParsed, proseMarkdown: '', + slug: 'button', + tier: 'composed', }); expect(page).toMatch( /\| `size` \| `'small' \\\| 'medium'` \| `'medium'` \| Sets the size\. \|/, @@ -85,36 +85,36 @@ describe('renderPage component body', () => { it('emits an extends pointer for external types', () => { const page = renderPage({ - kind: 'component', - slug: 'button', importPath: '@luke-ui/react/button', - tier: 'composed', + kind: 'component', parsed: sampleParsed, proseMarkdown: '', + slug: 'button', + tier: 'composed', }); expect(page).toContain('Extends [`react-aria-components` `ButtonProps`]'); }); it('omits Usage section for primitives', () => { const page = renderPage({ - kind: 'component', - slug: 'button-primitive', importPath: '@luke-ui/react/button/primitive', - tier: 'primitive', + kind: 'component', parsed: sampleParsed, proseMarkdown: undefined, + slug: 'button-primitive', + tier: 'primitive', }); expect(page).not.toContain('## Usage'); }); it('renders a (primitive) suffix on the title for primitive tier', () => { const page = renderPage({ - kind: 'component', - slug: 'button-primitive', importPath: '@luke-ui/react/button/primitive', - tier: 'primitive', + kind: 'component', parsed: sampleParsed, proseMarkdown: undefined, + slug: 'button-primitive', + tier: 'primitive', }); expect(page).toMatch(/^# Button \(primitive\)\n/); }); @@ -123,31 +123,31 @@ describe('renderPage component body', () => { describe('renderPage barrel body', () => { const sampleExports = [ { - name: 'WidgetControl', description: 'The main control.', + name: 'WidgetControl', type: 'function WidgetControl(props: WidgetControlProps): JSX.Element', }, - { name: 'WidgetItem', description: 'A single item.', type: undefined }, + { description: 'A single item.', name: 'WidgetItem', type: undefined }, ]; it('emits an H1 with the slug title', () => { const page = renderPage({ + exports: sampleExports, + importPath: '@luke-ui/react/combobox-field/primitive', kind: 'barrel', slug: 'combobox-field-primitive', - importPath: '@luke-ui/react/combobox-field/primitive', tier: 'primitive', - exports: sampleExports, }); expect(page).toMatch(/^# Combobox Field \(primitive\)\n/); }); it('renders an Import block listing all exports', () => { const page = renderPage({ + exports: sampleExports, + importPath: '@luke-ui/react/combobox-field/primitive', kind: 'barrel', slug: 'combobox-field-primitive', - importPath: '@luke-ui/react/combobox-field/primitive', tier: 'primitive', - exports: sampleExports, }); expect(page).toContain( "import { WidgetControl, WidgetItem } from '@luke-ui/react/combobox-field/primitive';", @@ -156,14 +156,14 @@ describe('renderPage barrel body', () => { it('marks type-only exports in the import block', () => { const page = renderPage({ + exports: [ + { description: 'Allowed levels.', name: 'HeadingLevel', type: 'type HeadingLevel = 1' }, + { description: 'Provider.', name: 'HeadingLevels', type: undefined }, + ], + importPath: '@luke-ui/react/heading-context', kind: 'barrel', slug: 'heading-context', - importPath: '@luke-ui/react/heading-context', tier: 'n/a', - exports: [ - { name: 'HeadingLevel', description: 'Allowed levels.', type: 'type HeadingLevel = 1' }, - { name: 'HeadingLevels', description: 'Provider.', type: undefined }, - ], }); expect(page).toContain( "import { type HeadingLevel, HeadingLevels } from '@luke-ui/react/heading-context';", @@ -172,11 +172,11 @@ describe('renderPage barrel body', () => { it('renders an ## Exports section with each export as an H3', () => { const page = renderPage({ + exports: sampleExports, + importPath: '@luke-ui/react/combobox-field/primitive', kind: 'barrel', slug: 'combobox-field-primitive', - importPath: '@luke-ui/react/combobox-field/primitive', tier: 'primitive', - exports: sampleExports, }); expect(page).toContain('## Exports'); expect(page).toContain('### `WidgetControl`'); @@ -185,23 +185,23 @@ describe('renderPage barrel body', () => { it('renders description as blockquote when provided', () => { const page = renderPage({ + description: 'A barrel of field primitives.', + exports: sampleExports, + importPath: '@luke-ui/react/field/primitive', kind: 'barrel', slug: 'field-primitive', - importPath: '@luke-ui/react/field/primitive', tier: 'primitive', - description: 'A barrel of field primitives.', - exports: sampleExports, }); expect(page).toContain('> A barrel of field primitives.'); }); it('renders type signature in a code fence when present', () => { const page = renderPage({ + exports: sampleExports, + importPath: '@luke-ui/react/combobox-field/primitive', kind: 'barrel', slug: 'combobox-field-primitive', - importPath: '@luke-ui/react/combobox-field/primitive', tier: 'primitive', - exports: sampleExports, }); expect(page).toContain('function WidgetControl(props: WidgetControlProps): JSX.Element'); }); diff --git a/packages/@luke-ui/docs-tools/src/discover-exports.ts b/packages/@luke-ui/docs-tools/src/discover-exports.ts index 0bf66af1..f0c8d3e5 100644 --- a/packages/@luke-ui/docs-tools/src/discover-exports.ts +++ b/packages/@luke-ui/docs-tools/src/discover-exports.ts @@ -6,31 +6,31 @@ export type ExportPageKind = 'component' | 'barrel' | 'asset'; export type ExportTier = 'atom' | 'composed' | 'primitive' | 'n/a'; export interface DiscoveredExport { + pageKind: ExportPageKind; /** The export specifier from package.json#exports, e.g. './button'. */ path: string; - /** The dist target path (right-hand side of #exports). */ - target: string; + shape: ExportShape; /** The slug used for the generated .md filename, e.g. 'button' or 'button-primitive'. */ slug: string; - shape: ExportShape; - pageKind: ExportPageKind; - tier: ExportTier; /** Absolute path to the source `index.tsx` for components/barrels; undefined for assets. */ sourcePath?: string; + /** The dist target path (right-hand side of #exports). */ + target: string; + tier: ExportTier; } export interface DiscoverExportsOptions { - packageRoot?: string; - /** - * Export specifiers to classify as `barrel` (multi-export foundations). - * Defaults to {@link DEFAULT_BARREL_PATHS}, which is coupled to `@luke-ui/react`. - */ - barrelPaths?: Iterable; /** * Export specifiers to classify as `asset` (no docs page). * Defaults to {@link DEFAULT_ASSET_PATHS}, which is coupled to `@luke-ui/react`. */ assetPaths?: Iterable; + /** + * Export specifiers to classify as `barrel` (multi-export foundations). + * Defaults to {@link DEFAULT_BARREL_PATHS}, which is coupled to `@luke-ui/react`. + */ + barrelPaths?: Iterable; + packageRoot?: string; } /** @@ -82,12 +82,12 @@ export function discoverExports( } const sourcePath = shape === 'asset' ? undefined : sourceFromExport(options.packageRoot, target); - const pageKind = pageKindFor({ shape, tier, sourcePath }); + const pageKind = pageKindFor({ shape, sourcePath, tier }); const slug = path .replace(LEADING_DOT_SLASH, '') .replace(SLASH, '-') .replace(FILE_EXTENSION, ''); - result.push({ path, target, slug, shape, pageKind, tier, sourcePath }); + result.push({ pageKind, path, shape, slug, sourcePath, target, tier }); } return result; } diff --git a/packages/@luke-ui/docs-tools/src/parse-types.ts b/packages/@luke-ui/docs-tools/src/parse-types.ts index c03db341..c5ecf1ca 100644 --- a/packages/@luke-ui/docs-tools/src/parse-types.ts +++ b/packages/@luke-ui/docs-tools/src/parse-types.ts @@ -10,11 +10,11 @@ import { Node, Project, SyntaxKind } from 'ts-morph'; import type { ExportTier } from './discover-exports.js'; export interface ParsedProp { - name: string; - type: string; - optional: boolean; default?: string; description: string; + name: string; + optional: boolean; + type: string; } export interface ParsedExtends { @@ -24,15 +24,15 @@ export interface ParsedExtends { } export interface ParsedComponent { - description: string; - tier?: ExportTier; /** Name of the exported function (e.g. `'TextInput'`), if one was found. */ componentName?: string; + description: string; propsInterface?: { name: string; extends: Array; members: Array; }; + tier?: ExportTier; } type PropsDeclaration = InterfaceDeclaration | TypeAliasDeclaration; @@ -47,9 +47,9 @@ interface PropsLookup { */ function createProject(): Project { return new Project({ - tsConfigFilePath: undefined, - skipAddingFilesFromTsConfig: true, compilerOptions: { allowJs: false, jsx: 4 /* React */ }, + skipAddingFilesFromTsConfig: true, + tsConfigFilePath: undefined, }); } @@ -89,7 +89,7 @@ export function parseComponent(sourcePath: string): ParsedComponent { const propsInterface = findPropsDeclaration(propsLookup, componentName); if (!propsInterface) { - return { description, componentName }; + return { componentName, description }; } const tier = readTierTag(propsInterface); @@ -99,14 +99,14 @@ export function parseComponent(sourcePath: string): ParsedComponent { const members = collectMembers(propsInterface); return { - description, - tier, componentName, + description, propsInterface: { - name: propsInterface.getName(), extends: extendsList, members, + name: propsInterface.getName(), }, + tier, }; } @@ -376,7 +376,7 @@ function collectMembersFromDeclaration( out: Array, names: Set, ): void { - const key = decl.getSourceFile().getFilePath() + ':' + decl.getName(); + const key = `${decl.getSourceFile().getFilePath()}:${decl.getName()}`; if (seen.has(key)) return; seen.add(key); @@ -475,7 +475,7 @@ function appendPropNamesFromDeclaration( out: Set, seen: Set, ): void { - const key = decl.getSourceFile().getFilePath() + ':' + decl.getName(); + const key = `${decl.getSourceFile().getFilePath()}:${decl.getName()}`; if (seen.has(key)) return; seen.add(key); @@ -509,11 +509,11 @@ function toParsedProp(prop: PropertySignature): ParsedProp { } } return { - name: prop.getName(), - type: resolvePropType(prop), - optional: prop.hasQuestionToken(), default: defaultValue, description, + name: prop.getName(), + optional: prop.hasQuestionToken(), + type: resolvePropType(prop), }; } @@ -545,8 +545,8 @@ function stripImportPaths(type: string): string { // --------------------------------------------------------------------------- export interface ParsedBarrelExport { - name: string; description: string; + name: string; type?: string; } @@ -594,13 +594,13 @@ export function parseBarrel(sourcePath: string): ParsedBarrel { if (kind === SyntaxKind.InterfaceDeclaration || kind === SyntaxKind.TypeAliasDeclaration) { if (isCompanionTypeExport(name, desc)) continue; // Standalone type alias with a description (e.g. ComboboxSize) — include it briefly. - exports.push({ name, description: desc, type: getTypeSignature(firstDecl, name) }); + exports.push({ description: desc, name, type: getTypeSignature(firstDecl, name) }); continue; } // Runtime export (function, class, arrow function, etc.) const type = getTypeSignature(firstDecl, name); - exports.push({ name, description: desc, type }); + exports.push({ description: desc, name, type }); } // Sort alphabetically for stable output. @@ -639,7 +639,7 @@ function buildReExportDescriptionMap(sourceFile: SourceFile): Map; includeLibraryAuthors: boolean; + packageName: string; } const ASSET_NOTES: Record = { - './stylesheet.css': "required side-effect import: `import '@luke-ui/react/stylesheet.css'`", + './package.json': 'package manifest export, used by tooling — no docs.', './spritesheet.svg': 'sprite sheet referenced by the `Icon` atom; bundlers resolve this automatically.', - './package.json': 'package manifest export, used by tooling — no docs.', + './stylesheet.css': "required side-effect import: `import '@luke-ui/react/stylesheet.css'`", }; export function renderIndex(input: RenderIndexInput): string { diff --git a/packages/@luke-ui/docs-tools/src/render-llms-full.ts b/packages/@luke-ui/docs-tools/src/render-llms-full.ts index 1b73b2c8..2beab1c4 100644 --- a/packages/@luke-ui/docs-tools/src/render-llms-full.ts +++ b/packages/@luke-ui/docs-tools/src/render-llms-full.ts @@ -1,12 +1,12 @@ import type { ExportShape, ExportTier } from './discover-exports.js'; export interface LlmsFullEntry { + md: string; + shape: ExportShape; /** The slug used for the generated .md filename, e.g. 'button' or 'button-primitive'. */ slug: string; - shape: ExportShape; /** Effective tier for the entry, including any JSDoc `@tier` overrides. */ tier: ExportTier; - md: string; } /** @@ -38,8 +38,8 @@ export function renderLlmsFull(entries: Array): string { export function sortLlmsFullEntries(entries: Array): Array { const buckets: Record> = { atom: [], - composed: [], barrel: [], + composed: [], primitive: [], }; for (const entry of entries) { diff --git a/packages/@luke-ui/docs-tools/src/render-page.ts b/packages/@luke-ui/docs-tools/src/render-page.ts index 2b222b06..a335e791 100644 --- a/packages/@luke-ui/docs-tools/src/render-page.ts +++ b/packages/@luke-ui/docs-tools/src/render-page.ts @@ -5,22 +5,22 @@ import { slugToTitle } from './title.js'; const RAC_DOCS_BASE = 'https://react-spectrum.adobe.com/react-aria'; interface RenderComponentPageInput { - kind: 'component'; - slug: string; importPath: string; - tier: ExportTier; + kind: 'component'; parsed: ParsedComponent; /** Authored Markdown prose for the Usage section. Undefined for primitives. */ proseMarkdown: string | undefined; + slug: string; + tier: ExportTier; } interface RenderBarrelPageInput { + description?: string; + exports: Array; + importPath: string; kind: 'barrel'; slug: string; - importPath: string; tier: ExportTier; - description?: string; - exports: Array; } export type RenderPageInput = RenderComponentPageInput | RenderBarrelPageInput; diff --git a/packages/@luke-ui/rainbow-sprinkles/package.json b/packages/@luke-ui/rainbow-sprinkles/package.json new file mode 100644 index 00000000..2c6dd46f --- /dev/null +++ b/packages/@luke-ui/rainbow-sprinkles/package.json @@ -0,0 +1,27 @@ +{ + "name": "@luke-ui/rainbow-sprinkles", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./create-runtime-fn": "./src/create-runtime-fn.ts", + "./package.json": "./package.json" + }, + "scripts": { + "check:format": "pnpm exec oxfmt -c ../../../.oxfmtrc.json --check .", + "check:lint": "pnpm exec oxlint . --type-aware", + "check:types": "tsc --noEmit -p tsconfig.json", + "fix:format": "pnpm exec oxfmt -c ../../../.oxfmtrc.json .", + "fix:lint": "pnpm exec oxlint . --type-aware --fix" + }, + "dependencies": { + "@vanilla-extract/css": "^1.20.1", + "@vanilla-extract/dynamic": "^2.1.2" + }, + "devDependencies": { + "@types/node": "catalog:", + "csstype": "^3.2.3", + "typescript": "^6.0.3" + } +} diff --git a/packages/@luke-ui/rainbow-sprinkles/src/create-runtime-fn.ts b/packages/@luke-ui/rainbow-sprinkles/src/create-runtime-fn.ts new file mode 100644 index 00000000..f1ae5dc9 --- /dev/null +++ b/packages/@luke-ui/rainbow-sprinkles/src/create-runtime-fn.ts @@ -0,0 +1,340 @@ +import { assignInlineVars } from '@vanilla-extract/dynamic'; +import type { + DefinePropertiesReturn, + DynamicConditionalProperty, + DynamicProperty, + RuntimeFnReturn, + ShorthandProperty, + SprinkleProperties, + SprinklesProps, + StaticConditionalProperty, + StaticConditionalPropertyArray, + StaticDynamicConditionalProperty, + StaticDynamicConditionalPropertyArray, + StaticDynamicProperty, + StaticDynamicPropertyArray, + StaticProperty, + StaticPropertyArray, +} from './types.js'; + +export type SprinklesFn> = { + >( + props: Props, + ): RuntimeFnReturn & Omit | 'className' | 'style'>; + properties: Set>; +}; + +function replaceVarsInValue( + propValue: string, + dynamicScale: Record | true | undefined, + staticScale: ReadonlyArray | Record | undefined, +): string | false { + if (Array.isArray(staticScale) && staticScale.indexOf(propValue) > -1) { + return false; + } + // Direct bare-name lookup in dynamic scale (no $ prefix needed) + if (dynamicScale && typeof dynamicScale === 'object' && propValue in dynamicScale) { + return dynamicScale[propValue]!; + } + if (staticScale && !Array.isArray(staticScale) && propValue in staticScale) { + return false; + } + return propValue; +} + +function getValueConfig( + propValue: string, + scale: Record }>, +): { default: string; conditions?: Record } | null { + if (propValue in scale) { + return scale[propValue]!; + } + return null; +} + +function assignClasses( + propertyConfig: SprinkleProperties[string], + propValue: unknown, + cache: Map, + condition?: string, +): string { + if (!propValue && propValue !== 0) { + return ''; + } + + if (typeof propValue === 'string' || typeof propValue === 'number') { + return handleEntry(propertyConfig, `${propValue}`, cache, condition); + } + const keys = Object.keys(propValue as Record); + if (keys.length < 1) { + return ''; + } + const parts: Array = []; + for (const cond of keys) { + const rawValueAtCondition = String((propValue as Record)[cond]); + const result = handleEntry(propertyConfig, rawValueAtCondition, cache, cond); + if (result) { + parts.push(result); + } + } + return parts.join(' '); +} + +function hasValues( + config: SprinkleProperties[string], +): config is + | StaticProperty + | StaticConditionalProperty + | StaticPropertyArray + | StaticConditionalPropertyArray + | StaticDynamicProperty + | StaticDynamicConditionalProperty + | StaticDynamicPropertyArray + | StaticDynamicConditionalPropertyArray { + return 'values' in config; +} + +function hasDynamic( + config: SprinkleProperties[string], +): config is + | DynamicProperty + | DynamicConditionalProperty + | StaticDynamicProperty + | StaticDynamicConditionalProperty + | StaticDynamicPropertyArray + | StaticDynamicConditionalPropertyArray { + return 'dynamic' in config; +} + +function handleEntry( + propertyConfig: SprinkleProperties[string], + propValue: string, + cache: Map, + condition?: string, +): string { + if ('mappings' in propertyConfig) { + return ''; + } + + const propName = (propertyConfig as DynamicProperty).name; + const staticScale = (propertyConfig as StaticProperty).staticScale as + | ReadonlyArray + | Record + | undefined; + const cacheKey = condition ? `${condition}${propValue}` : propValue; + const cached = cache.get(cacheKey); + if (cached !== undefined) { + return cached; + } + + if (hasValues(propertyConfig)) { + const values = propertyConfig.values; + if (Array.isArray(staticScale) && staticScale.includes(propValue)) { + const valueConfig = values[propValue] as { + default: string; + conditions?: Record; + }; + const result = condition ? valueConfig?.conditions?.[condition] : valueConfig?.default; + if (typeof result === 'string') { + cache.set(cacheKey, result); + return result; + } + } + // Direct bare-name lookup for static record scale (no $ prefix needed) + if ( + !Array.isArray(staticScale) && + staticScale && + propValue in staticScale && + propValue in values + ) { + const valueConfig = values[propValue] as { + default: string; + conditions?: Record; + }; + const result = condition ? valueConfig?.conditions?.[condition] : valueConfig?.default; + if (typeof result === 'string') { + cache.set(cacheKey, result); + return result; + } + } + const parsedValue = getValueConfig(propValue, values); + if (parsedValue) { + const result = condition ? parsedValue.conditions?.[condition] : parsedValue.default; + if (typeof result === 'string') { + cache.set(cacheKey, result); + return result; + } + } + } + if (hasDynamic(propertyConfig)) { + const dynamic = propertyConfig.dynamic as { + default: string; + conditions?: Record; + }; + const result = condition ? dynamic.conditions?.[condition] : dynamic.default; + if (typeof result === 'string') { + cache.set(cacheKey, result); + return result; + } + } + + // eslint-disable-next-line no-console + console.error( + `Rainbow Sprinkles: invalid value provided to prop '${propName}'. Expected one of ${ + hasValues(propertyConfig) + ? Object.keys(propertyConfig.values) + .map((c) => `"${c}"`) + .join(', ') + : 'dynamic' + }. Received: ${JSON.stringify(propValue)}.`, + ); + return ''; +} + +function assignVars( + style: Record, + propertyConfig: SprinkleProperties[string], + propValue: unknown, + cache: Map, +): void { + if ('mappings' in propertyConfig) { + return; + } + if (!hasDynamic(propertyConfig)) { + return; + } + const vars = propertyConfig.vars as + | { default: string; conditions?: Record } + | undefined; + const dynamicScale = propertyConfig.dynamicScale as Record | true | undefined; + const staticScale = (propertyConfig as StaticProperty).staticScale as + | ReadonlyArray + | Record + | undefined; + + if (typeof propValue === 'string' || typeof propValue === 'number') { + const cacheKey = `${propValue}`; + const cached = cache.get(cacheKey); + if (cached !== undefined) { + if (cached && vars) { + style[vars.default] = cached; + } + return; + } + const parsedValue = replaceVarsInValue(cacheKey, dynamicScale, staticScale); + cache.set(cacheKey, parsedValue); + if (parsedValue && vars) { + style[vars.default] = parsedValue; + } + return; + } + + if (propValue && typeof propValue === 'object') { + const propObj = propValue as Record; + const keys = Object.keys(propObj); + if (keys.length < 1) { + return; + } + for (const condition of keys) { + const value = propObj[condition]; + if (typeof value === 'string' || typeof value === 'number') { + const cacheKey = `${value}`; + const cached = cache.get(cacheKey); + if (cached !== undefined) { + if (cached && vars?.conditions) { + const conditionVar = vars.conditions[condition]; + if (conditionVar) { + style[conditionVar] = cached; + } + } + continue; + } + const parsedValue = replaceVarsInValue(cacheKey, dynamicScale, staticScale); + cache.set(cacheKey, parsedValue); + if (parsedValue && vars?.conditions) { + const conditionVar = vars.conditions[condition]; + if (conditionVar) { + style[conditionVar] = parsedValue; + } + } + } + } + } +} + +export function createRuntimeFn>( + ...configs: Configs +): SprinklesFn { + const cssConfig: SprinkleProperties = Object.assign( + {}, + ...configs.map((c) => c.config), + ) as SprinkleProperties; + const properties = Object.keys(cssConfig); + const propertiesSet = new Set(properties); + const shorthandNames = properties.filter((property) => 'mappings' in cssConfig[property]!); + + type PropertyCache = { + class: Map; + style: Map; + }; + + const cache = new Map(); + + const fn = (props: Record) => { + const style: Record = {}; + const className: Array = []; + const otherProps: Record = {}; + const shorthands: Record = {}; + const nonShorthands = { ...props }; + let hasShorthands = false; + + for (const shorthand of shorthandNames) { + const value = props[shorthand]; + if (value != null) { + const sprinkle = cssConfig[shorthand]! as ShorthandProperty; + hasShorthands = true; + for (const propMapping of sprinkle.mappings) { + shorthands[propMapping] = value; + if (nonShorthands[propMapping] == null) { + delete nonShorthands[propMapping]; + } + } + } + } + + const finalProps = hasShorthands ? { ...shorthands, ...nonShorthands } : props; + const finalPropsKeys = Object.keys(finalProps); + for (const property of finalPropsKeys) { + if (!propertiesSet.has(property)) { + otherProps[property] = props[property]; + continue; + } + const propertyConfig = cssConfig[property]; + const propValue = finalProps[property]; + if (!propertyConfig || 'mappings' in propertyConfig) { + continue; + } + let classCache: Map | undefined; + let styleCache: Map | undefined; + const c = cache.get(property); + if (c) { + classCache = c.class; + styleCache = c.style; + } else { + classCache = new Map(); + styleCache = new Map(); + cache.set(property, { class: classCache, style: styleCache }); + } + className.push(assignClasses(propertyConfig, propValue, classCache)); + assignVars(style, propertyConfig, propValue, styleCache); + } + return { + ...otherProps, + className: className.join(' ').trim(), + style: assignInlineVars(style), + }; + }; + return Object.assign(fn, { + properties: propertiesSet as unknown as Set>, + }) as SprinklesFn; +} diff --git a/packages/@luke-ui/rainbow-sprinkles/src/css.ts b/packages/@luke-ui/rainbow-sprinkles/src/css.ts new file mode 100644 index 00000000..5122c58a --- /dev/null +++ b/packages/@luke-ui/rainbow-sprinkles/src/css.ts @@ -0,0 +1,3 @@ +import type { Properties as CSSProperties } from 'csstype'; + +export interface Properties extends CSSProperties {} diff --git a/packages/@luke-ui/rainbow-sprinkles/src/define-properties.ts b/packages/@luke-ui/rainbow-sprinkles/src/define-properties.ts new file mode 100644 index 00000000..5bb2cbaf --- /dev/null +++ b/packages/@luke-ui/rainbow-sprinkles/src/define-properties.ts @@ -0,0 +1,201 @@ +import { createVar, style } from '@vanilla-extract/css'; +import type { + CommonOptions, + ConfigConditions, + ConfigDynamicProperties, + ConfigStaticProperties, + DefinePropertiesReturn, + MakeConfig, + ShorthandProperty, + SprinkleProperties, +} from './types.js'; + +function mapValues, R>( + obj: T, + callback: (value: T[keyof T], key: string, obj: T) => R, +): Record { + const result: Record = {}; + for (const key in obj) { + result[key] = callback(obj[key], key, obj); + } + return result; +} + +function createStyles( + property: string, + scale: Record | true, + conditions: ConfigConditions | undefined, + defaultCondition: string, + options: { '@layer'?: string } = {}, +): { + vars: { default: string } | { conditions: Record; default: string }; + dynamic: { default: string } | { conditions: Record; default: string }; + dynamicScale: Record | true; + name: string; +} { + if (!conditions) { + const cssVar = createVar(property)!; + const styleValue = { [property]: cssVar }; + const className = style( + options['@layer'] ? { '@layer': { [options['@layer']]: styleValue } } : styleValue, + property, + ); + return { + dynamic: { default: className }, + dynamicScale: scale, + name: property, + vars: { default: cssVar }, + }; + } + const vars = mapValues( + conditions, + (_, conditionName) => createVar(`${property}-${conditionName}`)!, + ); + const classes = mapValues(conditions, (conditionValue, conditionName) => { + let styleValue: Record = { [property]: vars[conditionName] }; + if (conditionValue['@media']) { + styleValue = { '@media': { [conditionValue['@media']]: styleValue } }; + } + if (conditionValue['@supports']) { + styleValue = { '@supports': { [conditionValue['@supports']]: styleValue } }; + } + if (conditionValue['@container']) { + styleValue = { '@container': { [conditionValue['@container']]: styleValue } }; + } + if (conditionValue.selector) { + styleValue = { selectors: { [conditionValue.selector]: styleValue } }; + } + return style( + options['@layer'] ? { '@layer': { [options['@layer']]: styleValue } } : styleValue, + `${property}-${conditionName}`, + ); + }); + return { + dynamic: { conditions: classes, default: classes[defaultCondition]! }, + dynamicScale: scale, + name: property, + vars: { conditions: vars, default: vars[defaultCondition]! }, + }; +} + +function createStaticStyles( + property: string, + scale: ReadonlyArray | Record, + conditions: ConfigConditions | undefined, + defaultCondition: string, + options: { '@layer'?: string } = {}, +): { + values: Record< + string, + { default: string } | { conditions: Record; default: string } + >; + name: string; + staticScale: ReadonlyArray | Record; +} { + const scaleObj = Array.isArray(scale) + ? Object.assign({}, ...scale.map((s) => ({ [s]: s }))) + : scale; + const values = mapValues(scaleObj, (scaleValue, scaleKey) => { + const styleValue = { [property]: scaleValue }; + if (!conditions) { + return { + default: style( + options['@layer'] ? { '@layer': { [options['@layer']]: styleValue } } : styleValue, + `${property}-${scaleKey}`, + ), + }; + } + const classes = mapValues(conditions, (conditionValue, conditionName) => { + let conditionalStyleValue: Record = { [property]: scaleValue }; + if (conditionValue['@media']) { + conditionalStyleValue = { '@media': { [conditionValue['@media']]: conditionalStyleValue } }; + } + if (conditionValue['@supports']) { + conditionalStyleValue = { + '@supports': { [conditionValue['@supports']]: conditionalStyleValue }, + }; + } + if (conditionValue['@container']) { + conditionalStyleValue = { + '@container': { [conditionValue['@container']]: conditionalStyleValue }, + }; + } + if (conditionValue.selector) { + conditionalStyleValue = { selectors: { [conditionValue.selector]: conditionalStyleValue } }; + } + return style( + options['@layer'] + ? { '@layer': { [options['@layer']]: conditionalStyleValue } } + : conditionalStyleValue, + `${property}-${scaleKey}-${conditionName}`, + ); + }); + return { conditions: classes, default: classes[defaultCondition]! }; + }); + return { name: property, staticScale: scale, values }; +} + +type DefinePropertiesOptions = CommonOptions & { + conditions?: ConfigConditions; + defaultCondition?: string; + dynamicProperties?: ConfigDynamicProperties; + staticProperties?: ConfigStaticProperties; + shorthands?: Record>; +}; + +export function defineProperties< + const Dyn extends ConfigDynamicProperties | undefined = undefined, + const Stat extends ConfigStaticProperties | undefined = undefined, + const Cond extends ConfigConditions | undefined = undefined, + const Short extends Record> | undefined = undefined, +>(options: { + '@layer'?: string; + conditions?: Cond; + defaultCondition?: string; + dynamicProperties?: Dyn; + staticProperties?: Stat; + shorthands?: Short; +}): DefinePropertiesReturn>; +export function defineProperties(options: DefinePropertiesOptions): DefinePropertiesReturn { + const { conditions, dynamicProperties, staticProperties, shorthands, defaultCondition } = options; + const config: SprinkleProperties = shorthands + ? Object.fromEntries( + Object.entries(shorthands).map(([prop, mappings]) => [ + prop, + { mappings } as ShorthandProperty, + ]), + ) + : {}; + + if (dynamicProperties) { + for (const dynamicProp of Object.keys(dynamicProperties)) { + config[dynamicProp] = createStyles( + dynamicProp, + // biome-ignore lint/suspicious/noExplicitAny: dynamic property access + (dynamicProperties as Record)[dynamicProp] as + | Record + | true, + conditions, + defaultCondition ?? '', + { '@layer': options['@layer'] }, + ); + } + } + + if (staticProperties) { + for (const staticProp of Object.keys(staticProperties)) { + const staticStyle = createStaticStyles( + staticProp, + // biome-ignore lint/suspicious/noExplicitAny: dynamic property access + (staticProperties as Record)[staticProp] as + | ReadonlyArray + | Record, + conditions, + defaultCondition ?? '', + { '@layer': options['@layer'] }, + ); + config[staticProp] = Object.assign({}, config[staticProp], staticStyle); + } + } + return { config }; +} diff --git a/packages/@luke-ui/rainbow-sprinkles/src/define-sprinkles.ts b/packages/@luke-ui/rainbow-sprinkles/src/define-sprinkles.ts new file mode 100644 index 00000000..cd2556da --- /dev/null +++ b/packages/@luke-ui/rainbow-sprinkles/src/define-sprinkles.ts @@ -0,0 +1,15 @@ +import { addFunctionSerializer } from '@vanilla-extract/css/functionSerializer'; +import type { SprinklesFn } from './create-runtime-fn.js'; +import { createRuntimeFn } from './create-runtime-fn.js'; +import type { DefinePropertiesReturn } from './types.js'; + +export function defineSprinkles>( + ...configs: Configs +): SprinklesFn { + const sprinkles = createRuntimeFn(...configs); + return addFunctionSerializer(sprinkles, { + args: configs, + importName: 'createRuntimeFn', + importPath: '@luke-ui/rainbow-sprinkles/create-runtime-fn', + }) as SprinklesFn; +} diff --git a/packages/@luke-ui/rainbow-sprinkles/src/index.ts b/packages/@luke-ui/rainbow-sprinkles/src/index.ts new file mode 100644 index 00000000..78529449 --- /dev/null +++ b/packages/@luke-ui/rainbow-sprinkles/src/index.ts @@ -0,0 +1,12 @@ +export { defineSprinkles } from './define-sprinkles.js'; +export { defineProperties } from './define-properties.js'; +export type { SprinklesFn } from './create-runtime-fn.js'; +export type { + ConfigConditions, + ConfigDynamicProperties, + ConfigStaticProperties, + DefinePropertiesReturn, + MakeConfig, + RuntimeFnReturn, + SprinklesProps, +} from './types.js'; diff --git a/packages/@luke-ui/rainbow-sprinkles/src/types.ts b/packages/@luke-ui/rainbow-sprinkles/src/types.ts new file mode 100644 index 00000000..8d009071 --- /dev/null +++ b/packages/@luke-ui/rainbow-sprinkles/src/types.ts @@ -0,0 +1,322 @@ +import type { Properties } from './css.js'; + +interface CSSProperties extends Properties {} + +type PropertyCssValue = T extends keyof CSSProperties ? CSSProperties[T] : never; + +export type ConfigStaticProperties = { + [k in keyof CSSProperties]?: ReadonlyArray | Record; +}; + +export type ConfigDynamicProperties = { + [k in keyof CSSProperties]?: Record | true; +}; + +export type ConfigConditions = { + [conditionName: string]: { + '@media'?: string; + '@supports'?: string; + '@container'?: string; + selector?: string; + }; +}; + +type ConditionalPropertyValue = { + default: string; + conditions: { + [conditionName: string]: string; + }; +}; + +type NonConditionalPropertyValue = { + default: string; +}; + +export type DynamicProperty = { + dynamic: NonConditionalPropertyValue; + vars: NonConditionalPropertyValue; + dynamicScale: + | { + [token: string]: string; + } + | true; + name: string; +}; + +export type DynamicConditionalProperty = { + dynamic: ConditionalPropertyValue; + vars: ConditionalPropertyValue; + dynamicScale: + | { + [token: string]: string; + } + | true; + name: string; +}; + +export type StaticPropertyArray = { + values: { + [value: string]: NonConditionalPropertyValue; + }; + staticScale: ReadonlyArray; + name: string; +}; + +export type StaticConditionalPropertyArray = { + values: { + [value: string]: ConditionalPropertyValue; + }; + staticScale: ReadonlyArray; + name: string; +}; + +export type StaticProperty = { + values: { + [value: string]: NonConditionalPropertyValue; + }; + staticScale: { + [token: string]: string; + }; + name: string; +}; + +export type StaticConditionalProperty = { + values: { + [value: string]: ConditionalPropertyValue; + }; + staticScale: { + [token: string]: string; + }; + name: string; +}; + +export type StaticDynamicPropertyArray = { + dynamic: NonConditionalPropertyValue; + values: { + [value: string]: NonConditionalPropertyValue; + }; + name: string; + staticScale: ReadonlyArray; + dynamicScale: true; + vars: NonConditionalPropertyValue; +}; + +export type StaticDynamicConditionalPropertyArray = { + dynamic: ConditionalPropertyValue; + values: { + [value: string]: ConditionalPropertyValue; + }; + name: string; + staticScale: ReadonlyArray; + dynamicScale: true; + vars: ConditionalPropertyValue; +}; + +export type StaticDynamicProperty = { + dynamic: NonConditionalPropertyValue; + values: { + [value: string]: NonConditionalPropertyValue; + }; + name: string; + vars: NonConditionalPropertyValue; + staticScale: { + [token: string]: string; + }; + dynamicScale: true; +}; + +export type StaticDynamicConditionalProperty = { + dynamic: ConditionalPropertyValue; + values: { + [value: string]: ConditionalPropertyValue; + }; + name: string; + vars: ConditionalPropertyValue; + staticScale: { + [token: string]: string; + }; + dynamicScale: true; +}; + +export type ShorthandProperty = ReadonlyArray> = { + mappings: Mappings; +}; + +export type SprinkleProperties = { + [k: string]: + | DynamicProperty + | StaticProperty + | StaticPropertyArray + | StaticDynamicPropertyArray + | StaticDynamicProperty + | DynamicConditionalProperty + | StaticConditionalProperty + | StaticConditionalPropertyArray + | StaticDynamicConditionalPropertyArray + | StaticDynamicConditionalProperty + | ShorthandProperty; +}; + +// Mapped config types produced by defineProperties, preserving literal scale key types. +type DynamicConfigEntry< + K extends string, + Scale extends Record | true, + HasCond extends boolean, +> = Scale extends true + ? HasCond extends true + ? { + dynamic: ConditionalPropertyValue; + vars: ConditionalPropertyValue; + dynamicScale: true; + name: K; + } + : { + dynamic: NonConditionalPropertyValue; + vars: NonConditionalPropertyValue; + dynamicScale: true; + name: K; + } + : Scale extends Record + ? HasCond extends true + ? { + dynamic: ConditionalPropertyValue; + vars: ConditionalPropertyValue; + dynamicScale: { [T in keyof Scale & string]: string }; + name: K; + } + : { + dynamic: NonConditionalPropertyValue; + vars: NonConditionalPropertyValue; + dynamicScale: { [T in keyof Scale & string]: string }; + name: K; + } + : never; + +type StaticConfigEntry< + K extends string, + Scale extends ReadonlyArray | Record, + HasCond extends boolean, +> = + Scale extends ReadonlyArray + ? HasCond extends true + ? { values: { [X in V]: ConditionalPropertyValue }; staticScale: Scale; name: K } + : { values: { [X in V]: NonConditionalPropertyValue }; staticScale: Scale; name: K } + : Scale extends Record + ? HasCond extends true + ? { + values: { [T in keyof Scale & string]: ConditionalPropertyValue }; + staticScale: { [T in keyof Scale & string]: string }; + name: K; + } + : { + values: { [T in keyof Scale & string]: NonConditionalPropertyValue }; + staticScale: { [T in keyof Scale & string]: string }; + name: K; + } + : never; + +export type MakeConfig< + Dyn extends ConfigDynamicProperties | undefined, + Stat extends ConfigStaticProperties | undefined, + Cond extends ConfigConditions | undefined, + Short extends Record> | undefined, +> = (Dyn extends ConfigDynamicProperties + ? { + [K in keyof Dyn & string]: DynamicConfigEntry< + K, + NonNullable, + Cond extends undefined ? false : true + >; + } + : Record) & + (Stat extends ConfigStaticProperties + ? { + [K in keyof Stat & string]: StaticConfigEntry< + K, + NonNullable, + Cond extends undefined ? false : true + >; + } + : Record) & + (Short extends Record> + ? { [K in keyof Short & string]: ShorthandProperty } + : Record); + +export type DefinePropertiesReturn = { + config: Config; +}; + +type ValueOrConditionObject = + | T + | null + | Partial>; + +type ValueOrConditionObjectStatic = + | T + | null + | { + [Condition in keyof Values[keyof Values]['conditions']]?: T | null; + }; + +type ChildSprinkle = Sprinkle extends StaticDynamicConditionalProperty + ? ValueOrConditionObject< + PropertyCssValue | (keyof Sprinkle['staticScale'] & string), + Sprinkle['vars'] + > + : Sprinkle extends StaticDynamicConditionalPropertyArray + ? ValueOrConditionObject, Sprinkle['vars']> + : Sprinkle extends DynamicConditionalProperty + ? Sprinkle['dynamicScale'] extends boolean + ? ValueOrConditionObject, Sprinkle['vars']> + : ValueOrConditionObject< + PropertyCssValue | (keyof Sprinkle['dynamicScale'] & string), + Sprinkle['vars'] + > + : Sprinkle extends StaticDynamicConditionalPropertyArray + ? ValueOrConditionObject + : Sprinkle extends StaticDynamicConditionalProperty + ? ValueOrConditionObjectStatic + : Sprinkle extends StaticConditionalProperty + ? ValueOrConditionObjectStatic< + keyof Sprinkle['staticScale'] & string, + Sprinkle['values'] + > + : Sprinkle extends StaticConditionalPropertyArray + ? ValueOrConditionObjectStatic + : Sprinkle extends StaticDynamicProperty + ? (keyof Sprinkle['staticScale'] & string) | PropertyCssValue + : Sprinkle extends StaticDynamicPropertyArray + ? PropertyCssValue + : Sprinkle extends DynamicProperty + ? + | PropertyCssValue + | (Sprinkle['dynamicScale'] extends boolean + ? never + : keyof Sprinkle['dynamicScale'] & string) + : Sprinkle extends StaticProperty + ? keyof Sprinkle['staticScale'] & string + : Sprinkle extends StaticPropertyArray + ? Sprinkle['staticScale'][number] + : never; + +type ChildSprinkles> = { + [Prop in keyof Sprinkles]?: Sprinkles[Prop] extends ShorthandProperty + ? ChildSprinkle + : ChildSprinkle; +}; + +export type SprinklesProps> = Args extends [infer L, ...infer R] + ? (L extends DefinePropertiesReturn> + ? ChildSprinkles + : never) & + SprinklesProps + : Record; + +export interface CommonOptions { + '@layer'?: string; +} + +export type RuntimeFnReturn = { + style: Record; + className: string; +}; diff --git a/packages/@luke-ui/rainbow-sprinkles/tsconfig.json b/packages/@luke-ui/rainbow-sprinkles/tsconfig.json new file mode 100644 index 00000000..dcb8c568 --- /dev/null +++ b/packages/@luke-ui/rainbow-sprinkles/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "lib": ["ES2022"], + "module": "NodeNext", + "moduleDetection": "force", + "moduleResolution": "NodeNext", + "noEmit": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/@luke-ui/react/package.json b/packages/@luke-ui/react/package.json index f8e37619..1ee66682 100644 --- a/packages/@luke-ui/react/package.json +++ b/packages/@luke-ui/react/package.json @@ -30,6 +30,7 @@ "./loading-spinner": "./dist/loading-spinner/index.js", "./numeral": "./dist/numeral/index.js", "./recipes": "./dist/recipes/index.js", + "./styles": "./dist/styles/index.js", "./text": "./dist/text/index.js", "./text-field": "./dist/text-field/index.js", "./text-field/primitive": "./dist/text-field/primitive/index.js", @@ -72,6 +73,7 @@ "dependencies": { "@capsizecss/metrics": "^3.7.0", "@capsizecss/vanilla-extract": "^2.0.4", + "@luke-ui/rainbow-sprinkles": "workspace:*", "@react-aria/utils": "^3.34.0", "@vanilla-extract/css": "^1.20.1", "@vanilla-extract/recipes": "^0.5.7", diff --git a/packages/@luke-ui/react/scripts/generate-docs.ts b/packages/@luke-ui/react/scripts/generate-docs.ts index cdff0e19..f7412459 100644 --- a/packages/@luke-ui/react/scripts/generate-docs.ts +++ b/packages/@luke-ui/react/scripts/generate-docs.ts @@ -27,14 +27,14 @@ const pages = await Promise.all( const parsed = parseBarrel(entry.sourcePath); if (parsed.description) descriptionByPath.set(entry.path, parsed.description); const md = renderPage({ + description: parsed.description, + exports: parsed.exports, + importPath: `${packageJson.name}${entry.path.replace(/^\./, '')}`, kind: 'barrel', slug: entry.slug, - importPath: `${packageJson.name}${entry.path.replace(/^\./, '')}`, tier: entry.tier, - description: parsed.description, - exports: parsed.exports, }); - return { slug: entry.slug, shape: entry.shape, tier: entry.tier, md }; + return { md, shape: entry.shape, slug: entry.slug, tier: entry.tier }; } const parsed = parseComponent(entry.sourcePath); @@ -43,14 +43,14 @@ const pages = await Promise.all( if (parsed.tier) tierByPath.set(entry.path, parsed.tier); const proseMarkdown = await readProse(packageRoot, entry).catch(() => undefined); const md = renderPage({ - kind: 'component', - slug: entry.slug, importPath: `${packageJson.name}${entry.path.replace(/^\./, '')}`, - tier, + kind: 'component', parsed, proseMarkdown, + slug: entry.slug, + tier, }); - return { slug: entry.slug, shape: entry.shape, tier, md }; + return { md, shape: entry.shape, slug: entry.slug, tier }; }), ); @@ -62,15 +62,15 @@ await Promise.all( const entriesWithDescriptions: Array = discovered.map((entry) => { return Object.assign({}, entry, { - tier: tierByPath.get(entry.path) ?? entry.tier, description: descriptionByPath.get(entry.path), + tier: tierByPath.get(entry.path) ?? entry.tier, }); }); const llmsTxt = renderIndex({ - packageName: packageJson.name, entries: entriesWithDescriptions, includeLibraryAuthors: true, + packageName: packageJson.name, }); await writeFile(join(docsDir, 'llms.txt'), llmsTxt, 'utf8'); diff --git a/packages/@luke-ui/react/scripts/tsconfig.json b/packages/@luke-ui/react/scripts/tsconfig.json index 3beb9f9e..e0bc4c8d 100644 --- a/packages/@luke-ui/react/scripts/tsconfig.json +++ b/packages/@luke-ui/react/scripts/tsconfig.json @@ -1,10 +1,10 @@ { - "extends": "../tsconfig.json", "compilerOptions": { "declaration": false, "declarationMap": false, "noEmit": true, "types": ["node"] }, + "extends": "../tsconfig.json", "include": ["./*.ts"] } diff --git a/packages/@luke-ui/react/src/button/index.tsx b/packages/@luke-ui/react/src/button/index.tsx index d6cb2f77..93d16984 100644 --- a/packages/@luke-ui/react/src/button/index.tsx +++ b/packages/@luke-ui/react/src/button/index.tsx @@ -14,25 +14,25 @@ interface PrimitiveButtonVariantProps extends NonNullable ( {isPending && ( - + )} @@ -76,9 +76,9 @@ export function Button(props: ButtonProps): JSX.Element { color="inherit" fontSize={BUTTON_FONT_SIZE[size]} fontWeight="inherit" + lineClamp={1} lineHeight="nospace" shouldDisableTrim - lineClamp={1} > {typeof children === 'function' ? children(renderProps) : children} diff --git a/packages/@luke-ui/react/src/combobox-field/combobox-field.stories.tsx b/packages/@luke-ui/react/src/combobox-field/combobox-field.stories.tsx index 1879004f..11e41b36 100644 --- a/packages/@luke-ui/react/src/combobox-field/combobox-field.stories.tsx +++ b/packages/@luke-ui/react/src/combobox-field/combobox-field.stories.tsx @@ -65,8 +65,8 @@ export const Default = meta.story({ render: function Render() { return ( @@ -229,13 +229,13 @@ export const AsyncFiltering = meta.story({
list.setFilterText(text)} - value={value} - onChange={setValue} label="Pick a Pokemon" + loadingState={list.loadingState} name="pokemon" + onChange={setValue} + onInputChange={(text) => list.setFilterText(text)} placeholder="Search for a Pokemon..." + value={value} > {(item) => {item.name}} @@ -263,8 +263,8 @@ export const AsyncInfiniteScroll = meta.story({ const json = await res.json(); return { - items: json.results, cursor: json.next, + items: json.results, }; }, }); diff --git a/packages/@luke-ui/react/src/combobox-field/index.tsx b/packages/@luke-ui/react/src/combobox-field/index.tsx index aa4be3ad..76659005 100644 --- a/packages/@luke-ui/react/src/combobox-field/index.tsx +++ b/packages/@luke-ui/react/src/combobox-field/index.tsx @@ -52,12 +52,12 @@ export interface ComboboxFieldProps /** Props forwarded to the inner listbox. */ listBoxProps?: DistributiveOmit, 'children' | 'items' | 'loadMoreItem'>; - /** Optional content appended after the main collection, e.g. a load-more sentinel. */ - loadMoreItem?: ComboboxListBoxProps['loadMoreItem']; - /** Async loading state used for built-in loading and empty states. */ loadingState?: ComboboxLoadingState; + /** Optional content appended after the main collection, e.g. a load-more sentinel. */ + loadMoreItem?: ComboboxListBoxProps['loadMoreItem']; + /** Width applied to the popover menu. */ menuWidth?: CSSProperties['width']; diff --git a/packages/@luke-ui/react/src/combobox-field/primitive/input.tsx b/packages/@luke-ui/react/src/combobox-field/primitive/input.tsx index 7f64d1fb..4a00c285 100644 --- a/packages/@luke-ui/react/src/combobox-field/primitive/input.tsx +++ b/packages/@luke-ui/react/src/combobox-field/primitive/input.tsx @@ -40,10 +40,10 @@ export function ComboboxTextInput(props: ComboboxTextInputProps): JSX.Element { return ( { return cx(styles.comboboxTextInput({ size }), className); })} + onClick={handleClick} /> ); } diff --git a/packages/@luke-ui/react/src/combobox-field/primitive/root.tsx b/packages/@luke-ui/react/src/combobox-field/primitive/root.tsx index bca73268..abbe523f 100644 --- a/packages/@luke-ui/react/src/combobox-field/primitive/root.tsx +++ b/packages/@luke-ui/react/src/combobox-field/primitive/root.tsx @@ -26,23 +26,22 @@ export interface ComboboxInputProps extends DistributiveOmit< | 'selectionMode' | 'value' > { - /** The currently selected key (controlled). Pass `null` for no selection. */ - value?: Key | null; - /** The initially selected key (uncontrolled). */ defaultValue?: Key | null; - /** Called when the selected value changes. */ - onChange?: (value: Key | null) => void; - - /** Called when the open state changes. */ - onOpenChange?: (isOpen: boolean) => void; - /** * The interaction required to display the ComboBox menu. * @default 'focus' */ menuTrigger?: 'focus' | 'input' | 'manual'; + + /** Called when the selected value changes. */ + onChange?: (value: Key | null) => void; + + /** Called when the open state changes. */ + onOpenChange?: (isOpen: boolean) => void; + /** The currently selected key (controlled). Pass `null` for no selection. */ + value?: Key | null; } export function ComboboxInput(props: ComboboxInputProps): JSX.Element { @@ -51,10 +50,10 @@ export function ComboboxInput(props: ComboboxInputProps): J return ( { return cx(styles.comboboxRoot, renderedClassName); })} + menuTrigger={menuTrigger} /> ); } diff --git a/packages/@luke-ui/react/src/field/primitive/index.tsx b/packages/@luke-ui/react/src/field/primitive/index.tsx index 26441a85..86fb4c8e 100644 --- a/packages/@luke-ui/react/src/field/primitive/index.tsx +++ b/packages/@luke-ui/react/src/field/primitive/index.tsx @@ -1,14 +1,14 @@ import type { ComponentProps, JSX, ReactNode } from 'react'; -import { FieldDescription } from './description.js'; import type { FieldDescriptionProps } from './description.js'; +import { FieldDescription } from './description.js'; import type { FieldErrorProps } from './error.js'; import { FieldError } from './error.js'; -import { FieldLabel } from './label.js'; import type { FieldLabelProps, FieldNecessityIndicator } from './label.js'; +import { FieldLabel } from './label.js'; import { Field as PrimitiveField } from './root.js'; -export { FieldDescription, FieldError, FieldLabel }; export type { FieldDescriptionProps, FieldErrorProps, FieldLabelProps, FieldNecessityIndicator }; +export { FieldDescription, FieldError, FieldLabel }; type PrimitiveFieldProps = ComponentProps; diff --git a/packages/@luke-ui/react/src/heading-context/index.tsx b/packages/@luke-ui/react/src/heading-context/index.tsx index a4db8e0b..cc127978 100644 --- a/packages/@luke-ui/react/src/heading-context/index.tsx +++ b/packages/@luke-ui/react/src/heading-context/index.tsx @@ -42,8 +42,8 @@ export const useHeadingLevel = ( ): HeadingLevelsRenderProps => { const level = useContext(HeadingLevelContext) ?? fallback; return { - level, element: `h${level}`, + level, }; }; @@ -98,8 +98,8 @@ export const HeadingLevels = ({ base, children }: HeadingLevelsProps) => { })(); const renderProps: HeadingLevelsRenderProps = { - level: value, element: `h${value}`, + level: value, }; return ( diff --git a/packages/@luke-ui/react/src/icon-button/icon-button.stories.tsx b/packages/@luke-ui/react/src/icon-button/icon-button.stories.tsx index c38d5f83..6d7dee4f 100644 --- a/packages/@luke-ui/react/src/icon-button/icon-button.stories.tsx +++ b/packages/@luke-ui/react/src/icon-button/icon-button.stories.tsx @@ -60,7 +60,7 @@ export const AllIcons = meta.story({ render: (props) => (
{iconNames.map((iconName) => ( - + ))}
), diff --git a/packages/@luke-ui/react/src/icon-button/index.tsx b/packages/@luke-ui/react/src/icon-button/index.tsx index 72e5b187..8c7c9a33 100644 --- a/packages/@luke-ui/react/src/icon-button/index.tsx +++ b/packages/@luke-ui/react/src/icon-button/index.tsx @@ -62,7 +62,7 @@ export function IconButton(props: IconButtonProps): JSX.Element { })} size={size} > - + ); } diff --git a/packages/@luke-ui/react/src/icon/icon.stories.tsx b/packages/@luke-ui/react/src/icon/icon.stories.tsx index 635db5d2..00b8444b 100644 --- a/packages/@luke-ui/react/src/icon/icon.stories.tsx +++ b/packages/@luke-ui/react/src/icon/icon.stories.tsx @@ -23,8 +23,8 @@ const colors = tokenKeys(tokens.foregroundColor); const wrapStyle = { display: 'grid', - gridTemplateColumns: 'repeat(auto-fill, minmax(11rem, 1fr))', gap: '1rem', + gridTemplateColumns: 'repeat(auto-fill, minmax(11rem, 1fr))', } as const satisfies CSSProperties; const iconItemStyle = { @@ -98,19 +98,19 @@ export const CreateYourOwnIcon = meta.story({ render: () => (
- + xsmall
- + small
- + medium
- + large
@@ -127,11 +127,11 @@ export const AllIcons = meta.story({ {iconNames.map((name) => (
  • diff --git a/packages/@luke-ui/react/src/numeral/index.tsx b/packages/@luke-ui/react/src/numeral/index.tsx index 91571380..75d70950 100644 --- a/packages/@luke-ui/react/src/numeral/index.tsx +++ b/packages/@luke-ui/react/src/numeral/index.tsx @@ -18,11 +18,6 @@ export type NumeralPrecision = number | readonly [number, number]; export interface NumeralProps extends Omit { /** Enables compact notation (`1.2K`, `1.2 thousand`). */ abbreviate?: NumeralAbbreviation; - /** - * Text alignment. Right-aligned by default for numeric columns. - * @default 'end' - */ - textAlign?: TextProps['textAlign']; /** Currency code such as `USD`. */ currency?: string; /** Number format style. Inferred from `currency`/`unit` when omitted. */ @@ -33,6 +28,11 @@ export interface NumeralProps extends Omit; /** Number to format. */ @@ -177,8 +177,8 @@ export function Numeral(props: NumeralProps) { (
    - Acme Corporation shares hit today + Acme Corporation shares hit today We asked investors which private company’s stock they would most like to own. More than{' '} - of respondents picked Acme Corp. + of respondents picked Acme Corp. Hooli, Acme’s parent component, went public earlier in the year. The median commitment was{' '} - though the average was significantly + though the average was significantly higher.
    diff --git a/packages/@luke-ui/react/src/recipes/button-composed.css.ts b/packages/@luke-ui/react/src/recipes/button-composed.css.ts index b1f27e48..fa3857e2 100644 --- a/packages/@luke-ui/react/src/recipes/button-composed.css.ts +++ b/packages/@luke-ui/react/src/recipes/button-composed.css.ts @@ -12,6 +12,12 @@ export const buttonContent = recipeInLayer('recipes', { }); export const buttonLabel = recipeInLayer('recipes', { + base: { + alignItems: 'center', + display: 'inline-flex', + gap: vars.space.xsmall, + minInlineSize: 0, + }, defaultVariants: { isPending: false, }, @@ -23,12 +29,6 @@ export const buttonLabel = recipeInLayer('recipes', { }, }, }, - base: { - alignItems: 'center', - display: 'inline-flex', - gap: vars.space.xsmall, - minInlineSize: 0, - }, }); export type ButtonLabelVariants = RecipeVariants; diff --git a/packages/@luke-ui/react/src/recipes/button.css.ts b/packages/@luke-ui/react/src/recipes/button.css.ts index ebac832c..90b4f201 100644 --- a/packages/@luke-ui/react/src/recipes/button.css.ts +++ b/packages/@luke-ui/react/src/recipes/button.css.ts @@ -3,6 +3,22 @@ import { recipeInLayer, styleInLayer } from '../styles/layered-style.css.js'; import { vars } from '../styles/vars.css.js'; const base = styleInLayer('recipes', { + '@media': { + '(forced-colors: active)': { + borderColor: 'ButtonText', + selectors: { + '&:disabled': { + borderColor: 'GrayText', + color: 'GrayText', + }, + '&:enabled:hover': { + backgroundColor: 'Highlight', + borderColor: 'Highlight', + color: 'HighlightText', + }, + }, + }, + }, alignItems: 'center', appearance: 'none', backgroundColor: vars.backgroundColor.default, @@ -17,40 +33,23 @@ const base = styleInLayer('recipes', { justifyContent: 'center', lineHeight: vars.font.lineHeight.nospace, minInlineSize: 0, - textDecoration: 'none', - transition: - 'color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events', - transitionTimingFunction: vars.motion.easing.standard, - transitionDuration: vars.motion.duration.fast, - whiteSpace: 'nowrap', selectors: { - '&:enabled:active': { - scale: 0.98, - }, '&:disabled': { backgroundColor: vars.backgroundColor.disabled, borderColor: vars.backgroundColor.disabled, color: vars.foregroundColor.disabled, }, - }, - - '@media': { - '(forced-colors: active)': { - borderColor: 'ButtonText', - selectors: { - '&:enabled:hover': { - backgroundColor: 'Highlight', - borderColor: 'Highlight', - color: 'HighlightText', - }, - '&:disabled': { - borderColor: 'GrayText', - color: 'GrayText', - }, - }, + '&:enabled:active': { + scale: 0.98, }, }, + textDecoration: 'none', + transition: + 'color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events', + transitionDuration: vars.motion.duration.fast, + transitionTimingFunction: vars.motion.easing.standard, + whiteSpace: 'nowrap', }); export const button = recipeInLayer('recipes', { @@ -68,46 +67,31 @@ export const button = recipeInLayer('recipes', { }, }, size: { - small: { - fontSize: vars.font.size.small, - blockSize: vars.controlSize.small, - paddingInline: vars.space.small, - }, medium: { - fontSize: vars.font.size.standard, blockSize: vars.controlSize.medium, + fontSize: vars.font.size.standard, paddingInline: vars.space.medium, }, + small: { + blockSize: vars.controlSize.small, + fontSize: vars.font.size.small, + paddingInline: vars.space.small, + }, }, tone: { - primary: { - backgroundColor: vars.themeColor.buttonBackgroundColor, - borderColor: vars.themeColor.buttonBorderColor, - color: vars.themeColor.buttonColor, - selectors: { - '&:enabled:hover': { - backgroundColor: vars.themeColor.buttonBackgroundColorHover, - borderColor: vars.themeColor.buttonBorderColorHover, - }, - '&:enabled:active': { - backgroundColor: vars.themeColor.buttonBackgroundColorActive, - borderColor: vars.themeColor.buttonBorderColorActive, - }, - }, - }, critical: { backgroundColor: vars.backgroundColor.criticalBold, borderColor: vars.backgroundColor.criticalBold, color: vars.themeColor.buttonColor, selectors: { - '&:enabled:hover': { - backgroundColor: vars.backgroundColor.criticalBoldHover, - borderColor: vars.backgroundColor.criticalBoldHover, - }, '&:enabled:active': { backgroundColor: vars.backgroundColor.criticalBoldPressed, borderColor: vars.backgroundColor.criticalBoldPressed, }, + '&:enabled:hover': { + backgroundColor: vars.backgroundColor.criticalBoldHover, + borderColor: vars.backgroundColor.criticalBoldHover, + }, }, }, ghost: { @@ -115,14 +99,14 @@ export const button = recipeInLayer('recipes', { borderColor: 'transparent', color: vars.foregroundColor.neutralBold, selectors: { - '&:enabled:hover': { - backgroundColor: vars.backgroundColor.neutralHover, - borderColor: vars.backgroundColor.neutralHover, - }, '&:enabled:active': { backgroundColor: vars.backgroundColor.neutralPressed, borderColor: vars.backgroundColor.neutralPressed, }, + '&:enabled:hover': { + backgroundColor: vars.backgroundColor.neutralHover, + borderColor: vars.backgroundColor.neutralHover, + }, }, }, neutral: { @@ -130,11 +114,26 @@ export const button = recipeInLayer('recipes', { borderColor: vars.border.default, color: vars.foregroundColor.primary, selectors: { + '&:enabled:active': { + backgroundColor: vars.backgroundColor.pressed, + }, '&:enabled:hover': { backgroundColor: vars.backgroundColor.hover, }, + }, + }, + primary: { + backgroundColor: vars.themeColor.buttonBackgroundColor, + borderColor: vars.themeColor.buttonBorderColor, + color: vars.themeColor.buttonColor, + selectors: { '&:enabled:active': { - backgroundColor: vars.backgroundColor.pressed, + backgroundColor: vars.themeColor.buttonBackgroundColorActive, + borderColor: vars.themeColor.buttonBorderColorActive, + }, + '&:enabled:hover': { + backgroundColor: vars.themeColor.buttonBackgroundColorHover, + borderColor: vars.themeColor.buttonBorderColorHover, }, }, }, diff --git a/packages/@luke-ui/react/src/recipes/combobox.css.ts b/packages/@luke-ui/react/src/recipes/combobox.css.ts index 7e7064f8..ff135c82 100644 --- a/packages/@luke-ui/react/src/recipes/combobox.css.ts +++ b/packages/@luke-ui/react/src/recipes/combobox.css.ts @@ -42,9 +42,6 @@ export const comboboxControl = recipeInLayer('recipes', { inlineSize: '100%', minInlineSize: 0, overflow: 'hidden', - transitionDuration: vars.motion.duration.fast, - transitionProperty, - transitionTimingFunction: vars.motion.easing.standard, selectors: { [controlDisabledSelector]: { @@ -78,20 +75,23 @@ export const comboboxControl = recipeInLayer('recipes', { backgroundColor: vars.backgroundColor.subtle, }, }, + transitionDuration: vars.motion.duration.fast, + transitionProperty, + transitionTimingFunction: vars.motion.easing.standard, }, defaultVariants: { size: 'medium', }, variants: { size: { - small: { - fontSize: vars.font.size.small, - minBlockSize: vars.controlSize.small, - }, medium: { fontSize: vars.font.size.standard, minBlockSize: vars.controlSize.medium, }, + small: { + fontSize: vars.font.size.small, + minBlockSize: vars.controlSize.small, + }, }, }, }); @@ -129,16 +129,16 @@ export const comboboxTextInput = recipeInLayer('recipes', { }, variants: { size: { - small: { - blockSize: vars.controlSize.small, - paddingInlineEnd: vars.space.xsmall, - paddingInlineStart: vars.space.xsmall, - }, medium: { blockSize: vars.controlSize.medium, paddingInlineEnd: vars.space.small, paddingInlineStart: vars.space.small, }, + small: { + blockSize: vars.controlSize.small, + paddingInlineEnd: vars.space.xsmall, + paddingInlineStart: vars.space.xsmall, + }, }, }, }); @@ -205,16 +205,16 @@ export const comboboxTrigger = recipeInLayer('recipes', { }, variants: { size: { - small: { - blockSize: vars.controlSize.small, - paddingInlineEnd: vars.space.xsmall, - paddingInlineStart: vars.space.xsmall, - }, medium: { blockSize: vars.controlSize.medium, paddingInlineEnd: vars.space.small, paddingInlineStart: vars.space.small, }, + small: { + blockSize: vars.controlSize.small, + paddingInlineEnd: vars.space.xsmall, + paddingInlineStart: vars.space.xsmall, + }, }, }, }); @@ -257,26 +257,26 @@ export const comboboxLoadMoreItem = recipeInLayer('recipes', { }, variants: { size: { - small: { - minBlockSize: vars.controlSize.small, - paddingBlock: vars.space.xxsmall, - paddingInline: vars.space.xsmall, - }, medium: { minBlockSize: vars.controlSize.medium, paddingBlock: vars.space.xsmall, paddingInline: vars.space.small, }, + small: { + minBlockSize: vars.controlSize.small, + paddingBlock: vars.space.xxsmall, + paddingInline: vars.space.xsmall, + }, }, }, }); export const comboboxSection = recipeInLayer('recipes', { base: { - paddingBlock: vars.space.xsmall, display: 'flex', flexDirection: 'column', gap: vars.space.xxsmall, + paddingBlock: vars.space.xsmall, selectors: { '& + &': { @@ -330,6 +330,12 @@ export const comboboxItem = recipeInLayer('recipes', { color: vars.foregroundColor.disabled, cursor: 'not-allowed', }, + '&[data-focus-visible="true"]': { + outlineColor: vars.themeColor.paletteThemePrimary200, + outlineOffset: 0, + outlineStyle: 'solid', + outlineWidth: '3px', + }, '&[data-focused="true"]:not([data-disabled="true"])': { backgroundColor: vars.backgroundColor.hover, }, @@ -337,12 +343,6 @@ export const comboboxItem = recipeInLayer('recipes', { backgroundColor: vars.backgroundColor.subtle, fontWeight: vars.font.weight.medium, }, - '&[data-focus-visible="true"]': { - outlineColor: vars.themeColor.paletteThemePrimary200, - outlineOffset: 0, - outlineStyle: 'solid', - outlineWidth: '3px', - }, }, }, defaultVariants: { @@ -350,14 +350,14 @@ export const comboboxItem = recipeInLayer('recipes', { }, variants: { size: { - small: { + medium: { fontSize: vars.font.size.small, - paddingBlock: vars.space.xsmall, + paddingBlock: vars.space.small, paddingInline: vars.space.small, }, - medium: { + small: { fontSize: vars.font.size.small, - paddingBlock: vars.space.small, + paddingBlock: vars.space.xsmall, paddingInline: vars.space.small, }, }, diff --git a/packages/@luke-ui/react/src/recipes/icon-button.css.ts b/packages/@luke-ui/react/src/recipes/icon-button.css.ts index d049b3ae..5d6ec578 100644 --- a/packages/@luke-ui/react/src/recipes/icon-button.css.ts +++ b/packages/@luke-ui/react/src/recipes/icon-button.css.ts @@ -9,12 +9,12 @@ export const iconButtonReset = styleInLayer('utilities', { export const iconButton = recipeInLayer('recipes', { variants: { size: { - small: { - inlineSize: vars.controlSize.small, - }, medium: { inlineSize: vars.controlSize.medium, }, + small: { + inlineSize: vars.controlSize.small, + }, }, }, }); diff --git a/packages/@luke-ui/react/src/recipes/link.css.ts b/packages/@luke-ui/react/src/recipes/link.css.ts index c9b0a521..2a5efd56 100644 --- a/packages/@luke-ui/react/src/recipes/link.css.ts +++ b/packages/@luke-ui/react/src/recipes/link.css.ts @@ -22,12 +22,12 @@ export const link = recipeInLayer('recipes', { isStandalone: { false: {}, true: { - textDecoration: 'none', selectors: { '&:enabled:hover': { textDecoration: 'underline', }, }, + textDecoration: 'none', }, }, tone: { @@ -39,6 +39,9 @@ export const link = recipeInLayer('recipes', { }, }, }, + inverted: { + color: vars.foregroundColor.inverse, + }, neutral: { color: vars.foregroundColor.secondary, selectors: { @@ -47,9 +50,6 @@ export const link = recipeInLayer('recipes', { }, }, }, - inverted: { - color: vars.foregroundColor.inverse, - }, }, }, }); diff --git a/packages/@luke-ui/react/src/recipes/text-input.css.ts b/packages/@luke-ui/react/src/recipes/text-input.css.ts index cc00baca..10881b1c 100644 --- a/packages/@luke-ui/react/src/recipes/text-input.css.ts +++ b/packages/@luke-ui/react/src/recipes/text-input.css.ts @@ -24,6 +24,26 @@ const descendantDisabledSelector = `:where(${descendantDisabledState}) &`; export const textInputGroup = recipeInLayer('recipes', { base: { + '@media': { + '(forced-colors: active)': { + selectors: { + [groupFocusWithinSelector]: { + outlineColor: 'Highlight', + }, + [groupInvalidSelector]: { + borderColor: 'ButtonText', + }, + [groupInvalidFocusWithinSelector]: { + borderColor: 'ButtonText', + outlineColor: 'Highlight', + }, + [groupDisabledSelector]: { + borderColor: 'GrayText', + color: 'GrayText', + }, + }, + }, + }, alignItems: 'center', backgroundColor: vars.backgroundColor.input, borderColor: vars.border.input, @@ -37,9 +57,6 @@ export const textInputGroup = recipeInLayer('recipes', { inlineSize: '100%', minInlineSize: 0, overflow: 'hidden', - transitionDuration: vars.motion.duration.fast, - transitionProperty, - transitionTimingFunction: vars.motion.easing.standard, selectors: { [groupDisabledSelector]: { @@ -73,40 +90,22 @@ export const textInputGroup = recipeInLayer('recipes', { backgroundColor: vars.backgroundColor.subtle, }, }, - - '@media': { - '(forced-colors: active)': { - selectors: { - [groupFocusWithinSelector]: { - outlineColor: 'Highlight', - }, - [groupInvalidSelector]: { - borderColor: 'ButtonText', - }, - [groupInvalidFocusWithinSelector]: { - borderColor: 'ButtonText', - outlineColor: 'Highlight', - }, - [groupDisabledSelector]: { - borderColor: 'GrayText', - color: 'GrayText', - }, - }, - }, - }, + transitionDuration: vars.motion.duration.fast, + transitionProperty, + transitionTimingFunction: vars.motion.easing.standard, }, defaultVariants: { size: 'medium', }, variants: { size: { - small: { - fontSize: vars.font.size.small, - blockSize: vars.controlSize.small, - }, medium: { - fontSize: vars.font.size.standard, blockSize: vars.controlSize.medium, + fontSize: vars.font.size.standard, + }, + small: { + blockSize: vars.controlSize.small, + fontSize: vars.font.size.small, }, }, }, @@ -150,16 +149,16 @@ export const textInputControl = recipeInLayer('recipes', { }, variants: { size: { - small: { - blockSize: vars.controlSize.small, - paddingInlineEnd: vars.space.xsmall, - paddingInlineStart: vars.space.xsmall, - }, medium: { blockSize: vars.controlSize.medium, paddingInlineEnd: vars.space.small, paddingInlineStart: vars.space.small, }, + small: { + blockSize: vars.controlSize.small, + paddingInlineEnd: vars.space.xsmall, + paddingInlineStart: vars.space.xsmall, + }, }, }, }); @@ -167,13 +166,13 @@ export const textInputControl = recipeInLayer('recipes', { export const textInputAdornmentStart = recipeInLayer('recipes', { base: { alignItems: 'center', + borderInlineEndColor: vars.border.input, + borderInlineEndStyle: 'solid', + borderInlineEndWidth: vars.borderWidth.thin, color: vars.foregroundColor.secondary, display: 'inline-flex', flexShrink: 0, lineHeight: vars.font.lineHeight.nospace, - borderInlineEndColor: vars.border.input, - borderInlineEndStyle: 'solid', - borderInlineEndWidth: vars.borderWidth.thin, selectors: { [descendantDisabledSelector]: { @@ -187,14 +186,14 @@ export const textInputAdornmentStart = recipeInLayer('recipes', { }, variants: { size: { - small: { - paddingInlineEnd: vars.space.xsmall, - paddingInlineStart: vars.space.xsmall, - }, medium: { paddingInlineEnd: vars.space.small, paddingInlineStart: vars.space.small, }, + small: { + paddingInlineEnd: vars.space.xsmall, + paddingInlineStart: vars.space.xsmall, + }, }, }, }); @@ -203,13 +202,13 @@ export const textInputAdornmentEnd = recipeInLayer('recipes', { base: { alignItems: 'center', backgroundColor: vars.backgroundColor.subtle, + borderInlineStartColor: vars.border.input, + borderInlineStartStyle: 'solid', + borderInlineStartWidth: vars.borderWidth.thin, color: vars.foregroundColor.secondary, display: 'inline-flex', flexShrink: 0, lineHeight: vars.font.lineHeight.nospace, - borderInlineStartColor: vars.border.input, - borderInlineStartStyle: 'solid', - borderInlineStartWidth: vars.borderWidth.thin, selectors: { [descendantDisabledSelector]: { @@ -223,14 +222,14 @@ export const textInputAdornmentEnd = recipeInLayer('recipes', { }, variants: { size: { - small: { - paddingInlineEnd: vars.space.xsmall, - paddingInlineStart: vars.space.xsmall, - }, medium: { paddingInlineEnd: vars.space.small, paddingInlineStart: vars.space.small, }, + small: { + paddingInlineEnd: vars.space.xsmall, + paddingInlineStart: vars.space.xsmall, + }, }, }, }); diff --git a/packages/@luke-ui/react/src/recipes/text.css.ts b/packages/@luke-ui/react/src/recipes/text.css.ts index 22efa352..4c778f30 100644 --- a/packages/@luke-ui/react/src/recipes/text.css.ts +++ b/packages/@luke-ui/react/src/recipes/text.css.ts @@ -167,9 +167,11 @@ const textVariantVariants = createVariants(textVariantKeys, (variant) => ({ fontVariantNumeric: variant === 'unset' ? 'normal' : variant, })); -const textWrapVariants = createVariants(textWrapKeys, (textWrap) => ({ - textWrap: textWrap === 'unset' ? 'wrap' : textWrap, -})); +const textWrapVariants = { + balance: { textWrap: 'balance' }, + pretty: { textWrap: 'pretty' }, + unset: {}, +} as const; const fontSizeVariants = Object.fromEntries(fontSizeKeys.map((key) => [key, {}])) as Record< FontSizeToken, @@ -231,50 +233,50 @@ export const text = recipeInLayer('recipes', { base, compoundVariants: typographyCompoundVariants, defaultVariants: { - textAlign: 'start', color: 'neutralBold', fontFamily: 'sans', fontSize: 'standard', fontWeight: 'regular', + isVisuallyHidden: false, + lineClamp: false, lineHeight: 'loose', - shouldInheritFont: false, shouldDisableTrim: false, + shouldInheritFont: false, + textAlign: 'start', textDecoration: 'none', textTransform: 'none', - lineClamp: false, - variant: 'unset', - isVisuallyHidden: false, textWrap: 'unset', + variant: 'unset', }, variants: { color: colorVariants, fontFamily: fontFamilyVariants, fontSize: fontSizeVariants, fontWeight: fontWeightVariants, - lineHeight: lineHeightVariants, - shouldInheritFont: { + isVisuallyHidden: { false: {}, true: { - font: 'inherit', + position: 'absolute', + transform: 'scale(0)', }, }, + lineClamp: lineClampVariants, + lineHeight: lineHeightVariants, shouldDisableTrim: { false: {}, true: {}, }, - lineClamp: lineClampVariants, - textAlign: textAlignVariants, - textDecoration: textDecorationVariants, - textTransform: textTransformVariants, - variant: textVariantVariants, - isVisuallyHidden: { + shouldInheritFont: { false: {}, true: { - position: 'absolute', - transform: 'scale(0)', + font: 'inherit', }, }, + textAlign: textAlignVariants, + textDecoration: textDecorationVariants, + textTransform: textTransformVariants, textWrap: textWrapVariants, + variant: textVariantVariants, }, }); diff --git a/packages/@luke-ui/react/src/styles/index.ts b/packages/@luke-ui/react/src/styles/index.ts new file mode 100644 index 00000000..428a2efa --- /dev/null +++ b/packages/@luke-ui/react/src/styles/index.ts @@ -0,0 +1,2 @@ +export type { SprinklesProps } from './utilities.css.js'; +export { createSprinkles } from './utilities.css.js'; diff --git a/packages/@luke-ui/react/src/styles/layers.css.ts b/packages/@luke-ui/react/src/styles/layers.css.ts index e6cf5877..bcf555af 100644 --- a/packages/@luke-ui/react/src/styles/layers.css.ts +++ b/packages/@luke-ui/react/src/styles/layers.css.ts @@ -9,9 +9,9 @@ import { globalLayer } from '@vanilla-extract/css'; * - **utilities** — One-off overrides; highest-priority layer for escape hatches. */ export const layers = { + recipes: globalLayer('recipes'), reset: globalLayer('reset'), theme: globalLayer('theme'), - recipes: globalLayer('recipes'), utilities: globalLayer('utilities'), } as const; diff --git a/packages/@luke-ui/react/src/styles/utilities.css.ts b/packages/@luke-ui/react/src/styles/utilities.css.ts new file mode 100644 index 00000000..e4ea53ef --- /dev/null +++ b/packages/@luke-ui/react/src/styles/utilities.css.ts @@ -0,0 +1,95 @@ +import { defineProperties, defineSprinkles } from '@luke-ui/rainbow-sprinkles'; +import type { SprinklesFn } from '@luke-ui/rainbow-sprinkles'; +import { tokenKeys, tokens } from '../tokens/index.js'; +import type { TokenName } from '../tokens/index.js'; +import { layers } from './layers.css.js'; +import { vars } from './vars.css.js'; + +// Builds a { [tokenKey]: cssVar } record while preserving the literal key type union. +function tokenScale( + group: TGroup, + varRecord: { readonly [K in TokenName]: string }, +): { [K in TokenName]: string } { + const scale = {} as { [K in TokenName]: string }; + for (const key of tokenKeys(group)) { + scale[key] = varRecord[key]; + } + return scale; +} + +const breakpointNames = tokenKeys(tokens.breakpoints); +const layerName = layers.utilities; + +const responsiveConditions = { + xsmall: {}, + ...Object.fromEntries( + breakpointNames.map((bp) => [ + bp, + { + '@media': `screen and (width >= ${tokens.breakpoints[bp].$value.value}${tokens.breakpoints[bp].$value.unit})`, + }, + ]), + ), +}; + +const spaceScale = tokenScale(tokens.space, vars.space); + +const responsiveProperties = defineProperties({ + '@layer': layerName, + conditions: responsiveConditions, + defaultCondition: 'xsmall', + dynamicProperties: { + columnGap: spaceScale, + gap: spaceScale, + padding: spaceScale, + paddingBlock: spaceScale, + paddingBlockEnd: spaceScale, + paddingBlockStart: spaceScale, + paddingInline: spaceScale, + paddingInlineEnd: spaceScale, + paddingInlineStart: spaceScale, + rowGap: spaceScale, + }, + shorthands: { + p: ['padding'], + px: ['paddingInlineStart', 'paddingInlineEnd'], + py: ['paddingBlockStart', 'paddingBlockEnd'], + }, + staticProperties: { + alignItems: ['flex-start', 'center', 'flex-end', 'baseline', 'stretch'], + blockSize: ['auto', '100%', 'fit-content', 'min-content', 'max-content'], + display: ['flex', 'block', 'inline', 'inline-flex', 'none'], + flexBasis: ['auto', '0'], + flexDirection: ['row', 'row-reverse', 'column', 'column-reverse'], + flexGrow: ['0', '1'], + flexShrink: ['0', '1'], + inlineSize: ['auto', '100%', 'fit-content', 'min-content', 'max-content'], + justifyContent: ['flex-start', 'center', 'flex-end', 'space-between', 'space-around'], + maxBlockSize: ['100%', 'none', '20rem', '40rem'], + maxInlineSize: ['100%', 'none', '20rem', '40rem'], + minBlockSize: ['0', '100%'], + minInlineSize: ['0', '100%'], + overflow: ['visible', 'hidden', 'auto', 'scroll', 'clip'], + overflowX: ['visible', 'hidden', 'auto', 'scroll', 'clip'], + overflowY: ['visible', 'hidden', 'auto', 'scroll', 'clip'], + textOverflow: ['ellipsis', 'clip'], + }, +}); + +const pseudoProperties = defineProperties({ + '@layer': layerName, + conditions: { + default: {}, + focusVisible: { selector: '&:focus-visible' }, + hover: { selector: '&:hover' }, + }, + defaultCondition: 'default', + staticProperties: { + backgroundColor: tokenScale(tokens.backgroundColor, vars.backgroundColor), + }, +}); + +export const createSprinkles: SprinklesFn<[typeof responsiveProperties, typeof pseudoProperties]> = + defineSprinkles(responsiveProperties, pseudoProperties); + +export type SprinklesProps = Parameters[0]; diff --git a/packages/@luke-ui/react/src/styles/utilities.stories.tsx b/packages/@luke-ui/react/src/styles/utilities.stories.tsx new file mode 100644 index 00000000..644a61c0 --- /dev/null +++ b/packages/@luke-ui/react/src/styles/utilities.stories.tsx @@ -0,0 +1,200 @@ +import { Button } from '@luke-ui/react/button'; +import { createSprinkles } from '@luke-ui/react/styles'; +import { vars } from '@luke-ui/react/theme'; +import { mergeProps } from '@luke-ui/react/utils'; +import type { CSSProperties } from 'react'; +import preview from '../../.storybook/preview.js'; + +const meta = preview.meta({ + title: 'Foundation/Utilities', +}); + +const panelStyle = { + borderColor: vars.border.default, + borderStyle: 'dashed', + borderWidth: 1, + padding: '1rem', +} as const satisfies CSSProperties; + +const stackStyle = { + display: 'flex', + flexDirection: 'column', + gap: '1rem', +} as const satisfies CSSProperties; + +/** + * `createSprinkles` returns a `className` and `style` object for token-aware, type-safe + * layout utilities. Use it with any element's `className` and `style` props. + */ +export const Layout = meta.story({ + render: () => { + const container = createSprinkles({ + display: 'flex', + flexDirection: 'column', + gap: 'medium', + padding: 'large', + }); + const row = createSprinkles({ + alignItems: 'center', + display: 'flex', + gap: 'small', + }); + return ( +
    +
    +
    + Row item 1 +
    +
    + Row item 2 (grows) +
    +
    +
    + ); + }, +}); + +/** + * Responsive values use object notation keyed by breakpoint names. Values + * cascade from smaller to larger breakpoints, so only changes need to be + * specified. + */ +export const Responsive = meta.story({ + render: () => { + const responsive = createSprinkles({ + display: 'flex', + flexDirection: { xsmall: 'column', medium: 'row' }, + gap: { xsmall: 'small', medium: 'large' }, + padding: { xsmall: 'small', large: 'xlarge' }, + }); + return ( +
    +
    + Resize viewport to see flex direction change at 768px. +
    +
    + Second item +
    +
    + ); + }, +}); + +/** + * Pseudo-state conditions apply styles on hover or focus-visible. Use + * `backgroundColor` with a condition object. + */ +export const PseudoStates = meta.story({ + render: () => { + const interactive = createSprinkles({ + padding: 'medium', + backgroundColor: { default: 'neutral', hover: 'neutralHover', focusVisible: 'input' }, + }); + return ( +
    + +
    + ); + }, +}); + +/** + * Combine `createSprinkles` with React Aria Components' `render` prop to style the + * underlying DOM element while preserving RAC behavior and accessibility. + * Use `mergeProps` to merge the provided DOM props with `createSprinkles()` output. + */ +export const WithRenderProp = meta.story({ + render: () => { + const buttonBox = createSprinkles({ + padding: 'medium', + backgroundColor: { default: 'neutral', hover: 'neutralHover' }, + }); + return ( + + )} + > + Button with createSprinkles utilities + + ); + }, +}); + +/** + * Shorthands expand to multiple properties. `px` sets both inline padding + * sides; `py` sets both block padding sides. + */ +export const Shorthands = meta.story({ + render: () => { + const padding = createSprinkles({ px: 'large', py: 'small' }); + return ( +
    +
    + Large horizontal padding, small vertical padding +
    +
    + ); + }, +}); + +/** + * Overflow and sizing utilities control content clipping and element + * dimensions. Use `textOverflow: 'ellipsis'` with `inlineSize` constraints + * for truncation patterns. + */ +export const OverflowAndSizing = meta.story({ + render: () => { + const truncate = createSprinkles({ + display: 'block', + inlineSize: '100%', + maxInlineSize: '20rem', + minInlineSize: '0', + overflowX: 'clip', + textOverflow: 'ellipsis', + }); + return ( +
    +
    + This text is truncated with an ellipsis when it exceeds the max-inline-size of 20rem. + Lorem ipsum dolor sit amet. +
    +
    + ); + }, +}); diff --git a/packages/@luke-ui/react/src/styles/vars.css.ts b/packages/@luke-ui/react/src/styles/vars.css.ts index 6db4518f..2bc6eb8e 100644 --- a/packages/@luke-ui/react/src/styles/vars.css.ts +++ b/packages/@luke-ui/react/src/styles/vars.css.ts @@ -62,11 +62,11 @@ const contract = createThemeContract({ borderWidth: toContract(tokens.borderWidth), boxShadow: toContract(tokens.boxShadow), controlSize: toContract(tokens.controlSize), - iconSize: toContract(tokens.iconSize), fontFamily: toContract(tokens.fontFamily), fontSize: toContract(tokens.fontSize), fontWeight: toContract(tokens.fontWeight), foregroundColor: toContract(tokens.foregroundColor), + iconSize: toContract(tokens.iconSize), lineHeight: toContract(tokens.lineHeight), motionDuration: toContract(tokens.motionDuration), motionEasing: toContract(tokens.motionEasing), @@ -82,11 +82,11 @@ export const themeClass = createTheme(contract, { borderWidth: toStringValues(tokens.borderWidth), boxShadow: toStringValues(tokens.boxShadow), controlSize: toStringValues(tokens.controlSize), - iconSize: toStringValues(tokens.iconSize), fontFamily: toStringValues(tokens.fontFamily), fontSize: toStringValues(tokens.fontSize), fontWeight: toStringValues(tokens.fontWeight), foregroundColor: toStringValues(tokens.foregroundColor), + iconSize: toStringValues(tokens.iconSize), lineHeight: toStringValues(tokens.lineHeight), motionDuration: toStringValues(tokens.motionDuration), motionEasing: toStringValues(tokens.motionEasing), @@ -146,8 +146,8 @@ const themeRootSelector = classSelector(lukeUiClassNames.themeRoot); globalStyleInLayer('theme', themeRootSelector, { accentColor: vars.themeColor.paletteThemePrimary500, - colorScheme: 'light', color: vars.foregroundColor.primary, + colorScheme: 'light', fontFamily: vars.font.family.body, fontSize: vars.font.size.standard, lineHeight: vars.font.lineHeight.loose, diff --git a/packages/@luke-ui/react/src/text-field/index.tsx b/packages/@luke-ui/react/src/text-field/index.tsx index 35951c49..2f56467b 100644 --- a/packages/@luke-ui/react/src/text-field/index.tsx +++ b/packages/@luke-ui/react/src/text-field/index.tsx @@ -11,18 +11,18 @@ import type { TextInputSize } from './primitive/index.js'; import { TextInput } from './primitive/index.js'; interface TextFieldRedeclaredRACProps { + /** Initial value (uncontrolled). */ + defaultValue?: RacTextFieldProps['defaultValue']; /** Whether the field is disabled. */ isDisabled?: RacTextFieldProps['isDisabled']; - /** Whether the field is read-only. */ - isReadOnly?: RacTextFieldProps['isReadOnly']; /** Whether the field has a validation error. */ isInvalid?: RacTextFieldProps['isInvalid']; - /** Controlled input value. */ - value?: RacTextFieldProps['value']; - /** Initial value (uncontrolled). */ - defaultValue?: RacTextFieldProps['defaultValue']; + /** Whether the field is read-only. */ + isReadOnly?: RacTextFieldProps['isReadOnly']; /** Called when the value changes. */ onChange?: RacTextFieldProps['onChange']; + /** Controlled input value. */ + value?: RacTextFieldProps['value']; } /** diff --git a/packages/@luke-ui/react/src/text/index.tsx b/packages/@luke-ui/react/src/text/index.tsx index 1b0c1f4f..e5c29ed1 100644 --- a/packages/@luke-ui/react/src/text/index.tsx +++ b/packages/@luke-ui/react/src/text/index.tsx @@ -25,21 +25,28 @@ interface TextStyleProps { * @default 'regular' */ fontWeight?: TextVariantProps['fontWeight']; + /** + * Hides text visually while keeping it accessible. + * @default false + */ + isVisuallyHidden?: TextVariantProps['isVisuallyHidden']; + /** Clamps text lines. `true` clamps to 1 line; numeric values clamp to 1–5. */ + lineClamp?: TextVariantProps['lineClamp']; /** * Sets the line height. * @default 'loose' */ lineHeight?: TextVariantProps['lineHeight']; /** - * Makes text inherit font styles. + * Turns off cap-height trim. * @default false */ - shouldInheritFont?: TextVariantProps['shouldInheritFont']; + shouldDisableTrim?: TextVariantProps['shouldDisableTrim']; /** - * Turns off cap-height trim. + * Makes text inherit font styles. * @default false */ - shouldDisableTrim?: TextVariantProps['shouldDisableTrim']; + shouldInheritFont?: TextVariantProps['shouldInheritFont']; /** * Sets text alignment. * @default 'start' @@ -55,23 +62,16 @@ interface TextStyleProps { * @default 'none' */ textTransform?: TextVariantProps['textTransform']; - /** Clamps text lines. `true` clamps to 1 line; numeric values clamp to 1–5. */ - lineClamp?: TextVariantProps['lineClamp']; /** - * Sets numeric glyph style. + * Sets text wrapping behavior. * @default 'unset' */ - variant?: TextVariantProps['variant']; - /** - * Hides text visually while keeping it accessible. - * @default false - */ - isVisuallyHidden?: TextVariantProps['isVisuallyHidden']; + textWrap?: TextVariantProps['textWrap']; /** - * Sets text wrapping behavior. + * Sets numeric glyph style. * @default 'unset' */ - textWrap?: TextVariantProps['textWrap']; + variant?: TextVariantProps['variant']; } /** @@ -115,16 +115,16 @@ export function Text(props: TextProps) { fontFamily, fontSize, fontWeight, + isVisuallyHidden, + lineClamp, lineHeight, - shouldInheritFont, shouldDisableTrim: resolvedShouldDisableTrim, + shouldInheritFont, textAlign, textDecoration, textTransform, textWrap, - lineClamp, variant, - isVisuallyHidden, }), className, )} diff --git a/packages/@luke-ui/react/src/text/text.stories.tsx b/packages/@luke-ui/react/src/text/text.stories.tsx index b1df7444..53c1aaa7 100644 --- a/packages/@luke-ui/react/src/text/text.stories.tsx +++ b/packages/@luke-ui/react/src/text/text.stories.tsx @@ -2,9 +2,11 @@ import type { TextProps } from '@luke-ui/react/text'; import { Text } from '@luke-ui/react/text'; import { vars } from '@luke-ui/react/theme'; import { tokenKeys, tokens } from '@luke-ui/react/tokens'; +import { mergeProps } from '@luke-ui/react/utils'; import type { CSSProperties } from 'react'; import { expect } from 'storybook/test'; import preview from '../../.storybook/preview.js'; +import { createSprinkles } from '../styles/index.js'; const meta = preview.meta({ component: Text, @@ -29,6 +31,53 @@ const stackContainerStyle = { const storyText = 'The quick brown fox jumps over the lazy dog.'; const loremIpsum = 'Lorem ipsum Lolor sit amet, Lonsectetur adipiscing elit. Duis eu ornare nisi, sed feugiat metus. Pellentesque rutrum vel metus non dignissim. Aenean egestas neque mattis mi maximus luctus. Praesent et commodo dui, nec eleifend lectus. Pellentesque blandit nisi tellus, id efficitur urna consectetur id. Sed convallis tempor dui vel aliquet.'; +const importantIdentifier = 'budget-allocation-review-cycle-2026-06-05-eu-west-1-BA-109284'; +const importantIdentifierSuffix = '-BA-109284'; + +type MiddleTruncatedIdentifierProps = Omit & { + identifier: string; + suffix: string; +}; + +function MiddleTruncatedIdentifier(props: MiddleTruncatedIdentifierProps) { + const { identifier, suffix, ...textProps } = props; + const prefix = identifier.slice(0, -suffix.length); + const row = createSprinkles({ + alignItems: 'baseline', + display: 'flex', + inlineSize: '100%', + minInlineSize: '0', + }); + const prefixStyle = createSprinkles({ + flexBasis: 'auto', + flexGrow: '1', + flexShrink: '1', + minInlineSize: '0', + textOverflow: 'clip', + }); + const fixed = createSprinkles({ flexBasis: 'auto', flexGrow: '0', flexShrink: '0' }); + + return ( + + + {prefix} + + + ... + + + {suffix} + + + ); +} const baseArgs = { children: storyText, @@ -108,7 +157,7 @@ export const FontWeight = meta.story({ render: (props) => (
    {fontWeights.map((fontWeight) => ( - + {fontWeight}: {storyText} ))} @@ -123,7 +172,7 @@ export const FontFamily = meta.story({ render: (props) => (
    {fontFamilies.map((fontFamily) => ( - + {fontFamily}: {storyText} ))} @@ -153,7 +202,7 @@ export const Color = meta.story({ render: (props) => (
    {colors.map((color) => ( - + {color}: {storyText} ))} @@ -214,20 +263,65 @@ export const Align = meta.story({ * Truncate limits text to one line with an ellipsis. */ export const Truncate = meta.story({ - render: (props) => ( -
    - - {loremIpsum} - -
    - ), + render: (props) => { + const container = createSprinkles({ + display: 'flex', + flexDirection: 'column', + gap: 'medium', + inlineSize: '100%', + maxInlineSize: '20rem', + minInlineSize: '0', + }); + return ( +
    + + {loremIpsum} + +
    + ); + }, +}); + +/** + * Middle truncation keeps an important suffix visible by truncating only the prefix. + * + * This story dog-foods the public `@luke-ui/react/styles` `createSprinkles()` utility to compose + * layout, flex-item, sizing, and text-overflow utilities into a reusable truncation + * pattern that component props do not cover. + */ +export const MiddleTruncation = meta.story({ + play: async ({ canvas }) => { + await expect(canvas.getByText(importantIdentifierSuffix)).toBeInTheDocument(); + }, + render: (props) => { + const container = createSprinkles({ + display: 'flex', + flexDirection: 'column', + gap: 'medium', + inlineSize: '100%', + maxInlineSize: '20rem', + minInlineSize: '0', + }); + return ( +
    + + End truncation + + + {importantIdentifier} + + + + Middle truncation + + +
    + ); + }, }); /** @@ -235,22 +329,25 @@ export const Truncate = meta.story({ * Covers values `false`, `true`, and `1`–`5`. */ export const LineClamp = meta.story({ - render: (props) => ( -
    - {lineClampOptions.map((value) => ( - - {String(value)}: {loremIpsum} - - ))} -
    - ), + render: (props) => { + const container = createSprinkles({ + display: 'flex', + flexDirection: 'column', + gap: 'medium', + inlineSize: '100%', + maxInlineSize: '20rem', + minInlineSize: '0', + }); + return ( +
    + {lineClampOptions.map((value) => ( + + {String(value)}: {loremIpsum} + + ))} +
    + ); + }, }); /** diff --git a/packages/@luke-ui/react/src/tokens/tokens-vars.stories.tsx b/packages/@luke-ui/react/src/tokens/tokens-vars.stories.tsx index e4cda43c..c1994995 100644 --- a/packages/@luke-ui/react/src/tokens/tokens-vars.stories.tsx +++ b/packages/@luke-ui/react/src/tokens/tokens-vars.stories.tsx @@ -140,9 +140,9 @@ const sectionStyle = css({ const tableWrapStyle = css({ borderColor: vars.border.default, + borderRadius: vars.borderRadius.medium, borderStyle: 'solid', borderWidth: 1, - borderRadius: vars.borderRadius.medium, inlineSize: '100%', maxInlineSize: '100%', overflow: 'auto', @@ -205,9 +205,9 @@ const descriptionStyle = css({ const previewSwatchStyle = css({ blockSize: '1.5rem', borderColor: vars.border.default, + borderRadius: vars.borderRadius.small, borderStyle: 'solid', borderWidth: 1, - borderRadius: vars.borderRadius.small, display: 'inline-block', inlineSize: '4rem', }); @@ -235,9 +235,9 @@ const mutedPreviewStyle = css({ const docsPageStyle = css({ backgroundColor: vars.backgroundColor.default, borderColor: vars.border.default, + borderRadius: vars.borderRadius.large, borderStyle: 'solid', borderWidth: 1, - borderRadius: vars.borderRadius.large, display: 'flex', flexDirection: 'column', gap: vars.space.large, @@ -261,9 +261,9 @@ const tokenSectionStyle = css({ const tokenTableWrapStyle = css({ backgroundColor: vars.backgroundColor.default, borderColor: vars.border.default, + borderRadius: vars.borderRadius.medium, borderStyle: 'solid', borderWidth: 1, - borderRadius: vars.borderRadius.medium, overflow: 'auto', }); @@ -330,9 +330,9 @@ const tokenValueStyle = css({ const exampleBarStyle = css({ blockSize: '2.75rem', borderColor: vars.border.default, + borderRadius: vars.borderRadius.small, borderStyle: 'solid', borderWidth: 1, - borderRadius: vars.borderRadius.small, inlineSize: '14rem', maxInlineSize: '100%', }); @@ -355,9 +355,9 @@ const motionRailStyle = css({ backgroundColor: vars.backgroundColor.inputDisabled, blockSize: '1.25rem', borderColor: vars.border.default, + borderRadius: vars.borderRadius.full, borderStyle: 'solid', borderWidth: 1, - borderRadius: vars.borderRadius.full, inlineSize: '100%', position: 'relative', }); @@ -680,8 +680,8 @@ function RenderExamplePreview({ mode, row }: { mode: ExampleMode; row: ExampleRo
    @@ -874,7 +874,7 @@ function TokenTypeStory({ {title} - + {description} diff --git a/packages/@luke-ui/react/src/utils/index.ts b/packages/@luke-ui/react/src/utils/index.ts index aa0837fe..429f16b5 100644 --- a/packages/@luke-ui/react/src/utils/index.ts +++ b/packages/@luke-ui/react/src/utils/index.ts @@ -9,6 +9,51 @@ export function cx(...parts: Array): string { return result; } +type Merged = { + [K in keyof A | keyof B]: K extends 'className' + ? string + : K extends 'style' + ? Record + : K extends keyof B + ? B[K] + : K extends keyof A + ? A[K] + : never; +}; + +type MergeableProps = { + className?: unknown; + style?: unknown; +}; + +/** + * Merges two prop objects, concatenating `className` with `cx` and shallowly + * merging `style` objects (later props win). All other properties are overwritten + * by the later object. Useful for combining component props with `createSprinkles()` output. + */ +export function mergeProps(a: A, b: B): Merged { + const result = { ...a } as Record; + const aProps = a as MergeableProps; + const bProps = b as MergeableProps; + + result.className = cx( + typeof aProps.className === 'string' && aProps.className, + typeof bProps.className === 'string' && bProps.className, + ); + result.style = { + ...(typeof aProps.style === 'object' && aProps.style !== null ? aProps.style : {}), + ...(typeof bProps.style === 'object' && bProps.style !== null ? bProps.style : {}), + }; + + for (const key in b) { + if (key !== 'className' && key !== 'style') { + result[key] = b[key as keyof B]; + } + } + + return result as Merged; +} + /** Converts a pixel value to rem. */ export function pxToRem(px: number, base: number = 16): string { return `${px / base}rem`; diff --git a/packages/@luke-ui/react/tsconfig.json b/packages/@luke-ui/react/tsconfig.json index 6aa091bf..c7ad23bb 100644 --- a/packages/@luke-ui/react/tsconfig.json +++ b/packages/@luke-ui/react/tsconfig.json @@ -18,12 +18,12 @@ "types": [], "verbatimModuleSyntax": true }, + "exclude": ["src/**/*.stories.tsx", ".storybook"], "include": [ "src/**/*.ts", "src/**/*.tsx", "tsdown.config.ts", "vitest.config.ts", "vitest.shims.d.ts" - ], - "exclude": ["src/**/*.stories.tsx", ".storybook"] + ] } diff --git a/packages/@luke-ui/react/tsconfig.storybook.json b/packages/@luke-ui/react/tsconfig.storybook.json index 044d9d3b..9e00442b 100644 --- a/packages/@luke-ui/react/tsconfig.storybook.json +++ b/packages/@luke-ui/react/tsconfig.storybook.json @@ -1,5 +1,5 @@ { + "exclude": [], "extends": "./tsconfig.json", - "include": ["src/**/*.stories.tsx", ".storybook/**/*.ts", ".storybook/**/*.tsx"], - "exclude": [] + "include": ["src/**/*.stories.tsx", ".storybook/**/*.ts", ".storybook/**/*.tsx"] } diff --git a/packages/@luke-ui/react/tsdown.config.ts b/packages/@luke-ui/react/tsdown.config.ts index c3776450..55893c66 100644 --- a/packages/@luke-ui/react/tsdown.config.ts +++ b/packages/@luke-ui/react/tsdown.config.ts @@ -34,24 +34,24 @@ async function cleanDistExceptPreservedFiles() { export default defineConfig((options) => ({ attw: { - profile: 'esm-only', // Exclude static asset exports — attw checks JS type resolution, but CSS/SVG // files don't need type definitions. Excluding them avoids false-positive // 'no-resolution' errors while keeping the rule active for actual JS exports. excludeEntrypoints: ['./stylesheet.css', './spritesheet.svg'], + profile: 'esm-only', }, + clean: false, deps: { neverBundle: Object.keys(packageJson.peerDependencies), }, - clean: false, dts: true, entry: { '*': ['src/*/index.tsx', 'src/*/index.ts', 'src/*/primitive/index.tsx'], }, exports: { customExports: { - './stylesheet.css': './dist/stylesheet.css', './spritesheet.svg': './dist/spritesheet.svg', + './stylesheet.css': './dist/stylesheet.css', }, }, format: ['esm'], diff --git a/packages/@luke-ui/react/vitest.config.ts b/packages/@luke-ui/react/vitest.config.ts index 0a644ec0..5263a3de 100644 --- a/packages/@luke-ui/react/vitest.config.ts +++ b/packages/@luke-ui/react/vitest.config.ts @@ -23,9 +23,9 @@ export default defineConfig({ // Runs tests for stories defined in Storybook config. storybookTest({ configDir }), argosVitestPlugin({ - uploadToArgos: process.env.ARGOS_UPLOAD === '1', - token: process.env.ARGOS_TOKEN, buildName: 'storybook', + token: process.env.ARGOS_TOKEN, + uploadToArgos: process.env.ARGOS_UPLOAD === '1', }), ], test: { diff --git a/packages/turbo-generators/config.ts b/packages/turbo-generators/config.ts index adf00162..49d62400 100644 --- a/packages/turbo-generators/config.ts +++ b/packages/turbo-generators/config.ts @@ -118,7 +118,7 @@ function addComponentToDocsMeta(answers: GeneratorAnswers): string { const content = readFileSync(groupMetaPath, 'utf8'); meta = JSON.parse(content) as { pages?: Array; title?: string }; } else { - meta = { title: toDisplayName(group), pages: [] }; + meta = { pages: [], title: toDisplayName(group) }; } const pages = Array.isArray(meta.pages) ? meta.pages : []; @@ -297,46 +297,31 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void { }); plop.setGenerator('component', { - description: 'Scaffold a new component in @luke-ui/react', - prompts: [ - { - type: 'list', - name: 'group', - message: 'Component group:', - choices: [...DOCS_GROUPS], - }, - { - type: 'input', - name: 'name', - message: 'Component name (PascalCase or kebab-case):', - validate: validateComponentName, - }, - ], actions: [ { - type: 'add', path: '{{ turbo.paths.root }}/packages/@luke-ui/react/src/{{kebabCase name}}/index.tsx', templateFile: 'templates/component/component.tsx.hbs', + type: 'add', }, { - type: 'add', path: '{{ turbo.paths.root }}/packages/@luke-ui/react/src/recipes/{{kebabCase name}}.css.ts', templateFile: 'templates/component/component.recipe.css.ts.hbs', + type: 'add', }, { - type: 'add', path: '{{ turbo.paths.root }}/packages/@luke-ui/react/src/{{kebabCase name}}.stories.tsx', templateFile: 'templates/component/component.stories.tsx.hbs', + type: 'add', }, { - type: 'add', path: '{{ turbo.paths.root }}/packages/@luke-ui/react/src/{{kebabCase name}}.docs.mdx', templateFile: 'templates/component/component.docs.mdx.hbs', + type: 'add', }, { - type: 'add', path: '{{ turbo.paths.root }}/apps/docs/content/docs/components/{{kebabCase group}}/{{kebabCase name}}.mdx', templateFile: 'templates/component/component.docs-page.mdx.hbs', + type: 'add', }, addComponentToDocsMeta, addComponentToDocsIndex, @@ -348,5 +333,20 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void { return 'Done. @luke-ui/react exports were refreshed via tsdown.'; }, ], + description: 'Scaffold a new component in @luke-ui/react', + prompts: [ + { + choices: [...DOCS_GROUPS], + message: 'Component group:', + name: 'group', + type: 'list', + }, + { + message: 'Component name (PascalCase or kebab-case):', + name: 'name', + type: 'input', + validate: validateComponentName, + }, + ], }); } diff --git a/packages/turbo-generators/scripts/generate-component.ts b/packages/turbo-generators/scripts/generate-component.ts index 6d5e02f6..aa0470cf 100644 --- a/packages/turbo-generators/scripts/generate-component.ts +++ b/packages/turbo-generators/scripts/generate-component.ts @@ -24,7 +24,7 @@ const child = spawn( 'packages/turbo-generators/config.ts', ...process.argv.slice(2), ], - { env, stdio: 'inherit', cwd: root }, + { cwd: root, env, stdio: 'inherit' }, ); child.on('error', () => process.exit(1)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1145975f..0c546b52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,21 @@ settings: catalogs: default: + '@argos-ci/storybook': + specifier: ^5.3.2 + version: 5.3.2 + '@storybook/addon-a11y': + specifier: ^10.3.5 + version: 10.3.5 + '@storybook/addon-docs': + specifier: ^10.3.5 + version: 10.3.5 + '@storybook/addon-vitest': + specifier: ^10.3.5 + version: 10.3.5 + '@storybook/react-vite': + specifier: ^10.3.5 + version: 10.3.5 '@types/node': specifier: ^25.6.0 version: 25.6.0 @@ -15,21 +30,51 @@ catalogs: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3 + '@vanilla-extract/vite-plugin': + specifier: ^5.2.2 + version: 5.2.2 '@vitejs/plugin-react': specifier: ^6.0.1 version: 6.0.1 + '@vitest/browser-playwright': + specifier: ^4.1.5 + version: 4.1.5 + '@vitest/coverage-v8': + specifier: ^4.1.5 + version: 4.1.5 + '@vitest/ui': + specifier: ^4.1.5 + version: 4.1.5 fumadocs-mdx: specifier: 14.2.10 version: 14.2.10 + oxlint-tsgolint: + specifier: ^0.22.0 + version: 0.22.0 react: specifier: ^19.2.5 version: 19.2.5 + react-aria-components: + specifier: ^1.17.0 + version: 1.17.0 react-dom: specifier: ^19.2.5 version: 19.2.5 + storybook: + specifier: ^10.3.5 + version: 10.3.5 + ts-morph: + specifier: ^28.0.0 + version: 28.0.0 + tsx: + specifier: ^4.21.0 + version: 4.21.0 vite: specifier: ^8.0.10 version: 8.0.10 + vitest: + specifier: ^4.1.5 + version: 4.1.5 overrides: undici: ^7.24.3 @@ -164,6 +209,25 @@ importers: specifier: 'catalog:' version: 4.1.5(@types/node@25.6.0)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + packages/@luke-ui/rainbow-sprinkles: + dependencies: + '@vanilla-extract/css': + specifier: ^1.20.1 + version: 1.20.1 + '@vanilla-extract/dynamic': + specifier: ^2.1.2 + version: 2.1.5 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 25.6.0 + csstype: + specifier: ^3.2.3 + version: 3.2.3 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + packages/@luke-ui/react: dependencies: '@capsizecss/metrics': @@ -172,6 +236,9 @@ importers: '@capsizecss/vanilla-extract': specifier: ^2.0.4 version: 2.0.4(@vanilla-extract/css@1.20.1) + '@luke-ui/rainbow-sprinkles': + specifier: workspace:* + version: link:../rainbow-sprinkles '@react-aria/utils': specifier: ^3.34.0 version: 3.34.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -3396,6 +3463,9 @@ packages: '@vanilla-extract/css@1.20.1': resolution: {integrity: sha512-5I9RNo5uZW9tsBnqrWzJqELegOqTHBrZyDFnES0gR9gJJHBB9dom1N0bwITM9tKwBcfKrTX4a6DHVeQdJ2ubQA==} + '@vanilla-extract/dynamic@2.1.5': + resolution: {integrity: sha512-QGIFGb1qyXQkbzx6X6i3+3LMc/iv/ZMBttMBL+Wm/DetQd36KsKsFg5CtH3qy+1hCA/5w93mEIIAiL4fkM8ycw==} + '@vanilla-extract/integration@8.0.10': resolution: {integrity: sha512-01IB5gbrgTe8IIrtfRXXTmACl5D8Enzqp2cKbCWaMKXmnoilXXVCPbJoA96q88PXkNDXsXepCxUugMvEmL3c7A==} @@ -4394,9 +4464,6 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-tsconfig@4.13.6: - resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} - get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} @@ -5351,10 +5418,6 @@ packages: resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} - engines: {node: ^10 || ^12 || >=14} - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -9059,6 +9122,10 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros + '@vanilla-extract/dynamic@2.1.5': + dependencies: + '@vanilla-extract/private': 1.0.9 + '@vanilla-extract/integration@8.0.10': dependencies: '@babel/core': 7.29.0 @@ -10160,10 +10227,6 @@ snapshots: get-stream@6.0.1: {} - get-tsconfig@4.13.6: - dependencies: - resolve-pkg-maps: 1.0.0 - get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -11507,12 +11570,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.8: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - prelude-ls@1.2.1: {} prettier@2.8.8: {} @@ -12189,7 +12246,7 @@ snapshots: tsx@4.21.0: dependencies: esbuild: 0.27.3 - get-tsconfig: 4.13.6 + get-tsconfig: 4.14.0 optionalDependencies: fsevents: 2.3.3 @@ -12372,7 +12429,7 @@ snapshots: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.8 + postcss: 8.5.10 rollup: 4.59.0 tinyglobby: 0.2.16 optionalDependencies: diff --git a/turbo.json b/turbo.json index 0b2ce378..86f1e2f7 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,9 @@ { "$schema": "https://v2-8-10.turborepo.dev/schema.json", "tasks": { + "@luke-ui/react#check:types": { + "dependsOn": ["build", "^build"] + }, "//#check:format-root": { "cache": true }, @@ -15,9 +18,6 @@ "cache": false, "dependsOn": ["@luke-ui/react#generate", "docs#generate"] }, - "@luke-ui/react#check:types": { - "dependsOn": ["build", "^build"] - }, "build": { "dependsOn": ["^build", "generate"], "env": ["VITE_BASE_URL"], @@ -47,26 +47,6 @@ "clean": { "cache": false }, - "generate": { - "dependsOn": ["^generate"], - "inputs": [ - "$TURBO_DEFAULT$", - "!docs/**", - "!dist/**", - "!.generated/**", - "!storybook-static/**", - "!**/*.test.ts", - "!**/*.test.tsx", - "!**/*.stories.tsx" - ], - "outputs": [ - ".generated/**", - "dist/spritesheet.svg", - "dist/docs/**", - "src/routeTree.gen.ts", - "docs/**" - ] - }, "dev": { "cache": false, "dependsOn": ["generate", "^build"], @@ -93,6 +73,26 @@ "cache": false, "dependsOn": ["generate"] }, + "generate": { + "dependsOn": ["^generate"], + "inputs": [ + "$TURBO_DEFAULT$", + "!docs/**", + "!dist/**", + "!.generated/**", + "!storybook-static/**", + "!**/*.test.ts", + "!**/*.test.tsx", + "!**/*.stories.tsx" + ], + "outputs": [ + ".generated/**", + "dist/spritesheet.svg", + "dist/docs/**", + "src/routeTree.gen.ts", + "docs/**" + ] + }, "test": { "dependsOn": ["^build", "generate"], "outputs": []