diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index 65999d7e..021ae6b0 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -87,12 +87,38 @@ jobs: - name: Build Storybook run: corepack pnpm build:storybook env: - STORYBOOK_BASE_PATH: ${{ steps.pages.outputs.base_path }} + STORYBOOK_BASE_PATH: ${{ steps.pages.outputs.base_path }}/storybook + + - name: Build docs + run: corepack pnpm build:docs + env: + VITE_BASE_URL: ${{ steps.pages.outputs.base_path }}/ + + - name: Assemble Pages artifact + run: | + BASE_PATH="${{ steps.pages.outputs.base_path }}" + # Strip the leading slash to get the subdirectory name (e.g. /luke-ui → luke-ui). + # The docs prerendering outputs HTML under .output/public// when basePath + # is set, so we copy from there and let GitHub Pages handle the /luke-ui/ prefix. + STRIPPED="${BASE_PATH#/}" + mkdir -p dist + if [ -n "$STRIPPED" ] && [ -d "apps/docs/.output/public/${STRIPPED}" ]; then + cp -r "apps/docs/.output/public/${STRIPPED}/." dist/ + else + # Fallback for root deployments (user/org Pages or custom domain) + cp -r apps/docs/.output/public/. dist/ + fi + # Assets are output at .output/public/assets/ regardless of basePath + [ -d apps/docs/.output/public/assets ] && cp -r apps/docs/.output/public/assets dist/ + [ -d apps/docs/.output/public/__tsr ] && cp -r apps/docs/.output/public/__tsr dist/ + [ -d apps/docs/.output/public/api ] && cp -r apps/docs/.output/public/api dist/ + # Storybook lives at /storybook/ within the Pages site + cp -r packages/@luke-ui/react/storybook-static dist/storybook - name: Upload Pages artifact uses: actions/upload-pages-artifact@v4 with: - path: packages/@luke-ui/react/storybook-static + path: dist - name: Deploy to GitHub Pages id: deployment diff --git a/.gitignore b/.gitignore index 3c5f3099..86bd553f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,15 +5,16 @@ .turbo/ *.local *.log +apps/docs/.story/ apps/docs/src/routeTree.gen.ts dist/ node_modules/ +packages/@luke-ui/react/.generated/ packages/@luke-ui/react/public/icons/ +packages/@luke-ui/react/screenshots/ packages/@luke-ui/react/spritesheet.svg packages/@luke-ui/react/src/generated/color-tokens.generated.ts packages/@luke-ui/react/src/generated/icons.tsx -packages/@luke-ui/react/.generated/ public/dist/ server/dist/ storybook-static/ -packages/@luke-ui/react/screenshots/ diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000..6b76481e --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,41 @@ +# Luke UI + +A React design system built on `react-aria-components` and `vanilla-extract`. Components fall into three vocabulary tiers that drive both how code is laid out and what gets documented. + +## Language + +**Atom**: +A component that presents as a single conceptual unit — one piece of text, one icon, one number. May compose `Text` or other atoms internally, but the consumer treats it as indivisible. App devs use it directly. +_Examples_: `Text`, `Link`, `Icon`, `LoadingSpinner`, `Heading`, `Emoji`, `Numeral`. +_Avoid_: calling these "primitives" in source comments — that name is reserved. + +**Composed**: +A component that combines two or more atoms or primitives into an opinionated, ready-to-drop-in unit aimed at app devs. +_Examples_: `Button`, `IconButton`, `CloseButton`, `TextField`, `ComboboxField`. + +**Primitive**: +A building block whose audience is library authors assembling the next composed component, not app devs. May be a single file (e.g. `text-field/primitive`) or a kit of parts (e.g. `combobox-field/primitive`, `field/primitive`). +_Examples_: `TextInput` (via `text-field/primitive`), the `Combobox*` kit (via `combobox-field/primitive`), `button/primitive`, `field/primitive`, the composed-but-internal `Field`. +_Avoid_: "base", "raw" — use **Primitive**. + +## Relationships + +- A **Composed** component is built from one or more **Atoms** and/or **Primitives**. +- A **Primitive** is never documented publicly; its contract lives in source. +- An **Atom** and a **Composed** component each get a public doc page. +- The composed `Field` is a **Primitive** by audience even though it composes other components — it exists to be wrapped by `*Field` components, not used directly by app devs. + +## Docs rule + +> Document what an app developer drops into their UI. Don't document what only exists to build the next component. + +| Tier | Docs page? | +| --------- | ---------- | +| Atom | yes | +| Composed | yes | +| Primitive | no | + +## Decisions + +- [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` diff --git a/apps/docs/content/docs/components/actions/button.mdx b/apps/docs/content/docs/components/actions/button.mdx index 126929b2..ac32be27 100644 --- a/apps/docs/content/docs/components/actions/button.mdx +++ b/apps/docs/content/docs/components/actions/button.mdx @@ -3,7 +3,10 @@ title: Button description: Button component with size and tone variants. --- -import ButtonDocs from '../../../../../../packages/@luke-ui/react/src/button.docs.mdx'; +import { storyClient } from '../../../../src/button/button.story.client'; +import ButtonDocs from '../../../../../../packages/@luke-ui/react/src/button/button.docs.mdx'; import defaultMdxComponents from 'fumadocs-ui/mdx'; + + diff --git a/apps/docs/content/docs/components/actions/close-button.mdx b/apps/docs/content/docs/components/actions/close-button.mdx new file mode 100644 index 00000000..78f6554f --- /dev/null +++ b/apps/docs/content/docs/components/actions/close-button.mdx @@ -0,0 +1,12 @@ +--- +title: Close Button +description: Icon button preset for close and dismiss actions. +--- + +import { storyClient } from '../../../../src/close-button/close-button.story.client'; +import CloseButtonDocs from '../../../../../../packages/@luke-ui/react/src/close-button/close-button.docs.mdx'; +import defaultMdxComponents from 'fumadocs-ui/mdx'; + + + + diff --git a/apps/docs/content/docs/components/actions/icon-button.mdx b/apps/docs/content/docs/components/actions/icon-button.mdx new file mode 100644 index 00000000..ccc6ed79 --- /dev/null +++ b/apps/docs/content/docs/components/actions/icon-button.mdx @@ -0,0 +1,12 @@ +--- +title: Icon Button +description: Button that renders only an icon. +--- + +import { storyClient } from '../../../../src/icon-button/icon-button.story.client'; +import IconButtonDocs from '../../../../../../packages/@luke-ui/react/src/icon-button/icon-button.docs.mdx'; +import defaultMdxComponents from 'fumadocs-ui/mdx'; + + + + diff --git a/apps/docs/content/docs/components/actions/link.mdx b/apps/docs/content/docs/components/actions/link.mdx index d9412755..855ecd7e 100644 --- a/apps/docs/content/docs/components/actions/link.mdx +++ b/apps/docs/content/docs/components/actions/link.mdx @@ -3,7 +3,10 @@ title: Link description: Link component for inline and standalone navigation with tone variants. --- -import LinkDocs from '../../../../../../packages/@luke-ui/react/src/link.docs.mdx'; +import { storyClient } from '../../../../src/link/link.story.client'; +import LinkDocs from '../../../../../../packages/@luke-ui/react/src/link/link.docs.mdx'; import defaultMdxComponents from 'fumadocs-ui/mdx'; + + diff --git a/apps/docs/content/docs/components/actions/meta.json b/apps/docs/content/docs/components/actions/meta.json index f6eb0cb5..86f9a753 100644 --- a/apps/docs/content/docs/components/actions/meta.json +++ b/apps/docs/content/docs/components/actions/meta.json @@ -1 +1,4 @@ -{ "title": "Actions", "pages": ["button", "link"] } +{ + "title": "Actions", + "pages": ["button", "close-button", "icon-button", "link"] +} diff --git a/apps/docs/content/docs/components/feedback/loading-spinner.mdx b/apps/docs/content/docs/components/feedback/loading-spinner.mdx index ae47c6a0..cea337dd 100644 --- a/apps/docs/content/docs/components/feedback/loading-spinner.mdx +++ b/apps/docs/content/docs/components/feedback/loading-spinner.mdx @@ -3,7 +3,10 @@ title: Loading Spinner description: Spinner component for indeterminate and determinate progress. --- -import LoadingSpinnerDocs from '../../../../../../packages/@luke-ui/react/src/loading-spinner.docs.mdx'; +import { storyClient } from '../../../../src/loading-spinner/loading-spinner.story.client'; +import LoadingSpinnerDocs from '../../../../../../packages/@luke-ui/react/src/loading-spinner/loading-spinner.docs.mdx'; import defaultMdxComponents from 'fumadocs-ui/mdx'; + + diff --git a/apps/docs/content/docs/components/forms/combobox-field.mdx b/apps/docs/content/docs/components/forms/combobox-field.mdx index 0af85a05..03e52a5b 100644 --- a/apps/docs/content/docs/components/forms/combobox-field.mdx +++ b/apps/docs/content/docs/components/forms/combobox-field.mdx @@ -3,7 +3,10 @@ title: Combobox Field description: Documentation for the Combobox Field component. --- -import ComboboxFieldDocs from '../../../../../../packages/@luke-ui/react/src/combobox-field.docs.mdx'; +import { storyClient } from '../../../../src/combobox-field/combobox-field.story.client'; +import ComboboxFieldDocs from '../../../../../../packages/@luke-ui/react/src/combobox-field/combobox-field.docs.mdx'; import defaultMdxComponents from 'fumadocs-ui/mdx'; + + diff --git a/apps/docs/content/docs/components/forms/combobox-input.mdx b/apps/docs/content/docs/components/forms/combobox-input.mdx deleted file mode 100644 index 018057d7..00000000 --- a/apps/docs/content/docs/components/forms/combobox-input.mdx +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Combobox Input -description: Documentation for the Combobox Input component. ---- - -import ComboboxDocs from '../../../../../../packages/@luke-ui/react/src/combobox.docs.mdx'; -import defaultMdxComponents from 'fumadocs-ui/mdx'; - - diff --git a/apps/docs/content/docs/components/forms/meta.json b/apps/docs/content/docs/components/forms/meta.json index e77571ea..6c649e5e 100644 --- a/apps/docs/content/docs/components/forms/meta.json +++ b/apps/docs/content/docs/components/forms/meta.json @@ -1,4 +1,4 @@ { "title": "Forms", - "pages": ["picker-field", "picker-input", "select-field", "select-input"] + "pages": ["combobox-field", "text-field"] } diff --git a/apps/docs/content/docs/components/forms/text-field.mdx b/apps/docs/content/docs/components/forms/text-field.mdx new file mode 100644 index 00000000..23cd7f47 --- /dev/null +++ b/apps/docs/content/docs/components/forms/text-field.mdx @@ -0,0 +1,12 @@ +--- +title: Text Field +description: Composed single-line text input with label and validation. +--- + +import { storyClient } from '../../../../src/text-field/text-field.story.client'; +import TextFieldDocs from '../../../../../../packages/@luke-ui/react/src/text-field/text-field.docs.mdx'; +import defaultMdxComponents from 'fumadocs-ui/mdx'; + + + + diff --git a/apps/docs/content/docs/components/typography/emoji.mdx b/apps/docs/content/docs/components/typography/emoji.mdx new file mode 100644 index 00000000..838f2bd5 --- /dev/null +++ b/apps/docs/content/docs/components/typography/emoji.mdx @@ -0,0 +1,12 @@ +--- +title: Emoji +description: Accessible emoji rendering with screen reader label. +--- + +import { storyClient } from '../../../../src/emoji/emoji.story.client'; +import EmojiDocs from '../../../../../../packages/@luke-ui/react/src/emoji/emoji.docs.mdx'; +import defaultMdxComponents from 'fumadocs-ui/mdx'; + + + + diff --git a/apps/docs/content/docs/components/typography/heading.mdx b/apps/docs/content/docs/components/typography/heading.mdx new file mode 100644 index 00000000..c47c9a44 --- /dev/null +++ b/apps/docs/content/docs/components/typography/heading.mdx @@ -0,0 +1,12 @@ +--- +title: Heading +description: Auto-leveling semantic heading with token-based typography. +--- + +import { storyClient } from '../../../../src/heading/heading.story.client'; +import HeadingDocs from '../../../../../../packages/@luke-ui/react/src/heading/heading.docs.mdx'; +import defaultMdxComponents from 'fumadocs-ui/mdx'; + + + + diff --git a/apps/docs/content/docs/components/typography/meta.json b/apps/docs/content/docs/components/typography/meta.json index a23e5561..e2df3706 100644 --- a/apps/docs/content/docs/components/typography/meta.json +++ b/apps/docs/content/docs/components/typography/meta.json @@ -1 +1,4 @@ -{ "title": "Typography", "pages": ["text"] } +{ + "title": "Typography", + "pages": ["emoji", "heading", "numeral", "text"] +} diff --git a/apps/docs/content/docs/components/typography/numeral.mdx b/apps/docs/content/docs/components/typography/numeral.mdx new file mode 100644 index 00000000..3f5a4351 --- /dev/null +++ b/apps/docs/content/docs/components/typography/numeral.mdx @@ -0,0 +1,12 @@ +--- +title: Numeral +description: Locale-aware number formatting with Intl.NumberFormat. +--- + +import { storyClient } from '../../../../src/numeral/numeral.story.client'; +import NumeralDocs from '../../../../../../packages/@luke-ui/react/src/numeral/numeral.docs.mdx'; +import defaultMdxComponents from 'fumadocs-ui/mdx'; + + + + diff --git a/apps/docs/content/docs/components/typography/text.mdx b/apps/docs/content/docs/components/typography/text.mdx index d3a7b297..d7465891 100644 --- a/apps/docs/content/docs/components/typography/text.mdx +++ b/apps/docs/content/docs/components/typography/text.mdx @@ -3,7 +3,10 @@ title: Text description: Typography component for rendering body copy and headings with token-driven sizing. --- -import TextDocs from '../../../../../../packages/@luke-ui/react/src/text.docs.mdx'; +import { storyClient } from '../../../../src/text/text.story.client'; +import TextDocs from '../../../../../../packages/@luke-ui/react/src/text/text.docs.mdx'; import defaultMdxComponents from 'fumadocs-ui/mdx'; + + diff --git a/apps/docs/content/docs/components/visuals/icon.mdx b/apps/docs/content/docs/components/visuals/icon.mdx index 66706563..d09650c9 100644 --- a/apps/docs/content/docs/components/visuals/icon.mdx +++ b/apps/docs/content/docs/components/visuals/icon.mdx @@ -3,7 +3,10 @@ title: Icon description: SVG icon component powered by the generated spritesheet. --- -import IconDocs from '../../../../../../packages/@luke-ui/react/src/icon.docs.mdx'; +import { storyClient } from '../../../../src/icon/icon.story.client'; +import IconDocs from '../../../../../../packages/@luke-ui/react/src/icon/icon.docs.mdx'; import defaultMdxComponents from 'fumadocs-ui/mdx'; + + diff --git a/apps/docs/content/docs/components/visuals/meta.json b/apps/docs/content/docs/components/visuals/meta.json index f6d26e5b..2fc15803 100644 --- a/apps/docs/content/docs/components/visuals/meta.json +++ b/apps/docs/content/docs/components/visuals/meta.json @@ -1 +1,4 @@ -{ "title": "Visuals", "pages": ["icon"] } +{ + "title": "Visuals", + "pages": ["icon"] +} diff --git a/apps/docs/content/docs/getting-started.mdx b/apps/docs/content/docs/getting-started.mdx index 6f6edd6a..7c59194a 100644 --- a/apps/docs/content/docs/getting-started.mdx +++ b/apps/docs/content/docs/getting-started.mdx @@ -64,6 +64,5 @@ import { themeRootClassName } from '@luke-ui/react/theme'; - Read the [Link](/docs/components/actions/link) docs. - Read the [Loading Spinner](/docs/components/feedback/loading-spinner) docs. - Read the [Combobox Field](/docs/components/forms/combobox-field) docs. -- Read the [Combobox Input](/docs/components/forms/combobox-input) docs. - Read the [Text](/docs/components/typography/text) docs. - Read the [Icon](/docs/components/visuals/icon) docs. diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx index 478cc885..67f22823 100644 --- a/apps/docs/content/docs/index.mdx +++ b/apps/docs/content/docs/index.mdx @@ -11,7 +11,6 @@ Use these docs to explore foundations, components, and usage patterns for `@luke - diff --git a/apps/docs/package.json b/apps/docs/package.json index bf17e43c..5d8d0319 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -21,6 +21,7 @@ "start": "serve .output/public" }, "dependencies": { + "@fumadocs/story": "0.0.14", "@luke-ui/react": "workspace:*", "@orama/orama": "^3.1.18", "@tanstack/react-router": "1.158.4", @@ -32,6 +33,7 @@ "lucide-react": "^1.11.0", "react": "catalog:", "react-dom": "catalog:", + "ts-morph": "^28.0.0", "vite": "catalog:" }, "devDependencies": { @@ -45,7 +47,6 @@ "nitro": "npm:nitro-nightly@3.0.1-20260128-211656-ae83c97e", "serve": "^14.2.6", "tailwindcss": "^4.2.4", - "typescript": "^6.0.3", - "vite-tsconfig-paths": "^6.1.1" + "typescript": "^6.0.3" } } diff --git a/apps/docs/src/button/button.story.client.tsx b/apps/docs/src/button/button.story.client.tsx new file mode 100644 index 00000000..86c3565b --- /dev/null +++ b/apps/docs/src/button/button.story.client.tsx @@ -0,0 +1,5 @@ +import { Button } from '@luke-ui/react/button'; +import { createWrappedStoryClient } from '../lib/story-wrapper'; +import type { story } from './button.story'; + +export const storyClient = createWrappedStoryClient(Button); diff --git a/apps/docs/src/button/button.story.tsx b/apps/docs/src/button/button.story.tsx new file mode 100644 index 00000000..b0a1a131 --- /dev/null +++ b/apps/docs/src/button/button.story.tsx @@ -0,0 +1,10 @@ +import type { Button } from '@luke-ui/react/button'; +import { defineComponentStory } from '../lib/define-component-story'; + +export const story = defineComponentStory( + new URL('./button.story.tsx', import.meta.url), + { + initial: { children: 'Button' }, + priorities: ['children', 'tone', 'size', 'isBlock', 'isDisabled', 'isPending'], + }, +); diff --git a/apps/docs/src/close-button/close-button.story.client.tsx b/apps/docs/src/close-button/close-button.story.client.tsx new file mode 100644 index 00000000..581c1766 --- /dev/null +++ b/apps/docs/src/close-button/close-button.story.client.tsx @@ -0,0 +1,5 @@ +import { CloseButton } from '@luke-ui/react/close-button'; +import { createWrappedStoryClient } from '../lib/story-wrapper'; +import type { story } from './close-button.story'; + +export const storyClient = createWrappedStoryClient(CloseButton); diff --git a/apps/docs/src/close-button/close-button.story.tsx b/apps/docs/src/close-button/close-button.story.tsx new file mode 100644 index 00000000..852537dc --- /dev/null +++ b/apps/docs/src/close-button/close-button.story.tsx @@ -0,0 +1,10 @@ +import type { CloseButton } from '@luke-ui/react/close-button'; +import { defineComponentStory } from '../lib/define-component-story'; + +export const story = defineComponentStory( + new URL('./close-button.story.tsx', import.meta.url), + { + initial: {}, + priorities: ['tone', 'size', 'isDisabled'], + }, +); diff --git a/apps/docs/src/combobox-field/combobox-field.story.client.tsx b/apps/docs/src/combobox-field/combobox-field.story.client.tsx new file mode 100644 index 00000000..3d8792fa --- /dev/null +++ b/apps/docs/src/combobox-field/combobox-field.story.client.tsx @@ -0,0 +1,23 @@ +import { ComboboxField } from '@luke-ui/react/combobox-field'; +import { ComboboxItem } from '@luke-ui/react/combobox-field/primitive'; +import { createWrappedStoryClient } from '../lib/story-wrapper'; +import type { ComboboxFieldStoryProps, story } from './combobox-field.story'; + +type Item = { id: string; label: string }; + +const items: Array = [ + { id: 'au', label: 'Australia' }, + { id: 'ca', label: 'Canada' }, + { id: 'nz', label: 'New Zealand' }, + { id: 'us', label: 'United States' }, +]; + +function ComboboxFieldStory(props: ComboboxFieldStoryProps) { + return ( + + {(item) => {item.label}} + + ); +} + +export const storyClient = createWrappedStoryClient(ComboboxFieldStory); diff --git a/apps/docs/src/combobox-field/combobox-field.story.tsx b/apps/docs/src/combobox-field/combobox-field.story.tsx new file mode 100644 index 00000000..13e7f30a --- /dev/null +++ b/apps/docs/src/combobox-field/combobox-field.story.tsx @@ -0,0 +1,16 @@ +import type { ComboboxFieldProps } from '@luke-ui/react/combobox-field'; +import type { FC } from 'react'; +import { defineComponentStory } from '../lib/define-component-story'; + +export type ComboboxFieldStoryProps = Pick< + ComboboxFieldProps, + 'label' | 'name' | 'placeholder' | 'description' | 'isRequired' | 'isDisabled' | 'size' +>; + +export const story = defineComponentStory>>( + new URL('./combobox-field.story.tsx', import.meta.url), + { + initial: { label: 'Country', name: 'country', placeholder: 'Select a country...' }, + priorities: ['name', 'label', 'placeholder', 'description', 'isRequired'], + }, +); diff --git a/apps/docs/src/emoji/emoji.story.client.tsx b/apps/docs/src/emoji/emoji.story.client.tsx new file mode 100644 index 00000000..e2168292 --- /dev/null +++ b/apps/docs/src/emoji/emoji.story.client.tsx @@ -0,0 +1,5 @@ +import { Emoji } from '@luke-ui/react/emoji'; +import { createWrappedStoryClient } from '../lib/story-wrapper'; +import type { story } from './emoji.story'; + +export const storyClient = createWrappedStoryClient(Emoji); diff --git a/apps/docs/src/emoji/emoji.story.tsx b/apps/docs/src/emoji/emoji.story.tsx new file mode 100644 index 00000000..916430fe --- /dev/null +++ b/apps/docs/src/emoji/emoji.story.tsx @@ -0,0 +1,10 @@ +import type { Emoji } from '@luke-ui/react/emoji'; +import { defineComponentStory } from '../lib/define-component-story'; + +export const story = defineComponentStory( + new URL('./emoji.story.tsx', import.meta.url), + { + initial: { emoji: '🎉', label: 'Celebration' }, + priorities: ['emoji', 'label'], + }, +); diff --git a/apps/docs/src/heading/heading.story.client.tsx b/apps/docs/src/heading/heading.story.client.tsx new file mode 100644 index 00000000..bc0afdef --- /dev/null +++ b/apps/docs/src/heading/heading.story.client.tsx @@ -0,0 +1,5 @@ +import { Heading } from '@luke-ui/react/heading'; +import { createWrappedStoryClient } from '../lib/story-wrapper'; +import type { story } from './heading.story'; + +export const storyClient = createWrappedStoryClient(Heading); diff --git a/apps/docs/src/heading/heading.story.tsx b/apps/docs/src/heading/heading.story.tsx new file mode 100644 index 00000000..4b1caf0a --- /dev/null +++ b/apps/docs/src/heading/heading.story.tsx @@ -0,0 +1,10 @@ +import type { Heading } from '@luke-ui/react/heading'; +import { defineComponentStory } from '../lib/define-component-story'; + +export const story = defineComponentStory( + new URL('./heading.story.tsx', import.meta.url), + { + initial: { children: 'Heading text' }, + priorities: ['children', 'level', 'color', 'fontWeight'], + }, +); diff --git a/apps/docs/src/icon-button/icon-button.story.client.tsx b/apps/docs/src/icon-button/icon-button.story.client.tsx new file mode 100644 index 00000000..5dfd532a --- /dev/null +++ b/apps/docs/src/icon-button/icon-button.story.client.tsx @@ -0,0 +1,5 @@ +import { IconButton } from '@luke-ui/react/icon-button'; +import { createWrappedStoryClient } from '../lib/story-wrapper'; +import type { story } from './icon-button.story'; + +export const storyClient = createWrappedStoryClient(IconButton); diff --git a/apps/docs/src/icon-button/icon-button.story.tsx b/apps/docs/src/icon-button/icon-button.story.tsx new file mode 100644 index 00000000..258eec59 --- /dev/null +++ b/apps/docs/src/icon-button/icon-button.story.tsx @@ -0,0 +1,10 @@ +import type { IconButton } from '@luke-ui/react/icon-button'; +import { defineComponentStory } from '../lib/define-component-story'; + +export const story = defineComponentStory( + new URL('./icon-button.story.tsx', import.meta.url), + { + initial: { icon: 'add', 'aria-label': 'Add' }, + priorities: ['icon', 'aria-label', 'tone', 'size', 'isDisabled'], + }, +); diff --git a/apps/docs/src/icon/icon.story.client.tsx b/apps/docs/src/icon/icon.story.client.tsx new file mode 100644 index 00000000..5f4a7c0d --- /dev/null +++ b/apps/docs/src/icon/icon.story.client.tsx @@ -0,0 +1,5 @@ +import { Icon } from '@luke-ui/react/icon'; +import { createWrappedStoryClient } from '../lib/story-wrapper'; +import type { story } from './icon.story'; + +export const storyClient = createWrappedStoryClient(Icon); diff --git a/apps/docs/src/icon/icon.story.tsx b/apps/docs/src/icon/icon.story.tsx new file mode 100644 index 00000000..561ccd09 --- /dev/null +++ b/apps/docs/src/icon/icon.story.tsx @@ -0,0 +1,10 @@ +import type { Icon } from '@luke-ui/react/icon'; +import { defineComponentStory } from '../lib/define-component-story'; + +export const story = defineComponentStory( + new URL('./icon.story.tsx', import.meta.url), + { + initial: { name: 'add', title: 'Add' }, + priorities: ['name', 'size', 'title'], + }, +); diff --git a/apps/docs/src/lib/define-component-story.ts b/apps/docs/src/lib/define-component-story.ts new file mode 100644 index 00000000..b7b80a34 --- /dev/null +++ b/apps/docs/src/lib/define-component-story.ts @@ -0,0 +1,18 @@ +import type { ComponentPropsWithoutRef, FC } from 'react'; +import { reorderProps } from './story-utils.js'; +import { defineStory } from './story.js'; + +export function defineComponentStory>( + url: URL, + options: { + initial?: Partial>; + priorities: Array; + }, +) { + return defineStory(url, { + args: { + initial: options.initial as ComponentPropsWithoutRef, + controls: { transform: reorderProps(options.priorities) }, + }, + }); +} diff --git a/apps/docs/src/lib/story-utils.ts b/apps/docs/src/lib/story-utils.ts new file mode 100644 index 00000000..ad215a41 --- /dev/null +++ b/apps/docs/src/lib/story-utils.ts @@ -0,0 +1,24 @@ +import type { TypeNode } from '@fumadocs/story/type-tree'; + +export function reorderProps(priorities: Array): (node: TypeNode) => TypeNode { + const prioritySet = new Set(priorities); + + return (node: TypeNode) => { + if (node.type !== 'object') return node; + const props = node.properties; + + const byName = new Map(props.map((p) => [p.name, p])); + + const result: Array<(typeof props)[number]> = []; + for (const name of priorities) { + const p = byName.get(name); + if (p !== undefined) result.push(p); + } + + for (const p of props) { + if (!prioritySet.has(p.name)) result.push(p); + } + + return { ...node, properties: result }; + }; +} diff --git a/apps/docs/src/lib/story-wrapper.tsx b/apps/docs/src/lib/story-wrapper.tsx new file mode 100644 index 00000000..2a3a12f5 --- /dev/null +++ b/apps/docs/src/lib/story-wrapper.tsx @@ -0,0 +1,43 @@ +import type { Story } from '@fumadocs/story'; +import { createStoryClient } from '@fumadocs/story/client'; +import { IconSpritesheetProvider } from '@luke-ui/react/icon'; +import spriteSheetHref from '@luke-ui/react/spritesheet.svg?url&no-inline'; +import { vars } from '@luke-ui/react/theme'; +import type { ComponentPropsWithoutRef, FC, ReactNode } from 'react'; +import { createElement } from 'react'; + +type StoryWrapperProps = { children: ReactNode }; + +function StoryWrapper({ children }: StoryWrapperProps) { + return ( +
+ {children} +
+ ); +} + +type StoryComponent = + StoryType extends Story> ? Component : never; + +export function createWrappedStoryClient( + Component: StoryComponent, +) { + type Component = StoryComponent; + type Props = ComponentPropsWithoutRef; + + return createStoryClient({ + Component: ((props: Props) => ( + {createElement(Component as FC, props)} + )) as Component, + }); +} diff --git a/apps/docs/src/lib/story.ts b/apps/docs/src/lib/story.ts new file mode 100644 index 00000000..82323844 --- /dev/null +++ b/apps/docs/src/lib/story.ts @@ -0,0 +1,14 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createFileSystemCache, defineStoryFactory } from '@fumadocs/story'; + +// Resolved at build time by storySourcePathPlugin — points to apps/docs/ +const appRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); + +export const { defineStory, getStoryPayloads } = defineStoryFactory({ + cache: + process.env.NODE_ENV === 'production' + ? createFileSystemCache(path.join(appRoot, '.story/cache')) + : undefined, + tsc: { tsconfigPath: path.join(appRoot, 'tsconfig.json') }, +}); diff --git a/apps/docs/src/link/link.story.client.tsx b/apps/docs/src/link/link.story.client.tsx new file mode 100644 index 00000000..081707b2 --- /dev/null +++ b/apps/docs/src/link/link.story.client.tsx @@ -0,0 +1,5 @@ +import { Link } from '@luke-ui/react/link'; +import { createWrappedStoryClient } from '../lib/story-wrapper'; +import type { story } from './link.story'; + +export const storyClient = createWrappedStoryClient(Link); diff --git a/apps/docs/src/link/link.story.tsx b/apps/docs/src/link/link.story.tsx new file mode 100644 index 00000000..5fd61639 --- /dev/null +++ b/apps/docs/src/link/link.story.tsx @@ -0,0 +1,10 @@ +import type { Link } from '@luke-ui/react/link'; +import { defineComponentStory } from '../lib/define-component-story'; + +export const story = defineComponentStory( + new URL('./link.story.tsx', import.meta.url), + { + initial: { href: '#', children: 'Link' }, + priorities: ['children', 'href', 'tone', 'isStandalone'], + }, +); diff --git a/apps/docs/src/loading-spinner/loading-spinner.story.client.tsx b/apps/docs/src/loading-spinner/loading-spinner.story.client.tsx new file mode 100644 index 00000000..9a0f2338 --- /dev/null +++ b/apps/docs/src/loading-spinner/loading-spinner.story.client.tsx @@ -0,0 +1,5 @@ +import { LoadingSpinner } from '@luke-ui/react/loading-spinner'; +import { createWrappedStoryClient } from '../lib/story-wrapper'; +import type { story } from './loading-spinner.story'; + +export const storyClient = createWrappedStoryClient(LoadingSpinner); diff --git a/apps/docs/src/loading-spinner/loading-spinner.story.tsx b/apps/docs/src/loading-spinner/loading-spinner.story.tsx new file mode 100644 index 00000000..f6f858b6 --- /dev/null +++ b/apps/docs/src/loading-spinner/loading-spinner.story.tsx @@ -0,0 +1,10 @@ +import type { LoadingSpinner } from '@luke-ui/react/loading-spinner'; +import { defineComponentStory } from '../lib/define-component-story'; + +export const story = defineComponentStory( + new URL('./loading-spinner.story.tsx', import.meta.url), + { + initial: { 'aria-label': 'Loading' }, + priorities: ['aria-label', 'size'], + }, +); diff --git a/apps/docs/src/numeral/numeral.story.client.tsx b/apps/docs/src/numeral/numeral.story.client.tsx new file mode 100644 index 00000000..a5099b26 --- /dev/null +++ b/apps/docs/src/numeral/numeral.story.client.tsx @@ -0,0 +1,5 @@ +import { Numeral } from '@luke-ui/react/numeral'; +import { createWrappedStoryClient } from '../lib/story-wrapper'; +import type { story } from './numeral.story'; + +export const storyClient = createWrappedStoryClient(Numeral); diff --git a/apps/docs/src/numeral/numeral.story.tsx b/apps/docs/src/numeral/numeral.story.tsx new file mode 100644 index 00000000..d0583b8d --- /dev/null +++ b/apps/docs/src/numeral/numeral.story.tsx @@ -0,0 +1,10 @@ +import type { Numeral } from '@luke-ui/react/numeral'; +import { defineComponentStory } from '../lib/define-component-story'; + +export const story = defineComponentStory( + new URL('./numeral.story.tsx', import.meta.url), + { + initial: { value: 12345.67 }, + priorities: ['value', 'format', 'currency', 'precision', 'abbreviate'], + }, +); diff --git a/apps/docs/src/router.tsx b/apps/docs/src/router.tsx index c33eef18..ac793521 100644 --- a/apps/docs/src/router.tsx +++ b/apps/docs/src/router.tsx @@ -3,7 +3,12 @@ import { NotFound } from './components/not-found'; import { routeTree } from './routeTree.gen'; export function getRouter() { + // Derive basePath from Vite's BASE_URL so TanStack Router knows about the + // sub-path when deployed to environments like GitHub Pages (/luke-ui/). + // Strip the trailing slash so the router receives e.g. '/luke-ui' not '/luke-ui/'. + const basePath = (import.meta.env.BASE_URL ?? '/').replace(/\/$/, ''); return createTanStackRouter({ + basepath: basePath, defaultNotFoundComponent: NotFound, defaultPreload: 'intent', routeTree, diff --git a/apps/docs/src/routes/docs/$.tsx b/apps/docs/src/routes/docs/$.tsx index dd5caa38..3e222c75 100644 --- a/apps/docs/src/routes/docs/$.tsx +++ b/apps/docs/src/routes/docs/$.tsx @@ -1,3 +1,6 @@ +import type { Story } from '@fumadocs/story'; +import type { StoryClient } from '@fumadocs/story/client'; +import { StoryPayloadProvider } from '@fumadocs/story/client'; import { createFileRoute, notFound } from '@tanstack/react-router'; import { createServerFn } from '@tanstack/react-start'; import { staticFunctionMiddleware } from '@tanstack/start-static-server-functions'; @@ -9,6 +12,23 @@ import { Suspense } from 'react'; import browserCollections from '../../../.source/browser'; import { baseOptions } from '../../lib/layout.shared'; import { source } from '../../lib/source'; +import { getStoryPayloads } from '../../lib/story'; + +const storyModules = import.meta.glob<{ story: Story }>('../../*/*.story.tsx', { eager: true }); +const clientStoryModules = import.meta.glob<{ storyClient: StoryClient }>( + '../../*/*.story.client.tsx', + { eager: true }, +); + +const dirName = (globPath: string) => globPath.split('/').at(-2)!; + +const stories: Record = Object.fromEntries( + Object.entries(storyModules).map(([p, mod]) => [dirName(p), mod.story]), +); + +const clientStories: Record = Object.fromEntries( + Object.entries(clientStoryModules).map(([p, mod]) => [dirName(p), mod.storyClient]), +); export const Route = createFileRoute('/docs/$')({ component: Page, @@ -24,7 +44,8 @@ const loader = createServerFn({ method: 'GET', }) .inputValidator((slugs: Array) => slugs) - .middleware([staticFunctionMiddleware]) + // staticFunctionMiddleware breaks Vite HMR in dev — only apply in prod build. + .middleware(import.meta.env.PROD ? [staticFunctionMiddleware] : []) .handler(async ({ data: slugs }) => { const page = source.getPage(slugs); if (!page) throw notFound(); @@ -32,13 +53,13 @@ const loader = createServerFn({ return { pageTree: await source.serializePageTree(source.getPageTree()), path: page.path, + storyPayloads: await getStoryPayloads(stories), }; }); const clientLoader = browserCollections.docs.createClientLoader({ component( { toc, frontmatter, default: MDX }, - // You can define props for the component props: { className?: string; }, @@ -64,11 +85,16 @@ function Page() { return ( - - {clientLoader.useContent(data.path, { - className: '', - })} - + + + {clientLoader.useContent(data.path, { + className: '', + })} + + ); } diff --git a/apps/docs/src/styles/app.css b/apps/docs/src/styles/app.css index 57b0c9ba..7d7a12e0 100644 --- a/apps/docs/src/styles/app.css +++ b/apps/docs/src/styles/app.css @@ -4,3 +4,4 @@ @import 'tailwindcss'; @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; +@import '@fumadocs/story/css/preset.css'; diff --git a/apps/docs/src/text-field/text-field.story.client.tsx b/apps/docs/src/text-field/text-field.story.client.tsx new file mode 100644 index 00000000..64ca579f --- /dev/null +++ b/apps/docs/src/text-field/text-field.story.client.tsx @@ -0,0 +1,5 @@ +import { TextField } from '@luke-ui/react/text-field'; +import { createWrappedStoryClient } from '../lib/story-wrapper'; +import type { story } from './text-field.story'; + +export const storyClient = createWrappedStoryClient(TextField); diff --git a/apps/docs/src/text-field/text-field.story.tsx b/apps/docs/src/text-field/text-field.story.tsx new file mode 100644 index 00000000..401f9993 --- /dev/null +++ b/apps/docs/src/text-field/text-field.story.tsx @@ -0,0 +1,10 @@ +import type { TextField } from '@luke-ui/react/text-field'; +import { defineComponentStory } from '../lib/define-component-story'; + +export const story = defineComponentStory( + new URL('./text-field.story.tsx', import.meta.url), + { + initial: { name: 'email', label: 'Email' }, + priorities: ['name', 'label', 'description', 'placeholder', 'size', 'isRequired'], + }, +); diff --git a/apps/docs/src/text/text.story.client.tsx b/apps/docs/src/text/text.story.client.tsx new file mode 100644 index 00000000..d53bfb25 --- /dev/null +++ b/apps/docs/src/text/text.story.client.tsx @@ -0,0 +1,5 @@ +import { Text } from '@luke-ui/react/text'; +import { createWrappedStoryClient } from '../lib/story-wrapper'; +import type { story } from './text.story'; + +export const storyClient = createWrappedStoryClient(Text); diff --git a/apps/docs/src/text/text.story.tsx b/apps/docs/src/text/text.story.tsx new file mode 100644 index 00000000..9b8881b3 --- /dev/null +++ b/apps/docs/src/text/text.story.tsx @@ -0,0 +1,10 @@ +import type { Text } from '@luke-ui/react/text'; +import { defineComponentStory } from '../lib/define-component-story'; + +export const story = defineComponentStory( + new URL('./text.story.tsx', import.meta.url), + { + initial: { children: 'The quick brown fox jumps over the lazy dog.' }, + priorities: ['children', 'fontSize', 'lineHeight', 'color', 'fontFamily', 'fontWeight'], + }, +); diff --git a/apps/docs/vite.config.ts b/apps/docs/vite.config.ts index b816512f..cd4b6d50 100644 --- a/apps/docs/vite.config.ts +++ b/apps/docs/vite.config.ts @@ -1,35 +1,98 @@ +import path from 'node:path'; import tailwindcss from '@tailwindcss/vite'; import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import react from '@vitejs/plugin-react'; import mdx from 'fumadocs-mdx/vite'; import { nitro } from 'nitro/vite'; import { defineConfig } from 'vite'; -import tsConfigPaths from 'vite-tsconfig-paths'; +import type { Plugin } from 'vite'; + +// Rewrites import.meta.url references in story-related files to absolute source +// paths so getStoryPayloads (ts-morph analysis) works when the preview server +// runs from the compiled .output/server directory during static prerendering. +function storySourcePathPlugin(): Plugin { + return { + name: 'story-source-path', + enforce: 'pre', + transform(code: string, id: string): string | null { + const cleanId = id.split('?')[0] ?? ''; + + // Story files: replace new URL('./foo.story.tsx', import.meta.url) + if (cleanId.endsWith('.story.tsx')) { + let replaced = false; + const result = code.replace( + /new URL\(\s*(['"])(\.\.?\/[^'"]+\.story\.tsx)\1\s*,\s*import\.meta\.url\s*\)/g, + (_, _quote, relativePath: string) => { + replaced = true; + const absolutePath = path.resolve(path.dirname(cleanId), relativePath); + return JSON.stringify(absolutePath); + }, + ); + if (!replaced) { + throw new Error( + `[story-source-path] ${cleanId} has no 'new URL("./name.story.tsx", import.meta.url)' literal. ` + + `This call is required so ts-morph can resolve the source file during prerendering.`, + ); + } + return result; + } + + // lib/story.ts: freeze import.meta.url to the source file URL so + // the derived appRoot (tsconfig + cache paths) is always correct. + if (cleanId.endsWith('/lib/story.ts')) { + const sourceUrl = `file://${cleanId.replace(/\\/g, '/')}`; + return code.replace(/import\.meta\.url/g, JSON.stringify(sourceUrl)); + } + + return null; + }, + }; +} export default defineConfig(async () => ({ + // Allow overriding the base URL for deployments to sub-paths (e.g. GitHub Pages). + // Set VITE_BASE_URL to the base path with a trailing slash, e.g. /luke-ui/ + base: process.env.VITE_BASE_URL ?? '/', + // Tell TanStack Start's plugin where the client output lives so it bakes the + // correct TSS_CLIENT_OUTPUT_DIR into the server bundle. Nitro's + // configEnvironment hook sets the resolved client env outDir to + // .output/public independently, but TSS_CLIENT_OUTPUT_DIR is frozen at + // build time from the *user* config before Nitro runs — without this, + // staticFunctionMiddleware writes __tsr cache files to dist/client/ instead + // of .output/public/, requiring a postbuild cp -r to fix it up. + environments: { + client: { + build: { + // TSS_CLIENT_OUTPUT_DIR is baked into the server bundle at build time + // from the user config (before Nitro overrides the resolved outDir). + // The preview server that runs during prerendering has CWD = .output/, + // so this value must be relative to .output/ — "public" resolves to + // .output/public, which is where staticFunctionMiddleware should write + // __tsr cache files. Nitro's configEnvironment hook overrides the + // actual build outDir to its own .output/public path independently. + outDir: 'public', + }, + }, + }, optimizeDeps: { exclude: ['@luke-ui/react'], }, + resolve: { + external: ['ts-morph'], + tsconfigPaths: true, + }, plugins: [ + storySourcePathPlugin(), mdx(await import('./source.config')), tailwindcss(), - tsConfigPaths({ - projects: ['./tsconfig.json'], - }), tanstackStart({ - pages: [{ path: '/docs' }, { path: '/api/search' }], - - spa: { + prerender: { enabled: true, - prerender: { - crawlLinks: true, - enabled: true, - outputPath: 'index.html', - }, + crawlLinks: true, }, + pages: [{ path: '/api/search' }], }), react(), - // Please see https://tanstack.com/start/latest/docs/framework/react/guide/hosting#nitro for guides on hosting nitro(), ], server: { diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index 05b12e7c..74154dd2 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -19,7 +19,20 @@ Managed by `oxfmt`. Tabs, 2 width, 80 width, single quotes (TS), double quotes ( ## Component Pattern Wrap `react-aria-components`. Use `composeRenderProps` for styling. -Split into `primitives/` (base) and `composed/` (opinionated). + +Components follow a three-tier taxonomy (see `CONTEXT.md` for full definitions): + +- **Atom** — single conceptual unit used directly by app devs (`Text`, `Link`, `Icon`, `Heading`, `Numeral`, `Emoji`, `LoadingSpinner`). Gets a doc page. +- **Composed** — combines atoms/primitives into a ready-to-drop-in pattern (`Button`, `IconButton`, `CloseButton`, `TextField`, `ComboboxField`). Gets a doc page. +- **Primitive** — building block for library authors only; never doc'd publicly. May be a single file (e.g. `text-input`) or a multi-file kit (e.g. `combobox/*`, `field/*`). + +## Package paths + +Composed components are exported at their bare name (`@luke-ui/react/button`). +Primitive kits that underpin a composed component are exported at +`[composed]/primitive` (`@luke-ui/react/text-field/primitive`, +`@luke-ui/react/combobox-field/primitive`, `@luke-ui/react/field/primitive`). +The `button/primitive` path follows the same pattern. ## Exports diff --git a/docs/adr/0001-component-tier-taxonomy.md b/docs/adr/0001-component-tier-taxonomy.md new file mode 100644 index 00000000..78cf0db6 --- /dev/null +++ b/docs/adr/0001-component-tier-taxonomy.md @@ -0,0 +1,16 @@ +# Component tier taxonomy: atom / composed / primitive + +Components are classified into three tiers — **atom**, **composed**, and **primitive** — which determine whether a component gets a public doc page. + +- **Atoms** (`Text`, `Link`, `Icon`, `LoadingSpinner`, `Heading`, `Emoji`, `Numeral`) present as a single conceptual unit; app devs use them directly. They get docs. +- **Composed** (`Button`, `IconButton`, `CloseButton`, `TextField`, `ComboboxField`) combine atoms or primitives into a ready-to-drop-in pattern for app devs. They get docs. +- **Primitives** (`TextInput`, the `Combobox*` kit, `button/primitive`, the `Field` kit) exist for library authors building the next composed component. They do not get docs; their contract lives in source. + +We rejected a two-tier "composed vs primitive" model because it couldn't classify atoms cleanly — `Text` is labeled "primitive" in its own JSDoc but is nothing like `TextInput` in terms of who reaches for it. We also rejected classifying the composed `Field` as doc-worthy: although it composes other components, its audience is library authors (it is always wrapped by a `*-Field`), not app devs. + +The rule in one line: **document what an app developer drops into their UI; don't document what only exists to build the next component.** + +## Consequences + +- Source JSDoc should use "atom", "composed", and "primitive" consistently — not the generic word "primitive" for everything that isn't a plain HTML element. +- `docs/CONVENTIONS.md` should be updated from its current "primitives/ (base) and composed/ (opinionated)" two-way description to the three-tier taxonomy. diff --git a/docs/adr/0002-primitive-package-path-convention.md b/docs/adr/0002-primitive-package-path-convention.md new file mode 100644 index 00000000..2826b6fb --- /dev/null +++ b/docs/adr/0002-primitive-package-path-convention.md @@ -0,0 +1,21 @@ +# Primitive kit exports live at `[composed]/primitive` + +When a primitive kit underpins a composed component, it is exported at `[composed]/primitive` rather than at a bare top-level path. + +Examples: + +- `@luke-ui/react/text-field/primitive` — exports `TextInput` +- `@luke-ui/react/combobox-field/primitive` — exports `ComboboxInput`, `ComboboxControl`, `ComboboxListBox`, etc. +- `@luke-ui/react/field/primitive` — exports the composed `Field`, the primitive field div, `FieldLabel`, `FieldError`, `FieldDescription` + +This extends the convention already established by `@luke-ui/react/button/primitive` and `@luke-ui/react/field/primitive`. + +We rejected a top-level `./combobox` (or `./text-input`) flat path because it gives no signal about audience — the import line looks identical to an atom or composed component. We rejected a source-folder reshuffle (`primitives/`, `composed/`, `atoms/` at the package root) because it churns internal import paths without changing the public API shape. We rejected making primitives fully internal (removed from `package.json` exports entirely) because the package is published publicly and future external consumers may need to assemble their own composed wrappers. + +The `[composed]/primitive` path is the right trade-off: the audience is visible in the import (`combobox-field/primitive` reads as "the primitive underpinning of ComboboxField"), the public API surface remains accessible, and no source folders move. + +## Consequences + +- There is intentionally no top-level `./field` export. Consumers who want the raw field anatomy import from `./field/primitive`. +- Primitive paths are not documented (see ADR-0001). Their presence in `package.json` is a lower-level escape hatch for library authors. +- When a new primitive kit is added, its package path must follow this convention. diff --git a/knip.config.ts b/knip.config.ts index 248ddf83..e977ed45 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -15,6 +15,7 @@ export default { 'src/styles/app.css', ], project: ['src/**/*.{ts,tsx}'], + ignoreDependencies: ['ts-morph'], }, 'packages/@luke-ui/react': { entry: [ diff --git a/packages/@luke-ui/react/AGENTS.md b/packages/@luke-ui/react/AGENTS.md index fdaf0040..361077ab 100644 --- a/packages/@luke-ui/react/AGENTS.md +++ b/packages/@luke-ui/react/AGENTS.md @@ -4,3 +4,18 @@ - When adding a component, use `pnpm generate:component` from repo root (not manual file creation) so the group barrel, styles index, and docs are updated correctly. - Stories (`*.stories.tsx`) are the tests; there are no separate `*.test.tsx` files. - React Compiler is enabled — do not use `useCallback` or `useMemo`; the compiler auto-memoizes. + +## Component Structure + +Each component directory contains: + +- `[component].docs.mdx` — documentation (consumed by `apps/docs`) +- `[component].stories.tsx` — Storybook stories (also serve as tests) +- `index.tsx` — component implementation +- `primitive/` — optional primitive exports + +## Component Taxonomy + +Components follow a three-tier system (Atom/Composed/Primitive). See `docs/CONVENTIONS.md` for definitions. + +**Important:** Primitives (exports from `*/primitive/`) are building blocks for library authors only and don't require docs or stories. diff --git a/packages/@luke-ui/react/package.json b/packages/@luke-ui/react/package.json index 9ce683af..78f8ce44 100644 --- a/packages/@luke-ui/react/package.json +++ b/packages/@luke-ui/react/package.json @@ -9,13 +9,12 @@ ], "exports": { "./button": "./dist/button/index.js", - "./button/primitive": "./dist/button/primitive.js", + "./button/primitive": "./dist/button/primitive/index.js", "./close-button": "./dist/close-button/index.js", - "./combobox": "./dist/combobox/index.js", "./combobox-field": "./dist/combobox-field/index.js", + "./combobox-field/primitive": "./dist/combobox-field/primitive/index.js", "./emoji": "./dist/emoji/index.js", - "./field": "./dist/field/index.js", - "./field/primitive": "./dist/field/primitive.js", + "./field/primitive": "./dist/field/primitive/index.js", "./heading": "./dist/heading/index.js", "./heading-context": "./dist/heading-context/index.js", "./icon": "./dist/icon/index.js", @@ -27,7 +26,7 @@ "./recipes": "./dist/recipes/index.js", "./text": "./dist/text/index.js", "./text-field": "./dist/text-field/index.js", - "./text-input": "./dist/text-input/index.js", + "./text-field/primitive": "./dist/text-field/primitive/index.js", "./theme": "./dist/theme/index.js", "./tokens": "./dist/tokens/index.js", "./utils": "./dist/utils/index.js", diff --git a/packages/@luke-ui/react/src/button.docs.mdx b/packages/@luke-ui/react/src/button/button.docs.mdx similarity index 100% rename from packages/@luke-ui/react/src/button.docs.mdx rename to packages/@luke-ui/react/src/button/button.docs.mdx diff --git a/packages/@luke-ui/react/src/button.stories.tsx b/packages/@luke-ui/react/src/button/button.stories.tsx similarity index 98% rename from packages/@luke-ui/react/src/button.stories.tsx rename to packages/@luke-ui/react/src/button/button.stories.tsx index f936afbb..637d19b9 100644 --- a/packages/@luke-ui/react/src/button.stories.tsx +++ b/packages/@luke-ui/react/src/button/button.stories.tsx @@ -4,7 +4,7 @@ import { Icon } from '@luke-ui/react/icon'; import { vars } from '@luke-ui/react/theme'; import type { CSSProperties } from 'react'; import { expect } from 'storybook/test'; -import preview from '../.storybook/preview.js'; +import preview from '../../.storybook/preview.js'; const meta = preview.meta({ component: Button, diff --git a/packages/@luke-ui/react/src/button/index.tsx b/packages/@luke-ui/react/src/button/index.tsx index 6fe4e61e..9cd7be4c 100644 --- a/packages/@luke-ui/react/src/button/index.tsx +++ b/packages/@luke-ui/react/src/button/index.tsx @@ -1,11 +1,11 @@ import type { JSX } from 'react'; -import type { ButtonProps as PrimitiveButtonProps } from '../button/primitive.js'; -import { Button as PrimitiveButton } from '../button/primitive.js'; import { LoadingSpinner } from '../loading-spinner/index.js'; import * as styles from '../recipes/button-composed.css.js'; import type * as primitiveStyles from '../recipes/button.css.js'; import { BUTTON_FONT_SIZE, BUTTON_ICON_SIZE } from '../sizing/button-sizing.js'; import { Text } from '../text/index.js'; +import type { ButtonProps as PrimitiveButtonProps } from './primitive/index.js'; +import { Button as PrimitiveButton } from './primitive/index.js'; interface ComposedButtonVariantProps extends NonNullable {} diff --git a/packages/@luke-ui/react/src/button/primitive.stories.tsx b/packages/@luke-ui/react/src/button/primitive.stories.tsx deleted file mode 100644 index 8eec069e..00000000 --- a/packages/@luke-ui/react/src/button/primitive.stories.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Button } from '@luke-ui/react/button'; -import { Icon } from '@luke-ui/react/icon'; -import type { CSSProperties } from 'react'; -import { expect } from 'storybook/test'; -import preview from '../../.storybook/preview.js'; - -const meta = preview.meta({ - component: Button, - tags: ['actions'], - title: 'Actions/Button (Primitive)', -}); - -const rowStyle = { - display: 'flex', - flexWrap: 'wrap', - gap: '1rem', -} as const satisfies CSSProperties; - -/** - * The primitive `Button` renders a single ` - - - ), -}); diff --git a/packages/@luke-ui/react/src/button/primitive.tsx b/packages/@luke-ui/react/src/button/primitive/index.tsx similarity index 85% rename from packages/@luke-ui/react/src/button/primitive.tsx rename to packages/@luke-ui/react/src/button/primitive/index.tsx index 2b5e1e17..ea7a8300 100644 --- a/packages/@luke-ui/react/src/button/primitive.tsx +++ b/packages/@luke-ui/react/src/button/primitive/index.tsx @@ -2,10 +2,10 @@ import type { JSX } from 'react'; import type { ButtonProps as RacButtonProps } from 'react-aria-components/Button'; import { Button as RacButton } from 'react-aria-components/Button'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; -import { IconSizeProvider } from '../icon-size-context/index.js'; -import * as styles from '../recipes/button.css.js'; -import { BUTTON_ICON_SIZE } from '../sizing/button-sizing.js'; -import { cx } from '../utils/index.js'; +import { IconSizeProvider } from '../../icon-size-context/index.js'; +import * as styles from '../../recipes/button.css.js'; +import { BUTTON_ICON_SIZE } from '../../sizing/button-sizing.js'; +import { cx } from '../../utils/index.js'; interface ButtonVariantProps extends NonNullable {} diff --git a/packages/@luke-ui/react/src/close-button/close-button.docs.mdx b/packages/@luke-ui/react/src/close-button/close-button.docs.mdx new file mode 100644 index 00000000..9af194f4 --- /dev/null +++ b/packages/@luke-ui/react/src/close-button/close-button.docs.mdx @@ -0,0 +1,16 @@ +`CloseButton` is an `IconButton` preset for close/dismiss actions. + +## Import + +```ts +import { CloseButton } from '@luke-ui/react/close-button'; +``` + +## Usage + +```tsx + +``` + +`CloseButton` renders a close icon with `aria-label="Close"` and `tone="ghost"` +by default. All `IconButton` props except `icon` are forwarded. diff --git a/packages/@luke-ui/react/src/close-button.stories.tsx b/packages/@luke-ui/react/src/close-button/close-button.stories.tsx similarity index 95% rename from packages/@luke-ui/react/src/close-button.stories.tsx rename to packages/@luke-ui/react/src/close-button/close-button.stories.tsx index e42a13d9..47ec2611 100644 --- a/packages/@luke-ui/react/src/close-button.stories.tsx +++ b/packages/@luke-ui/react/src/close-button/close-button.stories.tsx @@ -1,7 +1,7 @@ import type { CloseButtonProps } from '@luke-ui/react/close-button'; import { CloseButton } from '@luke-ui/react/close-button'; import { expect } from 'storybook/test'; -import preview from '../.storybook/preview.js'; +import preview from '../../.storybook/preview.js'; const meta = preview.meta({ component: CloseButton, diff --git a/packages/@luke-ui/react/src/combobox-field.docs.mdx b/packages/@luke-ui/react/src/combobox-field.docs.mdx deleted file mode 100644 index b57687b6..00000000 --- a/packages/@luke-ui/react/src/combobox-field.docs.mdx +++ /dev/null @@ -1,45 +0,0 @@ -`ComboboxField` is the composed searchable field built from `ComboboxInput` primitives. - -## Import - -```ts -import { ComboboxField } from '@luke-ui/react/combobox-field'; -import { ComboboxItem, ComboboxSection } from '@luke-ui/react/combobox'; -``` - -## Usage - -```tsx - - {(item) => {item.label}} - -``` - -## Grouped Options - -```tsx - - - Canada - - - Australia - - -``` - -## API Shape - -- Root props follow React Aria `ComboBox` naming. -- Composed convenience props: `label`, `description`, `errorMessage`, `necessityIndicator`, `size`, `placeholder`. -- `children` renders dynamic items from `items` or `defaultItems`. -- Composed props such as `loadingState`, `onLoadMore`, `menuWidth`, and `popoverProps` are supported directly on `ComboboxField`. -- `listBoxProps` and `loadMoreItem` remain available as lower-level escape hatches. -- Single-select only in v1. -- Async options are externally controlled and passed via `items`. -- Infinite scroll can be composed with `onLoadMore`, or customized fully with `loadMoreItem`. diff --git a/packages/@luke-ui/react/src/combobox-field/combobox-field.docs.mdx b/packages/@luke-ui/react/src/combobox-field/combobox-field.docs.mdx new file mode 100644 index 00000000..c447f3f4 --- /dev/null +++ b/packages/@luke-ui/react/src/combobox-field/combobox-field.docs.mdx @@ -0,0 +1,118 @@ +`ComboboxField` is the composed searchable single-select field. + +## Import + +```ts +import { ComboboxField } from '@luke-ui/react/combobox-field'; +import { ComboboxItem, ComboboxSection } from '@luke-ui/react/combobox-field/primitive'; +``` + +## Usage + +```tsx + + {(item) => {item.label}} + +``` + +## Grouped Options + +```tsx + + + Canada + + + Australia + + +``` + +## Required fields + +```tsx + + {(item) => {item.label}} + +``` + +## Validation + +```tsx +import { Form } from 'react-aria-components'; + +
+ validation.validationErrors.join(' ')} + > + {(item) => {item.label}} + +
; +``` + +## Async options + +Pass `loadingState` to activate built-in loading and empty states. Options are +controlled externally via `items`. + +```tsx + + {(item) => {item.label}} + +``` + +## Infinite scroll + +Use `onLoadMore` for automatic sentinel-based loading, or `loadMoreItem` for +full control. + +```tsx + + {(item) => {item.label}} + +``` + +## API Shape + +- Root props follow React Aria `ComboBox` naming. +- Composed convenience props: `label`, `description`, `errorMessage`, `necessityIndicator`, `size`, `placeholder`. +- `children` renders dynamic items from `items` or `defaultItems`. +- `listBoxProps` and `loadMoreItem` are lower-level escape hatches. +- Single-select only in v1. + +## Primitive kit + +The individual `Combobox*` building blocks used to assemble `ComboboxField` are +available for library authors who need a custom combobox layout. + +```ts +import { + ComboboxInput, + ComboboxControl, + ComboboxTextInput, + ComboboxTrigger, + ComboboxPopover, + ComboboxListBox, + ComboboxItem, + ComboboxSection, +} from '@luke-ui/react/combobox-field/primitive'; +``` diff --git a/packages/@luke-ui/react/src/combobox-field.stories.tsx b/packages/@luke-ui/react/src/combobox-field/combobox-field.stories.tsx similarity index 97% rename from packages/@luke-ui/react/src/combobox-field.stories.tsx rename to packages/@luke-ui/react/src/combobox-field/combobox-field.stories.tsx index 8cb6483c..1879004f 100644 --- a/packages/@luke-ui/react/src/combobox-field.stories.tsx +++ b/packages/@luke-ui/react/src/combobox-field/combobox-field.stories.tsx @@ -1,13 +1,13 @@ import { Button } from '@luke-ui/react/button'; -import { ComboboxItem, ComboboxSection } from '@luke-ui/react/combobox'; import { ComboboxField } from '@luke-ui/react/combobox-field'; +import { ComboboxItem, ComboboxSection } from '@luke-ui/react/combobox-field/primitive'; import type { CSSProperties } from 'react'; import { useState } from 'react'; import type { Key } from 'react-aria-components/Breadcrumbs'; import { Form } from 'react-aria-components/Form'; import { useAsyncList } from 'react-aria-components/useAsyncList'; import { expect, userEvent, within } from 'storybook/test'; -import preview from '../.storybook/preview.js'; +import preview from '../../.storybook/preview.js'; const meta = preview.meta({ component: ComboboxField, @@ -237,7 +237,7 @@ export const AsyncFiltering = meta.story({ name="pokemon" placeholder="Search for a Pokemon..." > - {(item: Pokemon) => {item.name}} + {(item) => {item.name}}

Selected: {value ?? 'None'}

@@ -282,7 +282,7 @@ export const AsyncInfiniteScroll = meta.story({ placeholder="Search or browse Pokemon..." value={value} > - {(item: Pokemon) => {item.name}} + {(item) => {item.name}}

Selected: {value ?? 'None'}

@@ -356,7 +356,7 @@ export const AriaValidation = meta.story({ placeholder="Select a country..." validationBehavior="aria" > - {(item: CountryItem) => {item.label}} + {(item) => {item.label}} diff --git a/packages/@luke-ui/react/src/combobox-field/index.tsx b/packages/@luke-ui/react/src/combobox-field/index.tsx index ab9420a7..2f233403 100644 --- a/packages/@luke-ui/react/src/combobox-field/index.tsx +++ b/packages/@luke-ui/react/src/combobox-field/index.tsx @@ -1,22 +1,22 @@ import type { CSSProperties, JSX, ReactNode } from 'react'; -import { ComboboxControl } from '../combobox/control.js'; -import { ComboboxEmptyState } from '../combobox/empty-state.js'; -import { ComboboxTextInput } from '../combobox/input.js'; -import { ComboboxLoadMoreItem } from '../combobox/item.js'; -import type { ComboboxLoadMoreItemProps } from '../combobox/item.js'; -import { ComboboxListBox } from '../combobox/listbox.js'; -import type { ComboboxListBoxProps } from '../combobox/listbox.js'; -import { ComboboxPopover } from '../combobox/popover.js'; -import type { ComboboxPopoverProps } from '../combobox/popover.js'; -import { ComboboxInput } from '../combobox/root.js'; -import type { ComboboxInputProps, ComboboxSize } from '../combobox/root.js'; -import { ComboboxTrigger } from '../combobox/trigger.js'; -import type { FieldErrorProps } from '../field/error.js'; -import { Field } from '../field/index.js'; -import type { FieldNecessityIndicator } from '../field/label.js'; +import type { FieldErrorProps } from '../field/primitive/error.js'; +import { Field } from '../field/primitive/index.js'; +import type { FieldNecessityIndicator } from '../field/primitive/label.js'; import { Icon } from '../icon/index.js'; import { LoadingSpinner } from '../loading-spinner/index.js'; import type { DistributiveOmit } from '../types/distributive-omit.js'; +import { ComboboxControl } from './primitive/control.js'; +import { ComboboxEmptyState } from './primitive/empty-state.js'; +import { ComboboxTextInput } from './primitive/input.js'; +import { ComboboxLoadMoreItem } from './primitive/item.js'; +import type { ComboboxLoadMoreItemProps } from './primitive/item.js'; +import { ComboboxListBox } from './primitive/listbox.js'; +import type { ComboboxListBoxProps } from './primitive/listbox.js'; +import { ComboboxPopover } from './primitive/popover.js'; +import type { ComboboxPopoverProps } from './primitive/popover.js'; +import { ComboboxInput } from './primitive/root.js'; +import type { ComboboxInputProps, ComboboxSize } from './primitive/root.js'; +import { ComboboxTrigger } from './primitive/trigger.js'; type ComboboxLoadingState = 'error' | 'filtering' | 'idle' | 'loading' | 'loadingMore' | 'sorting'; diff --git a/packages/@luke-ui/react/src/combobox/control.tsx b/packages/@luke-ui/react/src/combobox-field/primitive/control.tsx similarity index 85% rename from packages/@luke-ui/react/src/combobox/control.tsx rename to packages/@luke-ui/react/src/combobox-field/primitive/control.tsx index 2387776a..a9e75087 100644 --- a/packages/@luke-ui/react/src/combobox/control.tsx +++ b/packages/@luke-ui/react/src/combobox-field/primitive/control.tsx @@ -2,9 +2,9 @@ import type { JSX } from 'react'; import type { GroupProps as RacGroupProps } from 'react-aria-components/Group'; import { Group as RacGroup } from 'react-aria-components/Group'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; -import * as styles from '../recipes/combobox.css.js'; -import type { DistributiveOmit } from '../types/distributive-omit.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/combobox.css.js'; +import type { DistributiveOmit } from '../../types/distributive-omit.js'; +import { cx } from '../../utils/index.js'; interface ComboboxVariantProps extends NonNullable {} diff --git a/packages/@luke-ui/react/src/combobox/empty-state.tsx b/packages/@luke-ui/react/src/combobox-field/primitive/empty-state.tsx similarity index 77% rename from packages/@luke-ui/react/src/combobox/empty-state.tsx rename to packages/@luke-ui/react/src/combobox-field/primitive/empty-state.tsx index ba93e493..0641074e 100644 --- a/packages/@luke-ui/react/src/combobox/empty-state.tsx +++ b/packages/@luke-ui/react/src/combobox-field/primitive/empty-state.tsx @@ -1,6 +1,6 @@ import type { JSX, ReactNode } from 'react'; -import * as styles from '../recipes/combobox.css.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/combobox.css.js'; +import { cx } from '../../utils/index.js'; export interface ComboboxEmptyStateProps { children: ReactNode; diff --git a/packages/@luke-ui/react/src/combobox/index.tsx b/packages/@luke-ui/react/src/combobox-field/primitive/index.tsx similarity index 100% rename from packages/@luke-ui/react/src/combobox/index.tsx rename to packages/@luke-ui/react/src/combobox-field/primitive/index.tsx diff --git a/packages/@luke-ui/react/src/combobox/input.tsx b/packages/@luke-ui/react/src/combobox-field/primitive/input.tsx similarity index 88% rename from packages/@luke-ui/react/src/combobox/input.tsx rename to packages/@luke-ui/react/src/combobox-field/primitive/input.tsx index ee906bd2..7ef8487d 100644 --- a/packages/@luke-ui/react/src/combobox/input.tsx +++ b/packages/@luke-ui/react/src/combobox-field/primitive/input.tsx @@ -3,9 +3,9 @@ import { useContext } from 'react'; import type { InputProps as RacInputProps } from 'react-aria-components/ComboBox'; import { ComboBoxStateContext, Input as RacInput } from 'react-aria-components/ComboBox'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; -import * as styles from '../recipes/combobox.css.js'; -import type { DistributiveOmit } from '../types/distributive-omit.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/combobox.css.js'; +import type { DistributiveOmit } from '../../types/distributive-omit.js'; +import { cx } from '../../utils/index.js'; interface ComboboxVariantProps extends NonNullable {} diff --git a/packages/@luke-ui/react/src/combobox/item.tsx b/packages/@luke-ui/react/src/combobox-field/primitive/item.tsx similarity index 89% rename from packages/@luke-ui/react/src/combobox/item.tsx rename to packages/@luke-ui/react/src/combobox-field/primitive/item.tsx index e0d3fa8f..794f6c49 100644 --- a/packages/@luke-ui/react/src/combobox/item.tsx +++ b/packages/@luke-ui/react/src/combobox-field/primitive/item.tsx @@ -8,9 +8,9 @@ import { ListBoxLoadMoreItem as RacListBoxLoadMoreItem, } from 'react-aria-components/ComboBox'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; -import * as styles from '../recipes/combobox.css.js'; -import type { DistributiveOmit } from '../types/distributive-omit.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/combobox.css.js'; +import type { DistributiveOmit } from '../../types/distributive-omit.js'; +import { cx } from '../../utils/index.js'; interface ComboboxStyleProps { size?: 'small' | 'medium'; diff --git a/packages/@luke-ui/react/src/combobox/listbox.tsx b/packages/@luke-ui/react/src/combobox-field/primitive/listbox.tsx similarity index 91% rename from packages/@luke-ui/react/src/combobox/listbox.tsx rename to packages/@luke-ui/react/src/combobox-field/primitive/listbox.tsx index b1926f43..7179906d 100644 --- a/packages/@luke-ui/react/src/combobox/listbox.tsx +++ b/packages/@luke-ui/react/src/combobox-field/primitive/listbox.tsx @@ -5,9 +5,9 @@ import { ListBox as RacListBox } from 'react-aria-components/ComboBox'; import { ListBoxContext } from 'react-aria-components/ListBox'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; import { useSlottedContext } from 'react-aria-components/slots'; -import * as styles from '../recipes/combobox.css.js'; -import type { DistributiveOmit } from '../types/distributive-omit.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/combobox.css.js'; +import type { DistributiveOmit } from '../../types/distributive-omit.js'; +import { cx } from '../../utils/index.js'; /** Props for the styled listbox. */ export interface ComboboxListBoxProps extends DistributiveOmit< diff --git a/packages/@luke-ui/react/src/combobox/popover.tsx b/packages/@luke-ui/react/src/combobox-field/primitive/popover.tsx similarity index 82% rename from packages/@luke-ui/react/src/combobox/popover.tsx rename to packages/@luke-ui/react/src/combobox-field/primitive/popover.tsx index 7011739a..069cdce1 100644 --- a/packages/@luke-ui/react/src/combobox/popover.tsx +++ b/packages/@luke-ui/react/src/combobox-field/primitive/popover.tsx @@ -2,9 +2,9 @@ import type { JSX } from 'react'; import type { PopoverProps as RacPopoverProps } from 'react-aria-components/ComboBox'; import { Popover as RacPopover } from 'react-aria-components/ComboBox'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; -import * as styles from '../recipes/combobox.css.js'; -import { themeRootClassName } from '../theme/index.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/combobox.css.js'; +import { themeRootClassName } from '../../theme/index.js'; +import { cx } from '../../utils/index.js'; /** Props for the styled combobox popover. */ export interface ComboboxPopoverProps extends Omit {} diff --git a/packages/@luke-ui/react/src/combobox/root.tsx b/packages/@luke-ui/react/src/combobox-field/primitive/root.tsx similarity index 90% rename from packages/@luke-ui/react/src/combobox/root.tsx rename to packages/@luke-ui/react/src/combobox-field/primitive/root.tsx index f8afc42d..a35b80e6 100644 --- a/packages/@luke-ui/react/src/combobox/root.tsx +++ b/packages/@luke-ui/react/src/combobox-field/primitive/root.tsx @@ -2,9 +2,9 @@ import type { JSX } from 'react'; import type { ComboBoxProps as RacComboBoxProps, Key } from 'react-aria-components/ComboBox'; import { ComboBox as RacComboBox } from 'react-aria-components/ComboBox'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; -import * as styles from '../recipes/combobox.css.js'; -import type { DistributiveOmit } from '../types/distributive-omit.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/combobox.css.js'; +import type { DistributiveOmit } from '../../types/distributive-omit.js'; +import { cx } from '../../utils/index.js'; interface ComboboxVariantProps extends NonNullable {} diff --git a/packages/@luke-ui/react/src/combobox/section.tsx b/packages/@luke-ui/react/src/combobox-field/primitive/section.tsx similarity index 86% rename from packages/@luke-ui/react/src/combobox/section.tsx rename to packages/@luke-ui/react/src/combobox-field/primitive/section.tsx index 4200452b..5c7dff14 100644 --- a/packages/@luke-ui/react/src/combobox/section.tsx +++ b/packages/@luke-ui/react/src/combobox-field/primitive/section.tsx @@ -2,9 +2,9 @@ import type { JSX, ReactNode } from 'react'; import type { ListBoxSectionProps as RacListBoxSectionProps } from 'react-aria-components/ComboBox'; import { ListBoxSection as RacListBoxSection } from 'react-aria-components/ComboBox'; import { Header as RacHeader } from 'react-aria-components/Header'; -import * as styles from '../recipes/combobox.css.js'; -import type { DistributiveOmit } from '../types/distributive-omit.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/combobox.css.js'; +import type { DistributiveOmit } from '../../types/distributive-omit.js'; +import { cx } from '../../utils/index.js'; export interface ComboboxSectionProps extends DistributiveOmit< RacListBoxSectionProps, diff --git a/packages/@luke-ui/react/src/combobox/trigger.tsx b/packages/@luke-ui/react/src/combobox-field/primitive/trigger.tsx similarity index 85% rename from packages/@luke-ui/react/src/combobox/trigger.tsx rename to packages/@luke-ui/react/src/combobox-field/primitive/trigger.tsx index ec0cccd1..910e4b3a 100644 --- a/packages/@luke-ui/react/src/combobox/trigger.tsx +++ b/packages/@luke-ui/react/src/combobox-field/primitive/trigger.tsx @@ -2,9 +2,9 @@ import type { JSX } from 'react'; import type { ButtonProps as RacButtonProps } from 'react-aria-components/ComboBox'; import { Button as RacButton } from 'react-aria-components/ComboBox'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; -import * as styles from '../recipes/combobox.css.js'; -import type { DistributiveOmit } from '../types/distributive-omit.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/combobox.css.js'; +import type { DistributiveOmit } from '../../types/distributive-omit.js'; +import { cx } from '../../utils/index.js'; interface ComboboxVariantProps extends NonNullable {} diff --git a/packages/@luke-ui/react/src/combobox.docs.mdx b/packages/@luke-ui/react/src/combobox.docs.mdx deleted file mode 100644 index cb7ac317..00000000 --- a/packages/@luke-ui/react/src/combobox.docs.mdx +++ /dev/null @@ -1,43 +0,0 @@ -`ComboboxInput` provides low-level, Spectrum-style combobox primitives built on React Aria Components. - -## Import - -```ts -import { - ComboboxInput, - ComboboxControl, - ComboboxTextInput, - ComboboxTrigger, - ComboboxPopover, - ComboboxListBox, - ComboboxLoadMoreItem, - ComboboxItem, - ComboboxSection, -} from '@luke-ui/react/combobox'; -``` - -## Usage - -```tsx - - - - - - - - - {(item) => {item.label}} - - -``` - -## API Notes - -- Spectrum-first prop naming is used (`value`, `defaultValue`, `onChange`, `inputValue`, `onInputChange`). -- This v1 primitive is single-select only. -- Grouped options are composed with `ComboboxSection`. -- Async collections are controlled externally with `useAsyncList` (or equivalent), then passed via the root `items` prop. -- Use the `children` render prop on `ComboboxListBox` to render dynamic `items`. -- Use `renderEmptyState` on `ComboboxListBox` for initial async loading states. -- Use `loadMoreItem` on `ComboboxListBox` with `ComboboxLoadMoreItem` for infinite scroll sentinels and loading spinners. diff --git a/packages/@luke-ui/react/src/combobox.stories.tsx b/packages/@luke-ui/react/src/combobox.stories.tsx deleted file mode 100644 index 19fb4776..00000000 --- a/packages/@luke-ui/react/src/combobox.stories.tsx +++ /dev/null @@ -1,330 +0,0 @@ -import { - ComboboxControl, - ComboboxEmptyState, - ComboboxInput, - ComboboxItem, - ComboboxListBox, - ComboboxLoadMoreItem, - ComboboxPopover, - ComboboxSection, - ComboboxTextInput, - ComboboxTrigger, -} from '@luke-ui/react/combobox'; -import { Icon } from '@luke-ui/react/icon'; -import { LoadingSpinner } from '@luke-ui/react/loading-spinner'; -import type { CSSProperties } from 'react'; -import { useState } from 'react'; -import type { Key } from 'react-aria-components/Breadcrumbs'; -import { useAsyncList } from 'react-aria-components/useAsyncList'; -import { expect, userEvent, within } from 'storybook/test'; -import preview from '../.storybook/preview.js'; - -const meta = preview.meta({ - component: ComboboxInput, - tags: ['forms'], - title: 'Forms/ComboboxInput (Primitive)', -}); - -type CountryItem = { - id: string; - label: string; -}; - -const countryItems: Array = [ - { id: 'au', label: 'Australia' }, - { id: 'ca', label: 'Canada' }, - { id: 'nz', label: 'New Zealand' }, - { id: 'us', label: 'United States' }, - { id: 'se', label: 'Sweden' }, -]; - -const stackStyle = { - display: 'flex', - flexDirection: 'column', - gap: '1rem', - maxInlineSize: '24rem', -} as const satisfies CSSProperties; - -/** - * Primitive combobox composition with Spectrum-style `value`/`onChange` APIs. - */ -export const Default = meta.story({ - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const page = within(document.body); - const combobox = canvas.getByRole('combobox', { name: 'Country' }); - - // Verify the combobox renders with expected structure. - await expect(combobox).toBeInTheDocument(); - await expect(combobox).toHaveAttribute('aria-expanded', 'false'); - await expect(combobox).toHaveAttribute('placeholder', 'Select a country...'); - - // Verify trigger button renders. - await expect(canvas.getByRole('button', { name: /Toggle options/ })).toBeInTheDocument(); - - // Clicking the input opens the popover. - await userEvent.click(combobox); - await expect(combobox).toHaveAttribute('aria-expanded', 'true'); - await expect(page.getByRole('option', { name: 'Australia' })).toBeInTheDocument(); - - // Selecting an item closes the popover. - await userEvent.click(page.getByRole('option', { name: 'Australia' })); - await expect(combobox).toHaveValue('Australia'); - await expect(combobox).toHaveAttribute('aria-expanded', 'false'); - }, - render: function Render() { - return ( - - - - - - - - - - {(item) => {item.label}} - - - - ); - }, -}); - -/** - * Sizes should align with other Luke UI controls. - */ -export const Size = meta.story({ - render: function Render() { - return ( -
- - - - - - - - - - {(item) => {item.label}} - - - - - - - - - - - - - {(item) => {item.label}} - - - -
- ); - }, -}); - -/** - * Grouped options are composed with `ComboboxSection`. - */ -export const Groups = meta.story({ - render: function Render() { - return ( - - - - - - - - - - - Canada - United States - Sweden - - - Australia - New Zealand - - - - - ); - }, -}); - -interface Pokemon { - name: string; -} - -/** - * Async filtering keeps network state outside the primitive and renders - * loading and empty states through the listbox. - */ -export const AsyncFiltering = meta.story({ - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - await expect(canvas.getByRole('combobox', { name: 'Pick a Pokemon' })).toBeInTheDocument(); - await expect(canvas.getByRole('button', { name: 'Toggle options' })).toBeInTheDocument(); - }, - render: function AsyncFilteringExample() { - const [value, setValue] = useState(null); - - const list = useAsyncList({ - async load({ signal }) { - const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=100', { signal }); - const json = await res.json(); - - return { - items: json.results, - }; - }, - }); - - return ( - list.setFilterText(text)} - value={value} - onChange={setValue} - > - - - - - - - - - list.loadingState === 'loading' ? ( - - - - ) : ( - No results - ) - } - > - {(item: Pokemon) => {item.name}} - - - - ); - }, -}); - -/** - * Infinite scroll composes a load-more sentinel after the item renderer. - */ -export const AsyncInfiniteScroll = meta.story({ - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - await expect(canvas.getByRole('combobox', { name: 'Browse Pokemon' })).toBeInTheDocument(); - await expect(canvas.getByRole('button', { name: 'Toggle options' })).toBeInTheDocument(); - }, - render: function AsyncInfiniteScrollExample() { - const [value, setValue] = useState(null); - - const list = useAsyncList({ - async load({ cursor, signal }) { - const res = await fetch(cursor || 'https://pokeapi.co/api/v2/pokemon', { signal }); - const json = await res.json(); - - return { - items: json.results, - cursor: json.next, - }; - }, - }); - - return ( - list.setFilterText(text)} - value={value} - onChange={setValue} - > - - - - - - - - list.loadMore()} - > - - - } - renderEmptyState={() => - list.loadingState === 'loading' ? ( - - - - ) : ( - No results - ) - } - > - {(item: Pokemon) => {item.name}} - - - - ); - }, -}); - -/** - * Disabled and invalid states are controlled via root props. - */ -export const States = meta.story({ - render: function Render() { - return ( -
- - - - - - - - - - - - - - - - - - {(item) => {item.label}} - - - - - - - - - - - -
- ); - }, -}); diff --git a/packages/@luke-ui/react/src/emoji/emoji.docs.mdx b/packages/@luke-ui/react/src/emoji/emoji.docs.mdx new file mode 100644 index 00000000..5fb10b22 --- /dev/null +++ b/packages/@luke-ui/react/src/emoji/emoji.docs.mdx @@ -0,0 +1,16 @@ +`Emoji` renders an accessible emoji using `Text` with `role="img"`. + +## Import + +```ts +import { Emoji } from '@luke-ui/react/emoji'; +``` + +## Usage + +```tsx + +``` + +`label` is required and announced by screen readers in place of the raw emoji +character. Both props are required. diff --git a/packages/@luke-ui/react/src/emoji.stories.tsx b/packages/@luke-ui/react/src/emoji/emoji.stories.tsx similarity index 96% rename from packages/@luke-ui/react/src/emoji.stories.tsx rename to packages/@luke-ui/react/src/emoji/emoji.stories.tsx index c798198c..0d8b743d 100644 --- a/packages/@luke-ui/react/src/emoji.stories.tsx +++ b/packages/@luke-ui/react/src/emoji/emoji.stories.tsx @@ -3,7 +3,7 @@ import { Emoji } from '@luke-ui/react/emoji'; import { Text } from '@luke-ui/react/text'; import type { CSSProperties } from 'react'; import { expect } from 'storybook/test'; -import preview from '../.storybook/preview.js'; +import preview from '../../.storybook/preview.js'; const meta = preview.meta({ component: Emoji, diff --git a/packages/@luke-ui/react/src/field.docs.mdx b/packages/@luke-ui/react/src/field.docs.mdx deleted file mode 100644 index af7c1741..00000000 --- a/packages/@luke-ui/react/src/field.docs.mdx +++ /dev/null @@ -1,145 +0,0 @@ -`Field` composes label, description, and error messaging for React Aria form -controls. - -## Import - -Primitive building blocks: - -```ts -import { Field, FieldDescription, FieldError, FieldLabel } from '@luke-ui/react/field'; -``` - -Opinionated composed component: - -```ts -import { Field } from '@luke-ui/react/field'; -``` - -## Usage - -```tsx -import { Field } from '@luke-ui/react/field'; -import { Input, TextField } from 'react-aria-components'; - - - - - -; -``` - -### Label visibility - -When visual context already communicates purpose, omit the visible label and -provide an accessible name with `aria-label` or `aria-labelledby` on the parent -React Aria field. - -```tsx - - - - - -``` - -## Description - -Use `description` for persistent helper text. - -```tsx - - - - - -``` - -## Required - -`Field` supports `necessityIndicator="icon" | "label"` and reads required state -from the parent React Aria field (`[data-required]`). - -```tsx - - - - - -``` - -```tsx - - - - - -``` - -## Messages - -Use `errorMessage` to show validation feedback when React Aria marks the field -invalid. - -```tsx -import { Field } from '@luke-ui/react/field'; -import { Form, Input, TextField } from 'react-aria-components'; - -
- (value.length < 3 ? 'Must be at least 3 characters.' : null)} - > - validation.validationErrors.join(' ')}> - - - -
; -``` - -## Field columns - -Use a grid wrapper to position two fields horizontally. - -```tsx -
- - - - - - - - - - -
-``` - -## Supported components - -`Field` works with any compatible React Aria form control. Current Luke UI -primitive coverage includes: - -- `TextInput` - -## Composed `Field` Props - -| Prop | Type | Default | Notes | -| -------------------- | ----------------------------------------------- | -------- | ------------------------------------------------------- | -| `children` | `ReactNode` | - | Form control element (for example React Aria `Input`). | -| `label` | `ReactNode` | - | Label content rendered by `FieldLabel`. | -| `description` | `ReactNode` | - | Help text rendered in `slot="description"`. | -| `errorMessage` | `ComponentProps['children']` | - | Static or render-function error content. | -| `necessityIndicator` | `'icon' \| 'label'` | `'icon'` | Required indicator style when parent field is required. | -| `...rest` | `ComponentProps<'div'>` | - | Passed to root `Field` container. | - -## Accessibility Notes - -- `label` is optional, but a field must still have an accessible name via - `label`, `aria-label`, or `aria-labelledby` on the parent React Aria field. -- `FieldError` only renders when React Aria reports an invalid state. diff --git a/packages/@luke-ui/react/src/field.stories.tsx b/packages/@luke-ui/react/src/field.stories.tsx deleted file mode 100644 index ff3bf9a3..00000000 --- a/packages/@luke-ui/react/src/field.stories.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { Field } from '@luke-ui/react/field'; -import type { CSSProperties } from 'react'; -import { Form } from 'react-aria-components/Form'; -import { Input } from 'react-aria-components/Input'; -import { TextField } from 'react-aria-components/TextField'; -import { expect, userEvent, within } from 'storybook/test'; -import preview from '../.storybook/preview.js'; - -const meta = preview.meta({ - component: Field, - tags: ['forms'], - title: 'Forms/Field', -}); - -const stackStyle = { - display: 'flex', - flexDirection: 'column', - gap: '1rem', - maxInlineSize: '28rem', -} as const satisfies CSSProperties; - -/** - * Fields should be accompanied by a label so users understand what to enter. - */ -export const Label = meta.story({ - play: async ({ canvas }) => { - await expect(canvas.getByLabelText('Name')).toBeInTheDocument(); - }, - render: () => ( - - - - - - ), -}); - -/** - * When visual context is already strong, you can hide the label by omitting it - * and providing an accessible name on the parent field. - */ -export const LabelVisibility = meta.story({ - play: async ({ canvas }) => { - await expect(canvas.getByRole('textbox', { name: 'Search' })).toBeInTheDocument(); - }, - render: () => ( - - - - - - ), -}); - -/** - * Description text can provide persistent help content for field completion. - */ -export const Description = meta.story({ - play: async ({ canvas }) => { - await expect( - canvas.getByText('We will only use this to respond to your question.'), - ).toBeInTheDocument(); - }, - render: () => ( - - - - - - ), -}); - -/** - * Required fields can show either an icon (`*`) or text (`(required)`), - * controlled by `necessityIndicator`. - */ -export const Required = meta.story({ - render: () => ( -
- - - - - - - - - - -
- ), -}); - -/** - * Messages are shown through `FieldError` once React Aria marks the field - * invalid. The message can be static text or a render function. - */ -export const Messages = meta.story({ - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - await userEvent.click(canvas.getByRole('button', { name: 'Submit' })); - await expect(canvas.getByText('Please enter your full name.')).toBeInTheDocument(); - await expect(canvas.getByText('Must be at least 3 characters.')).toBeInTheDocument(); - }, - render: () => ( -
-
- - - - - - (value.length < 3 ? 'Must be at least 3 characters.' : null)} - > - validation.validationErrors.join(' ')} - label="Username" - > - - - - -
-
- ), -}); diff --git a/packages/@luke-ui/react/src/field/description.tsx b/packages/@luke-ui/react/src/field/primitive/description.tsx similarity index 88% rename from packages/@luke-ui/react/src/field/description.tsx rename to packages/@luke-ui/react/src/field/primitive/description.tsx index 1e83b58c..03f6297c 100644 --- a/packages/@luke-ui/react/src/field/description.tsx +++ b/packages/@luke-ui/react/src/field/primitive/description.tsx @@ -1,8 +1,8 @@ import type { JSX } from 'react'; import type { TextProps as RacTextProps } from 'react-aria-components/Text'; import { Text as RacText } from 'react-aria-components/Text'; -import * as styles from '../recipes/field.css.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/field.css.js'; +import { cx } from '../../utils/index.js'; interface FieldMessageVariantProps extends NonNullable {} diff --git a/packages/@luke-ui/react/src/field/error.tsx b/packages/@luke-ui/react/src/field/primitive/error.tsx similarity index 89% rename from packages/@luke-ui/react/src/field/error.tsx rename to packages/@luke-ui/react/src/field/primitive/error.tsx index 4272aa55..5b6f80c5 100644 --- a/packages/@luke-ui/react/src/field/error.tsx +++ b/packages/@luke-ui/react/src/field/primitive/error.tsx @@ -2,8 +2,8 @@ import type { JSX } from 'react'; import type { FieldErrorProps as RacFieldErrorProps } from 'react-aria-components/FieldError'; import { FieldError as RacFieldError } from 'react-aria-components/FieldError'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; -import * as styles from '../recipes/field.css.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/field.css.js'; +import { cx } from '../../utils/index.js'; interface FieldMessageVariantProps extends NonNullable {} diff --git a/packages/@luke-ui/react/src/field/index.tsx b/packages/@luke-ui/react/src/field/primitive/index.tsx similarity index 96% rename from packages/@luke-ui/react/src/field/index.tsx rename to packages/@luke-ui/react/src/field/primitive/index.tsx index ef8d01bf..eccf6c95 100644 --- a/packages/@luke-ui/react/src/field/index.tsx +++ b/packages/@luke-ui/react/src/field/primitive/index.tsx @@ -5,7 +5,7 @@ import type { FieldErrorProps } from './error.js'; import { FieldError } from './error.js'; import { FieldLabel } from './label.js'; import type { FieldLabelProps, FieldNecessityIndicator } from './label.js'; -import { Field as PrimitiveField } from './primitive.js'; +import { Field as PrimitiveField } from './root.js'; export { FieldDescription, FieldError, FieldLabel }; export type { FieldDescriptionProps, FieldErrorProps, FieldLabelProps, FieldNecessityIndicator }; diff --git a/packages/@luke-ui/react/src/field/label.tsx b/packages/@luke-ui/react/src/field/primitive/label.tsx similarity index 91% rename from packages/@luke-ui/react/src/field/label.tsx rename to packages/@luke-ui/react/src/field/primitive/label.tsx index 06cb7477..55025666 100644 --- a/packages/@luke-ui/react/src/field/label.tsx +++ b/packages/@luke-ui/react/src/field/primitive/label.tsx @@ -1,8 +1,8 @@ import type { JSX } from 'react'; import type { LabelProps as RacLabelProps } from 'react-aria-components/Label'; import { Label as RacLabel } from 'react-aria-components/Label'; -import * as styles from '../recipes/field.css.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/field.css.js'; +import { cx } from '../../utils/index.js'; interface FieldLabelVariantProps extends NonNullable {} diff --git a/packages/@luke-ui/react/src/field/primitive.tsx b/packages/@luke-ui/react/src/field/primitive/root.tsx similarity index 70% rename from packages/@luke-ui/react/src/field/primitive.tsx rename to packages/@luke-ui/react/src/field/primitive/root.tsx index 4353cd1a..287f88f2 100644 --- a/packages/@luke-ui/react/src/field/primitive.tsx +++ b/packages/@luke-ui/react/src/field/primitive/root.tsx @@ -1,9 +1,9 @@ import type { ComponentProps, JSX } from 'react'; -import * as styles from '../recipes/field.css.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/field.css.js'; +import { cx } from '../../utils/index.js'; /** Props for the primitive field container. */ -export type FieldProps = ComponentProps<'div'>; +type FieldProps = ComponentProps<'div'>; /** Simple wrapper used by field primitives. */ export function Field(props: FieldProps): JSX.Element { diff --git a/packages/@luke-ui/react/src/heading/heading.docs.mdx b/packages/@luke-ui/react/src/heading/heading.docs.mdx new file mode 100644 index 00000000..7b91cc94 --- /dev/null +++ b/packages/@luke-ui/react/src/heading/heading.docs.mdx @@ -0,0 +1,64 @@ +`Heading` renders a semantic heading element with automatic level management. + +## Import + +```ts +import { Heading } from '@luke-ui/react/heading'; +``` + +## Usage + +```tsx +Section title +``` + +```tsx +Explicit h2 +``` + +## Automatic leveling + +`Heading` reads its level from `HeadingLevels` context and increments +automatically when headings are nested. Set `base` on the context root; each +nested `HeadingLevels` advances to the next level. + +```tsx +import { Heading, HeadingLevels } from '@luke-ui/react/heading'; + + + h1 + + h2 — nested automatically + + h3 — nested again + + +; +``` + +The `level` prop overrides context for a single heading without affecting the +nesting depth for siblings or children. + +## Typography + +`Heading` accepts all `Text` props except `fontSize` (controlled internally by +level). By default it applies `fontWeight="bold"` and `lineHeight="tight"`. + +```tsx + + Informative heading + +``` + +```tsx + + Light-weight heading + +``` + +## Props + +| Prop | Type | Default | Notes | +| --------- | ---------------------------- | ------- | ---------------------------------------------- | +| `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | Context | Overrides context level for this heading only. | +| `...rest` | `TextProps` | - | All `Text` props forwarded except `fontSize`. | diff --git a/packages/@luke-ui/react/src/heading.stories.tsx b/packages/@luke-ui/react/src/heading/heading.stories.tsx similarity index 97% rename from packages/@luke-ui/react/src/heading.stories.tsx rename to packages/@luke-ui/react/src/heading/heading.stories.tsx index ba4108e0..f601b1df 100644 --- a/packages/@luke-ui/react/src/heading.stories.tsx +++ b/packages/@luke-ui/react/src/heading/heading.stories.tsx @@ -2,7 +2,7 @@ import type { HeadingProps } from '@luke-ui/react/heading'; import { Heading } from '@luke-ui/react/heading'; import type { CSSProperties } from 'react'; import { expect } from 'storybook/test'; -import preview from '../.storybook/preview.js'; +import preview from '../../.storybook/preview.js'; const meta = preview.meta({ component: Heading, diff --git a/packages/@luke-ui/react/src/icon-button/icon-button.docs.mdx b/packages/@luke-ui/react/src/icon-button/icon-button.docs.mdx new file mode 100644 index 00000000..565aae30 --- /dev/null +++ b/packages/@luke-ui/react/src/icon-button/icon-button.docs.mdx @@ -0,0 +1,50 @@ +`IconButton` renders a single icon as an interactive button. + +## Import + +```ts +import { IconButton } from '@luke-ui/react/icon-button'; +``` + +## Usage + +```tsx + +``` + +```tsx + +``` + +```tsx + +``` + +## Accessibility + +`IconButton` has no visible text label. Always provide `aria-label` or +`aria-labelledby` — without one, screen readers have no way to announce the +button's purpose. + +```tsx + +``` + +## When to use vs Button + +Use `IconButton` when the icon alone communicates the action and space is tight +(toolbars, table row actions, close buttons). Use `Button` when a visible text +label is needed — the label reduces ambiguity and improves discoverability for +less familiar users. + +## Props + +| Prop | Type | Default | Notes | +| ------------ | ------------------------------------------------- | ----------- | ------------------------------------------------ | +| `icon` | `IconName` | - | Required. Icon from the generated spritesheet. | +| `size` | `'small' \| 'medium'` | `'medium'` | Button and icon size. | +| `tone` | `'primary' \| 'critical' \| 'ghost' \| 'neutral'` | `'primary'` | Visual tone variant. | +| `isBlock` | `boolean` | `false` | Fill parent inline size. | +| `isDisabled` | `boolean` | `false` | Disables interaction. | +| `isPending` | `boolean` | `false` | Shows pending state (disables without feedback). | +| `...rest` | `ButtonProps` (primitive) | - | Forwarded to underlying primitive `Button`. | diff --git a/packages/@luke-ui/react/src/icon-button.stories.tsx b/packages/@luke-ui/react/src/icon-button/icon-button.stories.tsx similarity index 96% rename from packages/@luke-ui/react/src/icon-button.stories.tsx rename to packages/@luke-ui/react/src/icon-button/icon-button.stories.tsx index fd4485aa..c38d5f83 100644 --- a/packages/@luke-ui/react/src/icon-button.stories.tsx +++ b/packages/@luke-ui/react/src/icon-button/icon-button.stories.tsx @@ -3,7 +3,7 @@ import type { IconButtonProps } from '@luke-ui/react/icon-button'; import { IconButton } from '@luke-ui/react/icon-button'; import type { CSSProperties } from 'react'; import { expect } from 'storybook/test'; -import preview from '../.storybook/preview.js'; +import preview from '../../.storybook/preview.js'; const meta = preview.meta({ component: IconButton, diff --git a/packages/@luke-ui/react/src/icon-button/index.tsx b/packages/@luke-ui/react/src/icon-button/index.tsx index 8339ce5d..3e18e727 100644 --- a/packages/@luke-ui/react/src/icon-button/index.tsx +++ b/packages/@luke-ui/react/src/icon-button/index.tsx @@ -1,7 +1,7 @@ import type { JSX } from 'react'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; -import type { ButtonProps as PrimitiveButtonProps } from '../button/primitive.js'; -import { Button } from '../button/primitive.js'; +import type { ButtonProps as PrimitiveButtonProps } from '../button/primitive/index.js'; +import { Button } from '../button/primitive/index.js'; import type { IconName } from '../icon/index.js'; import { Icon } from '../icon/index.js'; import * as styles from '../recipes/icon-button.css.js'; diff --git a/packages/@luke-ui/react/src/icon.docs.mdx b/packages/@luke-ui/react/src/icon/icon.docs.mdx similarity index 100% rename from packages/@luke-ui/react/src/icon.docs.mdx rename to packages/@luke-ui/react/src/icon/icon.docs.mdx diff --git a/packages/@luke-ui/react/src/icon.stories.tsx b/packages/@luke-ui/react/src/icon/icon.stories.tsx similarity index 98% rename from packages/@luke-ui/react/src/icon.stories.tsx rename to packages/@luke-ui/react/src/icon/icon.stories.tsx index 662f7a05..635db5d2 100644 --- a/packages/@luke-ui/react/src/icon.stories.tsx +++ b/packages/@luke-ui/react/src/icon/icon.stories.tsx @@ -5,7 +5,7 @@ import { vars } from '@luke-ui/react/theme'; import { tokenKeys, tokens } from '@luke-ui/react/tokens'; import type { CSSProperties } from 'react'; import { expect } from 'storybook/test'; -import preview from '../.storybook/preview.js'; +import preview from '../../.storybook/preview.js'; const meta = preview.meta({ component: Icon, diff --git a/packages/@luke-ui/react/src/link.docs.mdx b/packages/@luke-ui/react/src/link/link.docs.mdx similarity index 100% rename from packages/@luke-ui/react/src/link.docs.mdx rename to packages/@luke-ui/react/src/link/link.docs.mdx diff --git a/packages/@luke-ui/react/src/link.stories.tsx b/packages/@luke-ui/react/src/link/link.stories.tsx similarity index 97% rename from packages/@luke-ui/react/src/link.stories.tsx rename to packages/@luke-ui/react/src/link/link.stories.tsx index f1b4cb4d..579a22ad 100644 --- a/packages/@luke-ui/react/src/link.stories.tsx +++ b/packages/@luke-ui/react/src/link/link.stories.tsx @@ -2,7 +2,7 @@ import type { LinkProps } from '@luke-ui/react/link'; import { Link } from '@luke-ui/react/link'; import type { CSSProperties } from 'react'; import { expect } from 'storybook/test'; -import preview from '../.storybook/preview.js'; +import preview from '../../.storybook/preview.js'; const meta = preview.meta({ component: Link, diff --git a/packages/@luke-ui/react/src/loading-spinner.docs.mdx b/packages/@luke-ui/react/src/loading-spinner/loading-spinner.docs.mdx similarity index 100% rename from packages/@luke-ui/react/src/loading-spinner.docs.mdx rename to packages/@luke-ui/react/src/loading-spinner/loading-spinner.docs.mdx diff --git a/packages/@luke-ui/react/src/loading-spinner.stories.tsx b/packages/@luke-ui/react/src/loading-spinner/loading-spinner.stories.tsx similarity index 96% rename from packages/@luke-ui/react/src/loading-spinner.stories.tsx rename to packages/@luke-ui/react/src/loading-spinner/loading-spinner.stories.tsx index 55aa698b..9d242fcd 100644 --- a/packages/@luke-ui/react/src/loading-spinner.stories.tsx +++ b/packages/@luke-ui/react/src/loading-spinner/loading-spinner.stories.tsx @@ -3,7 +3,7 @@ import { LoadingSpinner } from '@luke-ui/react/loading-spinner'; import { tokenKeys, tokens } from '@luke-ui/react/tokens'; import type { CSSProperties } from 'react'; import { expect } from 'storybook/test'; -import preview from '../.storybook/preview.js'; +import preview from '../../.storybook/preview.js'; const meta = preview.meta({ component: LoadingSpinner, diff --git a/packages/@luke-ui/react/src/numeral/index.tsx b/packages/@luke-ui/react/src/numeral/index.tsx index 3a843e9a..a03fe9d9 100644 --- a/packages/@luke-ui/react/src/numeral/index.tsx +++ b/packages/@luke-ui/react/src/numeral/index.tsx @@ -80,7 +80,7 @@ function validateProps(props: NumeralProps) { const numeralFormatCache = new Map(); function getCachedNumberFormat(locale: Intl.LocalesArgument, options: Intl.NumberFormatOptions) { - const key = `${locale}:${JSON.stringify(options)}`; + const key = `${String(locale)}:${JSON.stringify(options)}`; let cached = numeralFormatCache.get(key); if (!cached) { cached = new Intl.NumberFormat(locale, options); diff --git a/packages/@luke-ui/react/src/numeral/numeral.docs.mdx b/packages/@luke-ui/react/src/numeral/numeral.docs.mdx new file mode 100644 index 00000000..7df22542 --- /dev/null +++ b/packages/@luke-ui/react/src/numeral/numeral.docs.mdx @@ -0,0 +1,82 @@ +`Numeral` formats a number and renders it with `Text`. Formatting is done via +`Intl.NumberFormat` and respects the locale from React Aria's `I18nProvider`. + +## Import + +```ts +import { Numeral } from '@luke-ui/react/numeral'; +``` + +## Usage + +```tsx + +``` + +## Formats + +`format` is derived from `currency` and `unit` when omitted. Pass it explicitly +for `'percent'` or `'decimal'` without a currency or unit. + +```tsx + +``` + +```tsx + +``` + +```tsx + +``` + +```tsx + +``` + +## Compact notation + +```tsx + +``` + +```tsx + +``` + +## Precision + +Pass a number for fixed fraction digits, or a `[min, max]` tuple for a range. + +```tsx + +``` + +```tsx + +``` + +## Constraints + +`Numeral` throws in development when: + +- `currency` and `unit` are both provided. +- `format="currency"` is used without a `currency` code. +- `format="unit"` is used without a `unit` value. +- `precision` is not a non-negative integer or valid `[min, max]` tuple. + +## Props + +| Prop | Type | Default | Notes | +| --------------- | ----------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------- | +| `value` | `number` | - | Required. Number to format. | +| `abbreviate` | `boolean \| 'long'` | `false` | Compact notation. `true` = short (`1.2K`), `'long'` = long. | +| `currency` | `string` | - | ISO 4217 code (`AUD`, `USD`, `EUR`). Sets `format="currency"`. | +| `format` | `'decimal' \| 'percent' \| 'currency' \| 'unit'` | Derived | Derived from `currency`/`unit` when omitted. | +| `formatOptions` | `Intl.NumberFormatOptions` | - | Additional overrides forwarded to `Intl.NumberFormat`. | +| `locale` | `Intl.LocalesArgument` | Runtime locale | Overrides locale from `I18nProvider` context. | +| `precision` | `number \| [number, number]` | Locale default | Fixed fraction digits or `[min, max]` range. | +| `textAlign` | `TextProps['textAlign']` | `'end'` | Right-aligned by default for numeric columns. | +| `unit` | `Intl.NumberFormatOptions['unit']` | - | Unit identifier (`kilometer`, `liter`). Sets `format="unit"`. | +| `variant` | `'diagonal-fractions' \| 'ordinal' \| 'slashed-zero' \| 'tabular-nums'` | `'tabular-nums'` | Numeric glyph rendering mode. | +| `...rest` | `TextProps` | - | Forwarded to underlying `Text` (except `children`). | diff --git a/packages/@luke-ui/react/src/numeral.stories.tsx b/packages/@luke-ui/react/src/numeral/numeral.stories.tsx similarity index 98% rename from packages/@luke-ui/react/src/numeral.stories.tsx rename to packages/@luke-ui/react/src/numeral/numeral.stories.tsx index 53a1a2e2..fc4df8d4 100644 --- a/packages/@luke-ui/react/src/numeral.stories.tsx +++ b/packages/@luke-ui/react/src/numeral/numeral.stories.tsx @@ -4,7 +4,7 @@ import { Numeral } from '@luke-ui/react/numeral'; import { Text } from '@luke-ui/react/text'; import type { CSSProperties } from 'react'; import { expect } from 'storybook/test'; -import preview from '../.storybook/preview.js'; +import preview from '../../.storybook/preview.js'; const meta = preview.meta({ component: Numeral, diff --git a/packages/@luke-ui/react/src/text-field/index.tsx b/packages/@luke-ui/react/src/text-field/index.tsx index 962e28e0..82488833 100644 --- a/packages/@luke-ui/react/src/text-field/index.tsx +++ b/packages/@luke-ui/react/src/text-field/index.tsx @@ -4,11 +4,11 @@ import type { TextFieldProps as RacTextFieldProps, } from 'react-aria-components/TextField'; import { TextField as RacTextField } from 'react-aria-components/TextField'; -import type { FieldErrorProps } from '../field/error.js'; -import { Field } from '../field/index.js'; -import type { FieldNecessityIndicator } from '../field/label.js'; -import type { TextInputSize } from '../text-input/index.js'; -import { TextInput } from '../text-input/index.js'; +import type { FieldErrorProps } from '../field/primitive/error.js'; +import { Field } from '../field/primitive/index.js'; +import type { FieldNecessityIndicator } from '../field/primitive/label.js'; +import type { TextInputSize } from './primitive/index.js'; +import { TextInput } from './primitive/index.js'; /** Props for the composed text field. */ export interface TextFieldProps extends Omit { diff --git a/packages/@luke-ui/react/src/text-input/index.tsx b/packages/@luke-ui/react/src/text-field/primitive/index.tsx similarity index 95% rename from packages/@luke-ui/react/src/text-input/index.tsx rename to packages/@luke-ui/react/src/text-field/primitive/index.tsx index b7207dc2..b9a529e7 100644 --- a/packages/@luke-ui/react/src/text-input/index.tsx +++ b/packages/@luke-ui/react/src/text-field/primitive/index.tsx @@ -4,8 +4,8 @@ import { Group as RacGroup } from 'react-aria-components/Group'; import type { InputProps as RacInputProps } from 'react-aria-components/Input'; import { Input as RacInput } from 'react-aria-components/Input'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; -import * as styles from '../recipes/text-input.css.js'; -import { cx } from '../utils/index.js'; +import * as styles from '../../recipes/text-input.css.js'; +import { cx } from '../../utils/index.js'; interface TextInputVariantProps extends NonNullable {} diff --git a/packages/@luke-ui/react/src/text-field.docs.mdx b/packages/@luke-ui/react/src/text-field/text-field.docs.mdx similarity index 61% rename from packages/@luke-ui/react/src/text-field.docs.mdx rename to packages/@luke-ui/react/src/text-field/text-field.docs.mdx index bf5bdbd3..e1c8f1fc 100644 --- a/packages/@luke-ui/react/src/text-field.docs.mdx +++ b/packages/@luke-ui/react/src/text-field/text-field.docs.mdx @@ -17,17 +17,28 @@ import { TextField } from '@luke-ui/react/text-field'; /> ``` -## API Shape +When visual context already communicates purpose, omit `label` and provide an +accessible name with `aria-label` or `aria-labelledby` on the field itself. -This component follows Spectrum-style top-level props and forwards React Aria -`TextField` props directly. +```tsx + +``` + +## Required fields + +Use `isRequired` and `necessityIndicator` to communicate mandatory fields. +`'icon'` renders a visual required marker; `'label'` appends "(required)" to the +label text. + +```tsx + +``` -- Root props like `name`, `isRequired`, `isInvalid`, `validationBehavior`, and - `className` are passed to React Aria `TextField`. -- Input chrome props `size`, `placeholder`, `adornmentStart`, `adornmentEnd`, - and `inputClassName` are passed to internal `TextInput`. +```tsx + +``` -## Forms Integration +## Validation ```tsx import { Form } from 'react-aria-components'; @@ -41,6 +52,30 @@ import { Form } from 'react-aria-components'; ; ``` +## Adornments + +Use `adornmentStart` and `adornmentEnd` to place non-editable content inside +the input chrome. Adornments accept any `ReactNode`; you are responsible for +semantics if adornments are interactive. + +```tsx +import { Icon } from '@luke-ui/react/icon'; + +} +/>; +``` + +```tsx + +``` + +```tsx + +``` + ## Composed `TextField` Props | Prop | Type | Default | Notes | @@ -56,7 +91,12 @@ import { Form } from 'react-aria-components'; | `inputClassName` | `InputProps['className']` | - | Class name for the internal input element. | | `...rest` | `TextFieldProps` (RAC) | - | Forwarded to React Aria `TextField`. | -## Size Conflict Rule +## Size + +`size` controls height and typography. The HTML numeric `` attribute +is intentionally omitted; `size` is reserved for the design-system variant. -The HTML numeric input `size` attribute is intentionally omitted from this -component API. `size` is reserved for the design-system variant. +| Value | Description | +| ---------- | --------------------- | +| `'small'` | Compact input height. | +| `'medium'` | Default input height. | diff --git a/packages/@luke-ui/react/src/text-field.stories.tsx b/packages/@luke-ui/react/src/text-field/text-field.stories.tsx similarity index 98% rename from packages/@luke-ui/react/src/text-field.stories.tsx rename to packages/@luke-ui/react/src/text-field/text-field.stories.tsx index 04d5790f..ae313ef6 100644 --- a/packages/@luke-ui/react/src/text-field.stories.tsx +++ b/packages/@luke-ui/react/src/text-field/text-field.stories.tsx @@ -3,7 +3,7 @@ import { TextField } from '@luke-ui/react/text-field'; import type { CSSProperties } from 'react'; import { Form } from 'react-aria-components/Form'; import { expect, userEvent, within } from 'storybook/test'; -import preview from '../.storybook/preview.js'; +import preview from '../../.storybook/preview.js'; const meta = preview.meta({ component: TextField, diff --git a/packages/@luke-ui/react/src/text-input.docs.mdx b/packages/@luke-ui/react/src/text-input.docs.mdx deleted file mode 100644 index a996fb72..00000000 --- a/packages/@luke-ui/react/src/text-input.docs.mdx +++ /dev/null @@ -1,43 +0,0 @@ -`TextInput` is a primitive input control built from React Aria `Group` + `Input`. - -## Import - -```ts -import { TextInput } from '@luke-ui/react/text-input'; -``` - -## Usage - -Use `TextInput` inside React Aria `TextField` composition. - -```tsx -import { Field } from '@luke-ui/react/field'; -import { TextInput } from '@luke-ui/react/text-input'; -import { TextField } from 'react-aria-components'; - - - - - -; -``` - -## Adornments - -```tsx -} adornmentEnd="⌘K" /> -``` - -Adornments accept any `ReactNode`. Consumers are responsible for semantics if -adornments are interactive. - -## Size Behavior - -`TextInput` uses design-system `size` values. - -| Prop | Type | Default | Notes | -| ---- | --------------------- | ---------- | --------------------------------------- | -| size | `'small' \| 'medium'` | `'medium'` | Controls control height and typography. | - -The HTML numeric `` attribute is intentionally omitted. On this API, -`size` is a design-system visual variant. diff --git a/packages/@luke-ui/react/src/text-input.stories.tsx b/packages/@luke-ui/react/src/text-input.stories.tsx deleted file mode 100644 index 2dd65721..00000000 --- a/packages/@luke-ui/react/src/text-input.stories.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { Field } from '@luke-ui/react/field'; -import { TextInput } from '@luke-ui/react/text-input'; -import type { CSSProperties } from 'react'; -import { TextField as RacTextField } from 'react-aria-components/TextField'; -import { expect } from 'storybook/test'; -import preview from '../.storybook/preview.js'; - -const meta = preview.meta({ - component: TextInput, - tags: ['forms'], - title: 'Forms/TextInput (Primitive)', -}); - -const stackStyle = { - display: 'flex', - flexDirection: 'column', - gap: '1rem', - maxInlineSize: '24rem', -} as const satisfies CSSProperties; - -/** - * The `` component is most commonly used in a form context, where - * you would nest it inside a Field to get a label, validation message and - * spacing out of the box. - */ -export const Usage = meta.story({ - play: async ({ canvas }) => { - await expect(canvas.getByLabelText('Email')).toBeInTheDocument(); - }, - render: () => ( - - - - - - ), -}); - -/** - * The text input will by default render as ``, but also - * supports these types: `password`, `email`, `number`, `tel`, and `url`. When - * suitable, these are important to use for various browsers' auto-fill - * functionality e.g. - */ -export const Type = meta.story({ - render: () => ( -
- - - - - - -
- ), -}); - -/** - * The input supports two sizes, `small` and `medium` (default). Prefer using - * size consistently with other form controls (a button e.g.) that support the - * same sizes. - */ -export const Size = meta.story({ - render: () => ( -
- - -
- ), -}); - -/** - * Use `adornmentStart` or `adornmentEnd` prop when you need a prefix/suffix to - * the input, in text string format. - */ -export const Adornment = meta.story({ - render: () => ( -
- - -
- ), -}); - -/** - * Use the `placeholder` prop to provide a hint or example of what the user can - * enter. - * - * Avoid using placeholder text as a replacement for labels or descriptions. It - * should not contain any crucial information, as it disappears when the user - * starts typing. - */ -export const Placeholder = meta.story({ - render: () => ( - - - - - - ), -}); - -/** - * Use the `disabled` prop when the user shouldn't be able to manipulate the - * value of the input. - */ -export const Disabled = meta.story({ - render: () => , -}); - -/** - * Use the `invalid` prop to communicate that validation has failed for this - * particular text input. - - - */ -export const Invalid = meta.story({ - render: () => , -}); diff --git a/packages/@luke-ui/react/src/text.docs.mdx b/packages/@luke-ui/react/src/text/text.docs.mdx similarity index 67% rename from packages/@luke-ui/react/src/text.docs.mdx rename to packages/@luke-ui/react/src/text/text.docs.mdx index 386058a8..d9c61db1 100644 --- a/packages/@luke-ui/react/src/text.docs.mdx +++ b/packages/@luke-ui/react/src/text/text.docs.mdx @@ -50,50 +50,6 @@ import { Text } from '@luke-ui/react/text'; Long content truncated to one line. ``` -## Composed typography - -```ts -import { Emoji, Heading, Numeral } from '@luke-ui/react/heading'; -``` - -```tsx -Section title -``` - -```tsx - -``` - -```tsx - -``` - -```tsx - - - -``` - -```tsx - - -``` - -## `Numeral` props - -| Prop | Type | Default | Notes | -| --------------- | ----------------------------------------------------------------------- | ---------------- | -------------------------------------------- | -| `abbreviate` | `boolean \| 'long'` | `false` | Enables compact notation. | -| `currency` | `string` | - | ISO 4217 code (e.g. `AUD`, `USD`, `EUR`). | -| `format` | `'decimal' \| 'percent' \| 'currency' \| 'unit'` | Derived | Derives from `currency`/`unit` when omitted. | -| `formatOptions` | `Intl.NumberFormatOptions` | - | Additional formatting overrides. | -| `locale` | `Intl.LocalesArgument` | Runtime locale | Locale passed to `Intl.NumberFormat`. | -| `precision` | `number \| [number, number]` | Locale default | Sets fixed or min/max fraction digits. | -| `textAlign` | `TextProps['textAlign']` | `'end'` | Right-align by default for numeric columns. | -| `unit` | `Intl.NumberFormatOptions['unit']` | - | Unit identifier (e.g. `kilometer-per-hour`). | -| `value` | `number` | - | Numeric value to format. | -| `variant` | `'diagonal-fractions' \| 'ordinal' \| 'slashed-zero' \| 'tabular-nums'` | `'tabular-nums'` | Numeric glyph rendering mode. | - ## Props | Prop | Type | Default | Notes | diff --git a/packages/@luke-ui/react/src/text.stories.tsx b/packages/@luke-ui/react/src/text/text.stories.tsx similarity index 99% rename from packages/@luke-ui/react/src/text.stories.tsx rename to packages/@luke-ui/react/src/text/text.stories.tsx index 4ff754d0..b1df7444 100644 --- a/packages/@luke-ui/react/src/text.stories.tsx +++ b/packages/@luke-ui/react/src/text/text.stories.tsx @@ -4,7 +4,7 @@ import { vars } from '@luke-ui/react/theme'; import { tokenKeys, tokens } from '@luke-ui/react/tokens'; import type { CSSProperties } from 'react'; import { expect } from 'storybook/test'; -import preview from '../.storybook/preview.js'; +import preview from '../../.storybook/preview.js'; const meta = preview.meta({ component: Text, diff --git a/packages/@luke-ui/react/src/tokens-vars.stories.tsx b/packages/@luke-ui/react/src/tokens/tokens-vars.stories.tsx similarity index 99% rename from packages/@luke-ui/react/src/tokens-vars.stories.tsx rename to packages/@luke-ui/react/src/tokens/tokens-vars.stories.tsx index 3b05955f..e4cda43c 100644 --- a/packages/@luke-ui/react/src/tokens-vars.stories.tsx +++ b/packages/@luke-ui/react/src/tokens/tokens-vars.stories.tsx @@ -18,7 +18,7 @@ import { import ColorJs from 'colorjs.io'; import type { CSSProperties } from 'react'; import { expect } from 'storybook/test'; -import preview from '../.storybook/preview.js'; +import preview from '../../.storybook/preview.js'; type StoryTokenGroup = { $type: string; diff --git a/packages/@luke-ui/react/tsdown.config.ts b/packages/@luke-ui/react/tsdown.config.ts index 53b56277..b8e51b55 100644 --- a/packages/@luke-ui/react/tsdown.config.ts +++ b/packages/@luke-ui/react/tsdown.config.ts @@ -45,15 +45,7 @@ export default defineConfig((options) => ({ clean: false, dts: true, entry: { - '*': [ - 'src/*/index.tsx', - 'src/*/index.ts', - '!src/style-helpers/index.ts', - '!src/types/index.ts', - '!src/typography/index.ts', - ], - 'button/primitive': 'src/button/primitive.tsx', - 'field/primitive': 'src/field/primitive.tsx', + '*': ['src/*/index.tsx', 'src/*/index.ts', 'src/*/primitive/index.tsx'], }, exports: { customExports: { diff --git a/packages/@luke-ui/react/vitest.config.ts b/packages/@luke-ui/react/vitest.config.ts index 4d527ee4..0a644ec0 100644 --- a/packages/@luke-ui/react/vitest.config.ts +++ b/packages/@luke-ui/react/vitest.config.ts @@ -10,6 +10,9 @@ const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); const configDir = path.join(dirname, '.storybook'); export default defineConfig({ + optimizeDeps: { + include: ['@vanilla-extract/recipes/createRuntimeFn'], + }, test: { projects: [ { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82ac1758..1011ec4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: apps/docs: dependencies: + '@fumadocs/story': + specifier: 0.0.14 + version: 0.0.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-ui@16.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.158.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.11.0(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.4))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@luke-ui/react': specifier: workspace:* version: link:../../packages/@luke-ui/react @@ -137,6 +140,9 @@ importers: react-dom: specifier: 'catalog:' version: 19.2.5(react@19.2.5) + ts-morph: + specifier: ^28.0.0 + version: 28.0.0 vite: specifier: 'catalog:' version: 8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) @@ -174,9 +180,6 @@ importers: typescript: specifier: ^6.0.3 version: 6.0.3 - vite-tsconfig-paths: - specifier: ^6.1.1 - version: 6.1.1(typescript@6.0.3)(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/react: dependencies: @@ -1070,6 +1073,17 @@ packages: '@formatjs/intl-localematcher@0.8.4': resolution: {integrity: sha512-J51dAnynnqJdVUEXidHoIWn+qYve+yNQEgmFk9Dyfr3p0okzm+5QhQ+9QmsMz08+BeWTVpc1HadIiLfZmRYbAQ==} + '@fumadocs/story@0.0.14': + resolution: {integrity: sha512-m75t+s3MlL+7a2V+g/Mtd5futc2u9Z5JSUvInUxiXG3CvyIWOoqFPuz9GJGzbsp6RQtxcNGGAyvuSfAeyelmyA==} + peerDependencies: + '@types/react': '*' + fumadocs-ui: 16.x.x + react: ^19.2.0 + react-dom: ^19.2.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@fumadocs/tailwind@0.0.2': resolution: {integrity: sha512-4JrTJLRDKKdFF3gy07rAsakqGr17/0cJE042B1icCmMRrPA4a38cjR1qd4EqUiDJ+fzM0wgVN9QYiqds3HB2rg==} peerDependencies: @@ -1078,6 +1092,16 @@ packages: tailwindcss: optional: true + '@fumari/stf@1.0.5': + resolution: {integrity: sha512-O9UQIbV15ePV5vUgceCcaMDuBRYfrSuogDEY5E//CmrbIiWJ1Ji5VgGHHH0L0HQuRr5riUJuAEr9lYIvJl9OyQ==} + peerDependencies: + '@types/react': '*' + react: ^19.2.0 + react-dom: ^19.2.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -2500,6 +2524,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -3244,6 +3281,9 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@ts-morph/common@0.29.0': + resolution: {integrity: sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==} + '@turbo/darwin-64@2.9.6': resolution: {integrity: sha512-X/56SnVXIQZBLKwniGTwEQTGmtE5brSACnKMBWpY3YafuxVYefrC2acamfjgxP7BG5w3I+6jf0UrLoSzgPcSJg==} cpu: [x64] @@ -3749,6 +3789,9 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -4390,9 +4433,6 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -5243,6 +5283,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -5847,15 +5890,8 @@ packages: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} - tsconfck@3.1.6: - resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} - engines: {node: ^18 || >=20} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + ts-morph@28.0.0: + resolution: {integrity: sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==} tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} @@ -6117,11 +6153,6 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite-tsconfig-paths@6.1.1: - resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} - peerDependencies: - vite: '*' - vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -7064,12 +7095,35 @@ snapshots: dependencies: '@formatjs/fast-memoize': 3.1.2 + '@fumadocs/story@0.0.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-ui@16.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.158.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.11.0(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.4))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@fumari/stf': 1.0.5(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + class-variance-authority: 0.7.1 + fumadocs-ui: 16.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.158.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.11.0(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.4) + lucide-react: 1.11.0(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + tailwind-merge: 3.5.0 + ts-morph: 28.0.0 + optionalDependencies: + '@types/react': 19.2.14 + transitivePeerDependencies: + - '@types/react-dom' + '@fumadocs/tailwind@0.0.2(tailwindcss@4.2.4)': dependencies: postcss-selector-parser: 7.1.1 optionalDependencies: tailwindcss: 4.2.4 + '@fumari/stf@1.0.5(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -8110,6 +8164,35 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.5)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) @@ -8827,6 +8910,12 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 + '@ts-morph/common@0.29.0': + dependencies: + minimatch: 10.2.4 + path-browserify: 1.0.1 + tinyglobby: 0.2.16 + '@turbo/darwin-64@2.9.6': optional: true @@ -9438,6 +9527,8 @@ snapshots: clsx@2.1.1: {} + code-block-writer@13.0.3: {} + collapse-white-space@2.1.0: {} color-convert@2.0.1: @@ -10116,8 +10207,6 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - globrex@0.1.2: {} - graceful-fs@4.2.11: {} h3@2.0.1-rc.11(crossws@0.4.4(srvx@0.10.1)): @@ -11370,6 +11459,8 @@ snapshots: dependencies: entities: 6.0.1 + path-browserify@1.0.1: {} + path-exists@4.0.0: {} path-is-inside@1.0.2: {} @@ -12067,9 +12158,10 @@ snapshots: ts-dedent@2.2.0: {} - tsconfck@3.1.6(typescript@6.0.3): - optionalDependencies: - typescript: 6.0.3 + ts-morph@28.0.0: + dependencies: + '@ts-morph/common': 0.29.0 + code-block-writer: 13.0.3 tsconfig-paths@4.2.0: dependencies: @@ -12289,16 +12381,6 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@6.1.1(typescript@6.0.3)(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)): - dependencies: - debug: 4.4.3 - globrex: 0.1.2 - tsconfck: 3.1.6(typescript@6.0.3) - 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) - transitivePeerDependencies: - - supports-color - - typescript - vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 diff --git a/turbo.json b/turbo.json index fcb4836c..77f0ae37 100644 --- a/turbo.json +++ b/turbo.json @@ -20,6 +20,7 @@ }, "build": { "dependsOn": ["^build", "generate"], + "env": ["VITE_BASE_URL"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**", ".output/**", ".source/**"] },