···1010- Scaffold components non-interactively:
1111 `pnpm run generate:component --args <name> <atom|composed> <docs-group> <recipe|none>`.
1212- When you change code, update or delete the docs that describe it in the same change — comments,
1313- JSDoc, `.docs.md`, `README.md`, and `docs/*.md`. See
1313+ JSDoc, MDX files in `apps/docs/content/docs/`, `README.md`, and `docs/*.md`. See
1414 [docs/CONVENTIONS.md](docs/CONVENTIONS.md#keeping-docs-current).
1515+1616+## Dev loop
1717+1818+- Run `pnpm run check` from the repo root to verify changes before committing.
1919+- Component prose now lives in MDX files in `apps/docs/content/docs/`, not `.docs.md` files in the
2020+ package. Update the relevant MDX page in the same change as the component code.
2121+- Do not add or edit `.docs.md` files in `packages/@luke-ui/react/src/`.
+27-28
CONTEXT.md
···2323**Primitive**.
24242525**Component creation**: The act of adding the public surface a new Atom or Composed component needs
2626-to exist consistently. That includes source, stories, package docs, hosted docs, recipes when
2727-needed, and generated exports.
2626+to exist consistently. That includes source, stories, hosted docs, recipes when needed, and
2727+generated exports.
28282929## Relationships
30303131- A **Composed** component is built from one or more **Atoms** or **Primitives**.
3232-- A **Primitive** is documented in package docs so library authors and agents can find it. It is
3333- omitted from the hosted docs primary navigation and index.
3232+- A **Primitive** is documented in the hosted docs app so library authors and agents can find it. It
3333+ is omitted from the hosted docs primary navigation and index.
3434- The composed `Field` is a **Primitive** by audience, even though it composes other components. It
3535 exists for library authors building `TextField`, `ComboboxField`, and similar components.
3636-- An **Atom** or **Composed** component gets a hosted docs page and a primary package-doc entry.
3636+- An **Atom** or **Composed** component gets a hosted docs page.
37373838-## Package docs shape
3838+## Hosted docs shape
39394040-Each `src/<component>/<component>.docs.md` follows the section order from
4141-[ADR-0006](docs/adr/0006-docs-md-structure-standard.md):
4040+Each component docs page at `apps/docs/content/docs/components/<group>/<component>.mdx` follows the
4141+section order from [ADR-0006](docs/adr/0006-docs-md-structure-standard.md) (now superseded by
4242+ADR-0007; the section ordering still applies):
42434344- Usage lead-in with no explicit `## Usage` heading.
4445- `## Best Practices` table.
···4647- `## Accessibility` when the component has a user-facing accessibility contract.
4748- Cross-reference sections last.
48494949-All prose is authored in the package (`src/<component>/<component>.docs.md`). `apps/docs` MDX files
5050-only wire the page together with frontmatter, an interactive demo, and an `<include>` for the
5151-generated package doc. Full rationale is in [ADR-0006](docs/adr/0006-docs-md-structure-standard.md).
5050+All prose is authored in the hosted docs app (`apps/docs/content/docs/`). The package no longer
5151+ships generated docs on npm. Full rationale is in
5252+[ADR-0007](docs/adr/0007-docs-moved-to-hosted-app.md).
52535354## Docs rule
54555555-Two doc surfaces serve two audiences:
5656+The **hosted docs app** (`apps/docs`) is the primary docs surface. It serves both app developers and
5757+library authors. The package README links to it.
56585757-- **Hosted docs** (`apps/docs`): for app developers. They document components an app developer can
5858- drop into a UI.
5959-- **Package docs** (shipped on npm under `packages/@luke-ui/react/docs/`): for anyone reading the
6060- package off npm, including library authors and coding agents. They document every public export
6161- path because each one is reachable through `package.json#exports`.
5959+| Tier | Hosted docs? | Notes |
6060+| --------- | ----------------- | ------------------------------------------------ |
6161+| Atom | yes (primary nav) | App-developer-facing |
6262+| Composed | yes (primary nav) | App-developer-facing |
6363+| Primitive | yes (specialist) | Listed in a "Library authors / advanced" section |
62646363-| Tier | Hosted docs? | Package docs? |
6464-| --------- | ----------------- | ---------------------- |
6565-| Atom | yes (primary nav) | yes (primary index) |
6666-| Composed | yes (primary nav) | yes (primary index) |
6767-| Primitive | no | yes (specialist index) |
6565+**Specialist, not noise.** Primitive pages exist in the hosted docs so library authors and agents
6666+can find them. They are listed in a separate, de-emphasised "Library authors / advanced" section,
6767+never mixed into the primary index alongside atoms and composed components. The goal is reachability
6868+without crowding the main path. Do not document anything that is not part of the public API.
68696969-**Specialist, not noise.** Primitive pages exist in package docs so library authors and agents can
7070-find them. They are listed in a separate, de-emphasised "Library authors / advanced" section in
7171-`README.md` and `llms.txt`, never mixed into the primary index alongside atoms and composed
7272-components. The goal is reachability without crowding the main path. Do not document anything that
7373-is not part of the public API.
7070+Package docs are no longer shipped on npm.
74717572## Decisions
7673···7875- [ADR-0002](docs/adr/0002-primitive-package-path-convention.md): Primitive kits are exported at
7976 `[composed]/primitive`
8077- [ADR-0003](docs/adr/0003-package-docs-surface.md): Package docs are a separate AI-native surface
7878+ (superseded by ADR-0007)
8179- [ADR-0004](docs/adr/0004-styling-utilities-public-api.md): Styling utilities public API
8280- [ADR-0006](docs/adr/0006-docs-md-structure-standard.md): Standard structure for `.docs.md` prose
8383- files
8181+ files (superseded by ADR-0007)
8282+- [ADR-0007](docs/adr/0007-docs-moved-to-hosted-app.md): Docs moved to the hosted docs app
···3939 `Icon`, `Heading`, `Numeral`, `Emoji`, or `LoadingSpinner`. Atoms get hosted docs pages.
4040- **Composed**: an app-developer-facing pattern built from atoms or primitives, such as `Button`,
4141 `IconButton`, `TextField`, or `ComboboxField`. Composed components get hosted docs pages.
4242-- **Primitive**: a building block for library authors. Primitives are documented in package docs,
4343- but not in hosted docs. A primitive may be a single file or a multi-file kit.
4242+- **Primitive**: a building block for library authors. Primitives are documented in the hosted docs
4343+ app under a de-emphasised "Library authors" section, but not in the primary navigation. A
4444+ primitive may be a single file or a multi-file kit.
44454546## Package paths
4647···60616162## Docs
62636363-`.docs.md` files follow the structure in [ADR-0006](adr/0006-docs-md-structure-standard.md). Humans
6464-and coding agents both read these docs, so optimise for clarity, not only brevity.
6464+Component docs pages follow the structure in
6565+[ADR-0006](../docs/adr/0006-docs-md-structure-standard.md). ADR-0007 supersedes that ADR, but the
6666+section ordering still applies. Humans and coding agents both read these docs, so optimise for
6767+clarity, not only brevity.
65686669Headings use sentence case: capitalise only the first word and proper nouns.
6770···75787679Docs must stay factually accurate: no doc should state something false about the code — a path,
7780command, script, export, type, code snippet, or cross-reference that has since changed or been
7878-removed. This covers everything a human writes (comments, JSDoc, `.docs.md` prose, `README.md`,
7979-`docs/*.md`), not generated output; when generated docs are wrong, fix the authored source, not the
8080-generated file.
8181+removed. This covers everything a human writes (comments, JSDoc, MDX prose in
8282+`apps/docs/content/docs/`, `README.md`, `docs/*.md`), not generated output; when generated docs are
8383+wrong, fix the authored source, not the generated file.
81848285- Update or delete the docs that describe code in the same change as the code. Most rot is a doc
8386 that outlived the change that should have touched it.
···8992- Reference stable things. Don't pin prose to volatile details like line numbers, generated class
9093 names, or exact command output; reference the durable path, command, or heading instead.
91949292-## Fumadocs stories
9595+## Fumadocs examples
93969494-Each component's interactive hosted-docs demo lives in one file:
9595-`apps/docs/src/<component>/<component>.story.tsx`.
9797+Each component's interactive hosted-docs demo lives in example modules under
9898+`apps/docs/src/examples/<component>/`. An example module exports `meta` with a title and
9999+description, and a default component that renders the example.
961009797-The story uses `@fumadocs/story/vite/client`, which is registered as a Vite plugin in
9898-`apps/docs/vite.config.ts`.
101101+Use the `<Example>` component in an MDX page:
99102100100-Define a narrow `<Component>StoryProps` type with `Pick<<Component>Props, 'a' | 'b'>`. Include only
101101-props that make useful hosted controls. Drop event handlers, refs, escape hatches such as
102102-`className` and `style`, and obscure ARIA props.
103103+```tsx
104104+<Example component="button" name="tones" />
105105+```
103106104104-Control order follows the key order inside `Pick`, not the declaration order in `<Component>Props`.
105105-`@fumadocs/story` reads the resolved type with `ts-morph` and preserves the pick order, so list keys
106106-in the order they should appear in the panel.
107107+`<Example>` renders a live preview and a "Code" tab with the example source.
107108108108-Render the real component through a small `<Component>Playground` wrapper inside the shared
109109-`StoryWrapper` from `../lib/story-wrapper`. Pass it directly to
110110-`defineStory({ Component, args: { initial } })`.
109109+Keep example content aligned with the MDX feature sections. If you add a feature section for a prop
110110+or pattern, add or update an example in the same change.
111111112112-Generic components should fix the generic to one concrete sample type inside the playground. See
113113-`combobox-field.story.tsx`.
112112+Initial values should be short and legible. Do not use lorem ipsum.
114113115115-Keep story props aligned with `.docs.md` feature sections. If you add a feature section for a prop,
116116-add that prop to the story `Pick` in the same change.
117117-118118-Initial values should be short and legible, like the `.docs.md` examples. Do not use lorem ipsum.
119119-120120-Hosted `.mdx` files are wiring only: frontmatter, an interactive demo, and an `<include>` for
121121-generated package docs. Do not add hand-authored component prose there.
114114+Hosted `.mdx` files contain the component prose, frontmatter, `<Example>` components, and an
115115+`<auto-type-table>` for the API reference. Do not add generated package docs.
+1-1
.github/workflows/check.yml
···4545 TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
4646 TURBO_SCM_HEAD: ${{ github.event.pull_request.head.sha }}
4747 run: |
4848- corepack pnpm exec turbo run check:format-root check:format check:lint check:types check:docs ${{ github.event_name == 'pull_request' && '--affected' || '' }}
4848+ corepack pnpm exec turbo run check:format-root check:format check:lint check:types ${{ github.event_name == 'pull_request' && '--affected' || '' }}
4949 # knip is repo-wide, but --affected only selects packages whose own files
5050 # changed, so it must run unscoped. Running it via turbo (rather than the
5151 # bare `knip` script) pulls in its generate dependencies first.
···11# Component tier taxonomy: Atom, Composed, Primitive
2233+> **Note:** The docs location detail is superseded by [ADR-0007](0007-docs-moved-to-hosted-app.md).
44+> Primitives are mentioned in prose on their parent component's hosted docs page (see, for example,
55+> the "Primitive kit" section of the Combobox Field docs), but do not yet have their own generated
66+> API reference.
77+38Luke UI classifies components into three tiers: **Atom**, **Composed**, and **Primitive**. The tier
44-decides whether the component appears in hosted docs and how it is described in source and package
99+decides whether the component appears in hosted docs and how it is described in source and hosted
510docs.
611712## Decision
+3
docs/adr/0003-package-docs-surface.md
···11# Package docs are a separate AI-native surface
2233+> **Superseded by [ADR-0007](0007-docs-moved-to-hosted-app.md).** Package docs are no longer shipped
44+> on npm. The hosted docs app is the primary docs surface.
55+36`@luke-ui/react` ships per-export documentation under `packages/@luke-ui/react/docs/`. Those docs
47are generated from JSDoc, TypeScript types, and authored prose. They are separate from the hosted
58Fumadocs site in `apps/docs`.
+3
docs/adr/0006-docs-md-structure-standard.md
···11# Standard structure for `.docs.md` prose files
2233+> **Superseded by [ADR-0007](0007-docs-moved-to-hosted-app.md).** Component prose now lives in
44+> `apps/docs/content/docs/**/*.mdx`. `.docs.md` files are no longer used.
55+36Every `src/<component>/<component>.docs.md` file follows the same section order. Readers can then
47predict where to find usage, props, accessibility notes, and cross-references regardless of which
58component they are reading.
+38
docs/adr/0007-docs-moved-to-hosted-app.md
···11+# Docs moved to the hosted docs app
22+33+Component documentation has moved out of `@luke-ui/react` and into the hosted docs app in
44+`apps/docs`. The hosted docs app is now the primary docs surface. The package README links to it,
55+and package docs are no longer shipped on npm.
66+77+This supersedes [ADR-0003](0003-package-docs-surface.md), which established a separate package-docs
88+surface, and [ADR-0006](0006-docs-md-structure-standard.md), which standardised the `.docs.md` prose
99+format.
1010+1111+## Decision
1212+1313+- All component prose lives in `apps/docs/content/docs/**/*.mdx`.
1414+- Component MDX pages are the authoritative docs for app developers and library authors.
1515+- API reference tables are generated from TypeScript types with `fumadocs-typescript`'s
1616+ `remarkAutoTypeTable` plugin.
1717+- Interactive examples live in `apps/docs/src/examples/<component>/` and render through the
1818+ `<Example>` component.
1919+- `llms.txt`, `llms-full.txt`, and `.md` per-page routes are provided by Fumadocs built-ins.
2020+- The `@luke-ui/react` package README links to the hosted docs.
2121+- The package no longer ships generated docs under `packages/@luke-ui/react/docs/` or includes them
2222+ in the npm `files` allowlist.
2323+2424+## Rejected options
2525+2626+We rejected keeping the two-surface split (hosted docs + package docs). Maintaining the package docs
2727+generator, virtual modules, and a separate prose source in `src/<component>/<component>.docs.md`
2828+created duplication and tooling overhead without reaching a meaningfully different audience. The
2929+same readers can use the hosted docs, the `llms.txt` index, and the per-page `.md` routes.
3030+3131+## Consequences
3232+3333+- Prose is co-located with the docs app instead of the component source.
3434+- Component docs no longer need a `.docs.md` file or an `<include>` of generated package docs.
3535+- The `generate:docs` and `check:docs` scripts in `@luke-ui/react` are removed.
3636+- The `@luke-ui/docs-tools` package and package-docs Vite plugins are removed.
3737+- The hosted docs app type-checks and builds the full docs surface.
3838+- ADR-0003 and ADR-0006 are superseded.
+2-1
packages/turbo-generators/config.ts
···11-import { DOC_GROUPS } from '@luke-ui/docs-tools/package-docs-catalog';
21import type { PlopTypes } from '@turbo/gen';
32import * as z from 'zod';
43import { applyComponentCreationPlan } from './src/apply-component-creation-plan.js';
···76const COMPONENT_NAME_RE = /^[A-Za-z][A-Za-z0-9-]*$/;
87const COMPONENT_TIERS = ['atom', 'composed'] as const;
98const COMPONENT_STYLING = ['none', 'recipe'] as const;
99+// Mirrors apps/docs/content/docs/components/*/meta.json — the pages listed there.
1010+const DOC_GROUPS = ['actions', 'feedback', 'forms', 'typography', 'visuals'] as const;
10111112const componentAnswersSchema = z.object({
1213 docsGroup: z.enum(DOC_GROUPS),
···13131414A component directory contains:
15151616-- `[component].docs.md`: authored usage guidance consumed by the docs generator
1716- `[component].stories.tsx`: Storybook stories that also serve as tests
1817- `index.tsx`: component implementation
1918- `primitive/`: optional primitive exports
···25242625Primitives exported from `*/primitive/` are building blocks for library authors. They are not
2726promoted in beginner app-developer navigation, but they are public API and should have enough
2828-generated documentation for power users and agents.
2727+documentation in the hosted docs app for power users and agents.
29283030-## Documentation generation
2929+## Documentation
31303232-JSDoc and TypeScript types drive generated docs under `docs/`.
3131+JSDoc and TypeScript types drive the docs app.
33323433When adding or modifying a component:
3534···4342 `isDisabled?: RACButtonProps['isDisabled']`.
4443- Do not re-declare every React Aria Components prop. Re-declare only props an app developer is
4544 likely to reach for.
4646-- Long-tail inherited props are covered by the generated "Extends" pointer.
4747-4848-## Generated docs
4949-5050-Generated docs are ignored by Git. After changing JSDoc or `.docs.md` prose, run:
5151-5252-```sh
5353-pnpm --filter @luke-ui/react generate:docs
5454-```
5555-5656-Use `pnpm --filter @luke-ui/react check:docs` as a smoke test that the generator is healthy. It is
5757-not a stale-file check because generated output is ignored.
5858-5959-## Dev loop
6060-6161-During `pnpm dev` or `turbo dev`, the docs app does two things:
6262-6363-1. Watches generated `docs/*.md` files and hot-reloads pages when they change.
6464-2. Watches `src/**/*.{ts,tsx,docs.md}` in the package and re-runs `generate:docs` on change,
6565- debounced by about 300ms.
6666-6767-The loop is the same for prose and JSDoc edits:
6868-6969-1. Edit `[component].docs.md`, `index.tsx` JSDoc, or prop types.
7070-2. Save.
7171-3. The generator re-runs, `docs/*.md` updates, and the page reloads.
7272-7373-If the generator fails during a mid-edit syntax error, the dev server logs the error and keeps
7474-running. The next successful save regenerates the docs.
4545+- Long-tail inherited props are covered by the docs app's "Extends" pointer.
+8-2
packages/@luke-ui/react/README.md
···23232424## Components and docs
25252626-This package ships per-export documentation under `docs/`. The full index is in
2727-[`docs/llms.txt`](./docs/llms.txt), which is readable by humans and AI agents.
2626+Full component documentation, interactive examples, and API reference are at
2727+[lukebennett88.github.io/luke-ui/docs](https://lukebennett88.github.io/luke-ui/docs).
2828+2929+AI agents can fetch documentation at:
3030+3131+- [llms.txt](https://lukebennett88.github.io/luke-ui/llms.txt) — component index
3232+- [llms-full.txt](https://lukebennett88.github.io/luke-ui/llms-full.txt) — full docs
3333+- Append `.md` to any docs URL for per-page markdown
28342935Components follow the
3036[three-tier taxonomy](https://github.com/lukebennett88/luke-ui/blob/main/docs/adr/0001-component-tier-taxonomy.md):
···11+import { Text } from '@luke-ui/react/text';
22+import type { JSX } from 'react';
33+44+export default function Alignment(): JSX.Element {
55+ return (
66+ <Text textAlign="end" fontVariantNumeric="tabular-nums" style={{ display: 'block' }}>
77+ 12121.21
88+ </Text>
99+ );
1010+}
+10
apps/docs/src/examples/text/transform.tsx
···11+import { Text } from '@luke-ui/react/text';
22+import type { JSX } from 'react';
33+44+export default function Transform(): JSX.Element {
55+ return (
66+ <Text textTransform="uppercase" textDecoration="underline">
77+ Emphasized text
88+ </Text>
99+ );
1010+}
+12
apps/docs/src/examples/text/truncation.tsx
···11+import { Text } from '@luke-ui/react/text';
22+import type { JSX } from 'react';
33+44+export default function Truncation(): JSX.Element {
55+ return (
66+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', maxInlineSize: '20rem' }}>
77+ <Text lineClamp={2}>Long content that should be line clamped to two lines.</Text>
88+ <Text lineClamp>Long content truncated to one line.</Text>
99+ <Text shouldDisableTrim>Untrimmed text spacing</Text>
1010+ </div>
1111+ );
1212+}
+15
apps/docs/src/examples/text/typography.tsx
···11+import { Text } from '@luke-ui/react/text';
22+import type { JSX } from 'react';
33+44+export default function Typography(): JSX.Element {
55+ return (
66+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
77+ <Text fontSize="h2" lineHeight="tight">
88+ Heading-like text
99+ </Text>
1010+ <Text color="critical" fontFamily="mono" fontWeight="bold">
1111+ Alert-like inline text
1212+ </Text>
1313+ </div>
1414+ );
1515+}
+10-4
apps/docs/src/routes/docs/$.tsx
···22import { createServerFn } from '@tanstack/react-start';
33import { staticFunctionMiddleware } from '@tanstack/start-static-server-functions';
44import { useFumadocsLoader } from 'fumadocs-core/source/client';
55+import { AutoTypeTable } from 'fumadocs-typescript/ui';
66+import { TypeTable } from 'fumadocs-ui/components/type-table';
57import { DocsLayout } from 'fumadocs-ui/layouts/docs';
68import { DocsBody, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
79import defaultMdxComponents from 'fumadocs-ui/mdx';
810import { Suspense } from 'react';
911import * as z from 'zod';
1012import browserCollections from '../../../.source/browser';
1313+import { ExampleBlock } from '../../components/example-block';
1114import { PageActions } from '../../components/page-actions';
1215import { baseOptions } from '../../lib/layout.shared';
1313-import { withBasePath } from '../../lib/markdown-url';
1416import { source } from '../../lib/source';
1515-import { getStorybookStoryUrl } from '../../lib/storybook';
1717+import { getStorybookStoryUrl, withBasePath } from '../../lib/storybook';
16181719const GITHUB_DOCS_URL = 'https://github.com/lukebennett88/luke-ui/blob/main/apps/docs/content/docs';
2020+2121+const mdxComponents = { ...defaultMdxComponents, AutoTypeTable, TypeTable, ExampleBlock };
18221923export const Route = createFileRoute('/docs/$')({
2024 component: Page,
···3640 const page = source.getPage(slugs);
3741 if (!page) throw notFound();
38424343+ const markdownPath = `${page.url === '/docs' ? '/docs/index' : page.url}.md`;
4444+3945 return {
4046 githubUrl: `${GITHUB_DOCS_URL}/${page.path}`,
4141- markdownUrl: withBasePath(`${page.url}.md`, import.meta.env.BASE_URL),
4747+ markdownUrl: withBasePath(markdownPath, import.meta.env.BASE_URL),
4248 pageTree: await source.serializePageTree(source.getPageTree()),
4349 path: page.path,
4450 storybookUrl: getStorybookStoryUrl(page.path, import.meta.env.BASE_URL),
···6672 markdownUrl={markdownUrl}
6773 storybookUrl={storybookUrl}
6874 />
6969- <MDX components={defaultMdxComponents} />
7575+ <MDX components={mdxComponents} />
7076 </DocsBody>
7177 </DocsPage>
7278 );
···11-import { execFile } from 'node:child_process';
22-import { fileURLToPath } from 'node:url';
33-import { promisify } from 'node:util';
44-import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
55-import { describe, expect, it } from 'vite-plus/test';
66-import { z } from 'zod';
77-88-const execFileAsync = promisify(execFile);
99-const packageJsonPath = fileURLToPath(new URL('../../package.json', import.meta.url));
1010-const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
1111-const docsDir = fileURLToPath(new URL('../../docs/', import.meta.url));
1212-const staleDocsPath = fileURLToPath(
1313- new URL('../../docs/use-synchronize-animations.md', import.meta.url),
1414-);
1515-const turboJsonPath = fileURLToPath(new URL('../../../../../turbo.json', import.meta.url));
1616-const packageJsonSchema = z.object({
1717- scripts: z.record(z.string(), z.string()),
1818-});
1919-const turboJsonSchema = z.object({
2020- tasks: z.record(
2121- z.string(),
2222- z.object({
2323- dependsOn: z.array(z.string()).optional(),
2424- }),
2525- ),
2626-});
2727-2828-describe('package scripts', () => {
2929- it('lets Turbo order build before package docs generation', async () => {
3030- const packageJson = packageJsonSchema.parse(
3131- JSON.parse(await readFile(packageJsonPath, 'utf8')),
3232- );
3333- const turboJson = turboJsonSchema.parse(JSON.parse(await readFile(turboJsonPath, 'utf8')));
3434-3535- expect(packageJson.scripts.build).toBe('pnpm run build:tsdown');
3636- expect(packageJson.scripts.generate).toBe('pnpm run generate:assets');
3737- expect(turboJson.tasks['@luke-ui/react#generate:docs']?.dependsOn).toContain('build');
3838- expect(turboJson.tasks['docs#dev']?.dependsOn).toContain('@luke-ui/react#generate:docs');
3939- });
4040-4141- // Runs the real generate-docs script in a subprocess, which needs well over
4242- // the default 5s test timeout.
4343- it(
4444- 'removes stale generated package docs before writing current pages',
4545- { timeout: 60_000 },
4646- async () => {
4747- await mkdir(docsDir, { recursive: true });
4848- await writeFile(staleDocsPath, '# stale page\n', 'utf8');
4949-5050- try {
5151- await execFileAsync('pnpm', ['exec', 'tsx', 'scripts/generate-docs.ts'], {
5252- cwd: packageRoot,
5353- });
5454-5555- await expect(access(staleDocsPath)).rejects.toMatchObject({ code: 'ENOENT' });
5656- } finally {
5757- await rm(staleDocsPath, { force: true });
5858- }
5959- },
6060- );
6161-});
-96
packages/@luke-ui/react/src/button/button.docs.md
···11-`Button` expects the Luke UI theme class at the app or root level. See
22-[Getting Started](/docs/getting-started).
33-44-```tsx
55-<Button>Save</Button>
66-```
77-88-## Best practices
99-1010-| Guidance | Practices |
1111-| -------- | -------------------------------------------------------------------------------------------------------------- |
1212-| Do | Use one `tone="primary"` button for the main action in a view. Use `neutral` or `ghost` for secondary actions. |
1313-| Do | Write a label that names the action, such as "Save changes" or "Delete account". Avoid vague labels like "OK". |
1414-| Do | Set `isPending` while an action is in flight so the user can see that work is still happening. |
1515-| Don't | Use `Button` for navigation. If the control only moves the user to another page, use `Link`. |
1616-1717-## Tone
1818-1919-`Button` has four tones: `primary` (default), `neutral`, `critical`, and `ghost`.
2020-2121-```tsx
2222-<Button tone="neutral">Cancel</Button>
2323-```
2424-2525-```tsx
2626-<Button tone="critical">Delete</Button>
2727-```
2828-2929-```tsx
3030-<Button tone="ghost">Dismiss</Button>
3131-```
3232-3333-## Size
3434-3535-`Button` has two sizes: `medium` (default) and `small`.
3636-3737-```tsx
3838-<Button size="small">Save</Button>
3939-```
4040-4141-## Icons
4242-4343-Use `startIcon` and `endIcon` to place an icon before or after the label. The icon inherits the
4444-button size, so the icon does not need its own `size` prop.
4545-4646-```tsx
4747-import { Icon } from '@luke-ui/react/icon';
4848-4949-<Button startIcon={<Icon name="add" aria-hidden />}>Add item</Button>;
5050-```
5151-5252-## Disabled
5353-5454-Disabled buttons cannot be focused or pressed.
5555-5656-```tsx
5757-<Button isDisabled>Save</Button>
5858-```
5959-6060-## Pending
6161-6262-Set `isPending` while an action is in flight. A spinner overlays the label and the button becomes
6363-non-interactive.
6464-6565-```tsx
6666-<Button isPending>Saving</Button>
6767-```
6868-6969-## Full width
7070-7171-Set `isBlock` to make the button fill the inline size of its container.
7272-7373-```tsx
7474-<Button isBlock>Save</Button>
7575-```
7676-7777-## Accessibility
7878-7979-`Button` wraps its children in `Text`, so visible text usually provides the accessible name. You
8080-normally do not need `aria-label`.
8181-8282-The pending spinner is `aria-hidden` and does not announce busy state to screen readers. If screen
8383-reader users need to hear the pending state, change the label text itself, for example to "Saving",
8484-while `isPending` is set.
8585-8686-## Primitive Button
8787-8888-The lower-level `Button` primitive is available when you need full control over children, such as
8989-custom loading states, render-prop children, or non-standard content.
9090-9191-```ts
9292-import { Button } from '@luke-ui/react/button/primitive';
9393-```
9494-9595-The primitive renders a single `<button>` element with no internal wrapper. Its children are direct
9696-flex items, so you manage layout yourself.
···11-Use `ComboboxField` for a single-select combobox with label, description, error, popover, listbox,
22-and optional async loading built in.
33-44-```tsx
55-<ComboboxField
66- label="Country"
77- name="country"
88- placeholder="Select a country..."
99- defaultItems={countries}
1010->
1111- {(item) => <ComboboxItem>{item.label}</ComboboxItem>}
1212-</ComboboxField>
1313-```
1414-1515-## Best practices
1616-1717-| Guidance | Practices |
1818-| -------- | ------------------------------------------------------------------------------------------------------- |
1919-| Do | Use `defaultItems` for a static list. Use `items` with `loadingState` when options load asynchronously. |
2020-| Don't | Use `ComboboxField` for multi-select. It is single-select only in v1. |
2121-2222-## API shape
2323-2424-- Root props follow React Aria `ComboBox` naming.
2525-- Composed convenience props include `label`, `description`, `errorMessage`, `necessityIndicator`,
2626- `size`, and `placeholder`.
2727-- `children` renders items from `items` or `defaultItems`.
2828-- `listBoxProps` and `loadMoreItem` are lower-level escape hatches.
2929-3030-## Selection indicators
3131-3232-The selected option shows a checkmark in the listbox. When the control has a selection, it shows a
3333-clear button before the trigger. Pressing the clear button clears the selection and input text.
3434-3535-The clear button is omitted when the field is disabled or read-only.
3636-3737-```tsx
3838-<ComboboxField defaultValue="ca" defaultItems={countries} label="Country" name="country">
3939- {(item) => <ComboboxItem>{item.label}</ComboboxItem>}
4040-</ComboboxField>
4141-```
4242-4343-## Required fields
4444-4545-Use `isRequired` with `necessityIndicator` to show that the field is mandatory.
4646-4747-```tsx
4848-<ComboboxField isRequired label="Country" name="country" necessityIndicator="icon">
4949- {(item) => <ComboboxItem>{item.label}</ComboboxItem>}
5050-</ComboboxField>
5151-```
5252-5353-## Validation
5454-5555-Pass field-level validation through React Aria `Form`. Use `errorMessage` to render the validation
5656-message.
5757-5858-```tsx
5959-import { Form } from 'react-aria-components';
6060-6161-<Form validationErrors={{ country: 'Please select a country.' }}>
6262- <ComboboxField
6363- name="country"
6464- label="Country"
6565- errorMessage={(validation) => validation.validationErrors.join(' ')}
6666- >
6767- {(item) => <ComboboxItem>{item.label}</ComboboxItem>}
6868- </ComboboxField>
6969-</Form>;
7070-```
7171-7272-## Grouped options
7373-7474-Use `ComboboxSection` to group related options inside the listbox.
7575-7676-```tsx
7777-<ComboboxField label="Country" name="country">
7878- <ComboboxSection title="Northern hemisphere">
7979- <ComboboxItem id="ca">Canada</ComboboxItem>
8080- </ComboboxSection>
8181- <ComboboxSection title="Southern hemisphere">
8282- <ComboboxItem id="au">Australia</ComboboxItem>
8383- </ComboboxSection>
8484-</ComboboxField>
8585-```
8686-8787-## Async options
8888-8989-Pass `loadingState` for built-in loading and empty states. Control the option collection with
9090-`items`.
9191-9292-```tsx
9393-<ComboboxField
9494- label="Country"
9595- name="country"
9696- items={results}
9797- loadingState={status}
9898- onInputChange={setQuery}
9999->
100100- {(item) => <ComboboxItem>{item.label}</ComboboxItem>}
101101-</ComboboxField>
102102-```
103103-104104-## Infinite scroll
105105-106106-Use `onLoadMore` for automatic sentinel-based loading, or `loadMoreItem` when you need full control
107107-over the load-more row.
108108-109109-```tsx
110110-<ComboboxField
111111- label="Country"
112112- name="country"
113113- items={results}
114114- loadingState={status}
115115- onLoadMore={fetchNextPage}
116116->
117117- {(item) => <ComboboxItem>{item.label}</ComboboxItem>}
118118-</ComboboxField>
119119-```
120120-121121-## Primitive kit
122122-123123-The individual `Combobox*` building blocks are available for library authors who need a custom
124124-combobox layout.
125125-126126-```ts
127127-import {
128128- ComboboxInput,
129129- ComboboxControl,
130130- ComboboxTextInput,
131131- ComboboxClearButton,
132132- ComboboxTrigger,
133133- ComboboxPopover,
134134- ComboboxListBox,
135135- ComboboxItem,
136136- ComboboxSection,
137137-} from '@luke-ui/react/combobox-field/primitive';
138138-```
139139-140140-### Size propagation
141141-142142-When you set `size` on `ComboboxInput`, it is inherited by `ComboboxControl`, `ComboboxTextInput`,
143143-`ComboboxClearButton`, `ComboboxTrigger`, `ComboboxItem`, and `ComboboxLoadMoreItem`.
144144-145145-You can override the inherited size on an individual child by passing that child an explicit `size`
146146-prop.
-20
packages/@luke-ui/react/src/emoji/emoji.docs.md
···11-Use `Emoji` when an emoji needs a reliable accessible label.
22-33-```tsx
44-<Emoji emoji="🎉" label="Celebration" />
55-```
66-77-`Emoji` accepts all `Text` props except `children` and `elementType`, so it inherits typography
88-controls such as `fontSize` and `color`.
99-1010-## Best practices
1111-1212-| Guidance | Practices |
1313-| -------- | ---------------------------------------------------------------------------------------------------------- |
1414-| Do | Write a `label` that describes the emoji's meaning in context, such as "Celebration". |
1515-| Don't | Repeat text that already appears in the surrounding sentence. That creates duplicate screen reader output. |
1616-1717-## Accessibility
1818-1919-Both `emoji` and `label` are required. Screen readers announce the label instead of the raw emoji
2020-character, which is not announced consistently across platforms.
···11-Use `Heading` for section headings. It can infer heading level from `HeadingLevels` context, or you
22-can pass `level` directly.
33-44-```tsx
55-<Heading>Section title</Heading>
66-```
77-88-```tsx
99-<Heading level={2}>Explicit h2</Heading>
1010-```
1111-1212-## Best practices
1313-1414-| Guidance | Practices |
1515-| -------- | ------------------------------------------------------------------------------------------------------------- |
1616-| Do | Prefer automatic leveling for nested sections instead of hardcoding every `level`. |
1717-| Don't | Skip heading levels, such as an h2 followed directly by an h4. Screen reader users navigate by heading level. |
1818-1919-## Automatic leveling
2020-2121-`Heading` reads its level from `HeadingLevels` context. Set `base` on the root context. Each nested
2222-`HeadingLevels` advances the next heading level.
2323-2424-```tsx
2525-import { Heading, HeadingLevels } from '@luke-ui/react/heading';
2626-2727-<HeadingLevels base={1}>
2828- <Heading>h1</Heading>
2929- <HeadingLevels>
3030- <Heading>h2 nested automatically</Heading>
3131- <HeadingLevels>
3232- <Heading>h3 nested again</Heading>
3333- </HeadingLevels>
3434- </HeadingLevels>
3535-</HeadingLevels>;
3636-```
3737-3838-The `level` prop overrides context for one heading without changing the nesting depth for siblings
3939-or children.
4040-4141-## Typography
4242-4343-`Heading` accepts all `Text` props except `fontSize`, which is controlled by the heading level. By
4444-default it applies `fontWeight="bold"` and `lineHeight="tight"`.
4545-4646-```tsx
4747-<Heading level={3} color="informative">
4848- Informative heading
4949-</Heading>
5050-```
5151-5252-```tsx
5353-<Heading level={4} fontWeight="regular">
5454- Light-weight heading
5555-</Heading>
5656-```
···11-Use `IconButton` for compact actions where an icon can carry the visible UI. Always provide an
22-accessible label.
33-44-```tsx
55-<IconButton icon="add" aria-label="Add item" />
66-```
77-88-## Best practices
99-1010-| Guidance | Practices |
1111-| -------- | ---------------------------------------------------------------------------- |
1212-| Do | Name the action in `aria-label`, such as "Delete row". Do not name the icon. |
1313-| Do | Use `tone="critical"` for destructive icon actions such as delete. |
1414-1515-## Tone
1616-1717-```tsx
1818-<IconButton icon="close" aria-label="Close" tone="ghost" />
1919-```
2020-2121-```tsx
2222-<IconButton icon="delete" aria-label="Delete" tone="critical" />
2323-```
2424-2525-## Size
2626-2727-```tsx
2828-<IconButton icon="delete" aria-label="Delete" size="small" />
2929-```
3030-3131-## Accessibility
3232-3333-`IconButton` has no visible text label. Always provide `aria-label` or `aria-labelledby`. Without
3434-one, screen readers have no way to announce the button's purpose.
3535-3636-```tsx
3737-<IconButton icon="search" aria-label="Search orders" />
3838-```
3939-4040-## When to use vs Button
4141-4242-Use `IconButton` when the icon alone communicates the action and space is tight, such as in
4343-toolbars, table row actions, or close buttons.
4444-4545-Use `Button` when you need a visible text label. Labels are clearer for unfamiliar users.
-84
packages/@luke-ui/react/src/icon/icon.docs.md
···11-`Icon` renders a symbol from the Luke UI spritesheet. It requires an `IconSpritesheetProvider`
22-ancestor. See Setup below.
33-44-```tsx
55-<Icon name="add" title="Add" size="xsmall" />
66-```
77-88-```tsx
99-<Icon name="close" aria-hidden size="medium" />
1010-```
1111-1212-## Best practices
1313-1414-| Guidance | Practices |
1515-| -------- | --------------------------------------------------------------------------------------------------------- |
1616-| Do | Pass `title` when the icon conveys meaning without adjacent text, such as a standalone status icon. |
1717-| Don't | Pass both `aria-hidden` and `title`. A titled icon should be exposed to assistive technology, not hidden. |
1818-1919-## Setup
2020-2121-Wrap your app with `IconSpritesheetProvider`.
2222-2323-- Source asset: `@luke-ui/react/spritesheet.svg`, exported from `./dist/spritesheet.svg`
2424-- Runtime lookup: `<configured-sprite-href>#<icon-name>`
2525-2626-```tsx
2727-<IconSpritesheetProvider href="/assets/spritesheet.svg">
2828- <App />
2929-</IconSpritesheetProvider>
3030-```
3131-3232-Vite and Storybook should import the spritesheet as a URL.
3333-3434-```ts
3535-import spriteSheetHref from '@luke-ui/react/spritesheet.svg?url&no-inline';
3636-3737-<IconSpritesheetProvider href={spriteSheetHref}>
3838- <App />
3939-</IconSpritesheetProvider>;
4040-```
4141-4242-The `no-inline` query avoids `data:` URLs, which can break `<use href="...#icon-id">` rendering.
4343-4444-When developing `@luke-ui/react`, generate icons from `packages/@luke-ui/react/icons/*.svg` into
4545-`packages/@luke-ui/react/dist/spritesheet.svg`.
4646-4747-```bash
4848-pnpm --dir packages/@luke-ui/react run generate:icons
4949-```
5050-5151-## How it works
5252-5353-`Icon` renders an `<svg>` that references a symbol in the generated spritesheet with
5454-`<use href="..." />`.
5555-5656-- `name` chooses the symbol id.
5757-- The sprite URL comes from `IconSpritesheetProvider`.
5858-- `viewBox` defaults to the generated icon viewBox.
5959-- Icon fill follows `currentColor`.
6060-6161-```tsx
6262-<Icon name="add" className="myIcon" style={{ color: 'tomato' }} />
6363-```
6464-6565-## Create your own icon
6666-6767-Use `createIcon` for a one-off icon that is not in the generated spritesheet.
6868-6969-```tsx
7070-import { createIcon } from '@luke-ui/react/icon';
7171-7272-const HeartIcon = createIcon({
7373- path: (
7474- <path d="M12 21a1 1 0 0 1-.7-.3L5 14.5a5 5 0 1 1 7-6 5 5 0 1 1 7 6l-6.3 6.2a1 1 0 0 1-.7.3Z" />
7575- ),
7676-});
7777-7878-<HeartIcon title="Favorite" size="small" />;
7979-```
8080-8181-## Accessibility
8282-8383-If `title` is provided, the icon is exposed to assistive technology with `role="img"`. If `title` is
8484-omitted, `aria-hidden` defaults to `true`.
-64
packages/@luke-ui/react/src/link/link.docs.md
···11-`Link` expects the Luke UI theme class at the app or root level. See
22-[Getting Started](/docs/getting-started).
33-44-```tsx
55-<Link href="/help">Help center</Link>
66-```
77-88-## Best practices
99-1010-| Guidance | Practices |
1111-| -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
1212-| Do | Use `tone="inverted"` only on dark backgrounds. On light backgrounds, it is hard to read. |
1313-| Do | Use `isStandalone` for links that are not part of a sentence, such as card links and nav items. Leave it `false` for inline links. |
1414-1515-## Tone
1616-1717-`Link` has three tones: `brand` (default), `neutral`, and `inverted`.
1818-1919-```tsx
2020-<Link href="/docs/accessibility" tone="neutral">
2121- Accessibility docs
2222-</Link>
2323-```
2424-2525-```tsx
2626-<div style={{ background: 'black', padding: 4 }}>
2727- <Link href="/status" tone="inverted">
2828- Service status
2929- </Link>
3030-</div>
3131-```
3232-3333-Use `neutral` for a more subtle link. Use `inverted` on dark backgrounds.
3434-3535-## Standalone
3636-3737-Use `isStandalone` when a link stands on its own. Leave it `false` for links inside paragraph text.
3838-3939-```tsx
4040-<Link href="/terms" isStandalone>
4141- Terms and conditions
4242-</Link>
4343-```
4444-4545-- `isStandalone={true}`: no underline until hover.
4646-- `isStandalone={false}`: underlined inline link style.
4747-4848-## Disabled
4949-5050-```tsx
5151-<Link href="/archived" isDisabled>
5252- Archived report
5353-</Link>
5454-```
5555-5656-## Accessibility
5757-5858-Screen readers announce a disabled link as unavailable, but not why. Put the reason in nearby
5959-visible text instead of relying on disabled state alone.
6060-6161-## When to use vs Button
6262-6363-Use `Link` to navigate to a new URL or route. Use `Button` for in-page actions, such as saving,
6464-submitting, or opening a dialog.
···11-`LoadingSkeleton` expects the Luke UI theme class at the app or root level. See
22-[Getting Started](/docs/getting-started).
33-44-Use it when loading content should keep the same footprint as the loaded state. Text renders as an
55-inline skeleton. Elements keep their layout while the skeleton surface is painted over them.
66-77-```tsx
88-<LoadingSkeleton isLoading={isLoading}>{user?.name ?? 'Placeholder name'}</LoadingSkeleton>
99-```
1010-1111-```tsx
1212-<LoadingSkeleton isLoading={isLoading}>
1313- <Button>Submit</Button>
1414-</LoadingSkeleton>
1515-```
1616-1717-All mounted skeletons use the same pulse timing, even when they mount at different times.
1818-1919-## Best practices
2020-2121-| Guidance | Practices |
2222-| -------- | ------------------------------------------------------------------------------------------------------------------- |
2323-| Do | Wrap real content so the skeleton matches the final size exactly. |
2424-| Don't | Use `LoadingSkeleton` for content whose final size is unknown. Size mismatch causes layout shift when loading ends. |
2525-2626-## Loading state
2727-2828-`isLoading` defaults to `true`. Pass `isLoading={false}` when content is ready.
2929-3030-```tsx
3131-<LoadingSkeleton isLoading={false}>
3232- <Button>Submit</Button>
3333-</LoadingSkeleton>
3434-```
3535-3636-## Multi-line text
3737-3838-Wrap text directly when the copy spans more than one line. Each line gets its own skeleton shape.
3939-4040-```tsx
4141-<div style={{ maxInlineSize: '16rem' }}>
4242- <LoadingSkeleton isLoading={isLoading}>
4343- A short paragraph of placeholder copy that wraps across two lines.
4444- </LoadingSkeleton>
4545-</div>
4646-```
4747-4848-## Element type
4949-5050-`LoadingSkeleton` renders a `span` by default. Use `as` when the surrounding markup needs another
5151-element.
5252-5353-```tsx
5454-<ul>
5555- <LoadingSkeleton as="li" isLoading={isLoading}>
5656- List item placeholder
5757- </LoadingSkeleton>
5858-</ul>
5959-```
6060-6161-## LoadingSkeletonProvider
6262-6363-Use `LoadingSkeletonProvider` when one loading state controls a group of skeletons. The provider
6464-value overrides descendant `isLoading` props.
6565-6666-```tsx
6767-<LoadingSkeletonProvider isLoading={isLoading}>
6868- <LoadingSkeleton isLoading={false}>Ada Lovelace</LoadingSkeleton>
6969- <LoadingSkeleton isLoading={false}>
7070- <Button>Edit profile</Button>
7171- </LoadingSkeleton>
7272-</LoadingSkeletonProvider>
7373-```
7474-7575-## Border radius
7676-7777-Use `borderRadius` when the wrapped child has no radius, but the visible control inside it does.
7878-7979-```tsx
8080-<LoadingSkeleton borderRadius="0.25rem" isLoading={isLoading}>
8181- <TextField label="Email" name="email" />
8282-</LoadingSkeleton>
8383-```
8484-8585-## Custom dimensions
8686-8787-Wrap an element with explicit dimensions when you need a placeholder for a fixed shape, such as an
8888-avatar.
8989-9090-```tsx
9191-<LoadingSkeleton isLoading={isLoading}>
9292- <div style={{ borderRadius: '9999px', height: '3rem', width: '3rem' }} />
9393-</LoadingSkeleton>
9494-```
9595-9696-## Accessibility
9797-9898-While loading, skeleton content is hidden from assistive technology and cannot be focused or
9999-clicked. `LoadingSkeleton` sets `aria-hidden`, `inert`, `tabIndex={-1}`, and disables pointer
100100-events.
···11-`LoadingSpinner` expects the Luke UI theme class at the app or root level. See
22-[Getting Started](/docs/getting-started).
33-44-```tsx
55-<LoadingSpinner aria-label="Loading" />
66-```
77-88-All mounted indeterminate spinners rotate and pulse in sync, even when they mount at different
99-times.
1010-1111-## Progress mode
1212-1313-Omit `value` for indeterminate progress. Pass `value` for determinate progress.
1414-1515-```tsx
1616-<LoadingSpinner aria-label="Loading profile" value={66} />
1717-```
1818-1919-## Size
2020-2121-```tsx
2222-<LoadingSpinner aria-label="Syncing" size="small" />
2323-```
2424-2525-## Color
2626-2727-```tsx
2828-<LoadingSpinner aria-label="Syncing" color="informative" />
2929-```
3030-3131-## Accessibility
3232-3333-`aria-label` defaults to `"pending"` when omitted. Override it with what is loading, such as
3434-"Loading profile", for a clearer announcement.
···11-`Numeral` formats numbers with `Intl.NumberFormat`. It respects locale from React Aria's
22-`I18nProvider`.
33-44-```tsx
55-<Numeral value={12_345.67} />
66-```
77-88-## Formats
99-1010-`Numeral` infers `format` from `currency` or `unit` when omitted. Pass `format` explicitly for
1111-`'percent'` or `'decimal'`, or when you want to be direct about currency and unit formatting.
1212-1313-```tsx
1414-<Numeral value={3_500} format="percent" />
1515-```
1616-1717-```tsx
1818-<Numeral value={98.76} currency="AUD" />
1919-```
2020-2121-```tsx
2222-<Numeral value={98} unit="kilometer-per-hour" />
2323-```
2424-2525-```tsx
2626-<Numeral value={12_345} format="decimal" />
2727-```
2828-2929-## Compact notation
3030-3131-Use `abbreviate` for compact notation.
3232-3333-```tsx
3434-<Numeral value={12_345} abbreviate />
3535-```
3636-3737-```tsx
3838-<Numeral value={12_345} abbreviate="long" />
3939-```
4040-4141-## Precision
4242-4343-Pass a number for fixed fraction digits, or a `[min, max]` tuple for a range.
4444-4545-```tsx
4646-<Numeral value={98.7654} precision={2} />
4747-```
4848-4949-```tsx
5050-<Numeral value={1_234.5678} precision={[0, 2]} />
5151-```
5252-5353-## Constraints
5454-5555-`Numeral` throws in development when:
5656-5757-- both `currency` and `unit` are provided.
5858-- `format="currency"` is used without a `currency` code.
5959-- `format="unit"` is used without a `unit` value.
6060-- `precision` is not a non-negative integer or valid `[min, max]` tuple.
···11-Use `TextField` for a single text input with label, description, validation, and optional adornments
22-built in.
33-44-```tsx
55-<TextField
66- name="email"
77- label="Email"
88- description="We'll only use this for account updates."
99- placeholder="name@example.com"
1010-/>
1111-```
1212-1313-## Best practices
1414-1515-| Guidance | Practices |
1616-| -------- | --------------------------------------------------------------------------------------------------------------------------- |
1717-| Do | Use `label` for every field where possible. It works better with assistive technology and autofill than `aria-label` alone. |
1818-| Don't | Use `placeholder` as a label substitute. It disappears after typing and often fails colour contrast requirements. |
1919-2020-## Required fields
2121-2222-Use `isRequired` and `necessityIndicator` to communicate mandatory fields. `'icon'` renders a visual
2323-required marker. `'label'` appends "(required)" to the label text.
2424-2525-```tsx
2626-<TextField isRequired name="firstName" label="First name" necessityIndicator="icon" />
2727-```
2828-2929-```tsx
3030-<TextField isRequired name="lastName" label="Last name" necessityIndicator="label" />
3131-```
3232-3333-## Validation
3434-3535-Pass field-level validation through React Aria `Form`. Use `errorMessage` to render the validation
3636-message.
3737-3838-```tsx
3939-import { Form } from 'react-aria-components';
4040-4141-<Form validationErrors={{ username: 'Username is not available.' }}>
4242- <TextField
4343- name="username"
4444- label="Username"
4545- errorMessage={(validation) => validation.validationErrors.join(' ')}
4646- />
4747-</Form>;
4848-```
4949-5050-## Adornments
5151-5252-Use `adornmentStart` and `adornmentEnd` to place non-editable content inside the input chrome.
5353-Adornments accept any `ReactNode`. If an adornment is interactive, you are responsible for its
5454-semantics.
5555-5656-```tsx
5757-import { Icon } from '@luke-ui/react/icon';
5858-5959-<TextField
6060- name="search"
6161- label="Search"
6262- adornmentStart={<Icon name="search" aria-hidden size="small" />}
6363-/>;
6464-```
6565-6666-```tsx
6767-<TextField name="url" label="URL" adornmentStart="https://" />
6868-```
6969-7070-```tsx
7171-<TextField name="price" label="Price" adornmentEnd="AUD" />
7272-```
7373-7474-## Size
7575-7676-`size` controls height and typography. The HTML numeric `<input size>` attribute is intentionally
7777-omitted because `size` is reserved for the design-system variant.
7878-7979-| Value | Description |
8080-| ---------- | --------------------- |
8181-| `'small'` | Compact input height. |
8282-| `'medium'` | Default input height. |
8383-8484-## Accessibility
8585-8686-When visual context already communicates purpose, you may omit `label` and provide an accessible
8787-name with `aria-label` or `aria-labelledby` on the field.
8888-8989-```tsx
9090-<TextField aria-label="Search" name="search" placeholder="Search" />
9191-```
9292-9393-## Primitive TextInput
9494-9595-The lower-level `TextInput` primitive is available when you need the input without the label,
9696-description, or error slots that `Field` provides.
9797-9898-```ts
9999-import { TextInput } from '@luke-ui/react/text-field/primitive';
100100-```
-101
packages/@luke-ui/react/src/text/text.docs.md
···11-`Text` expects the Luke UI theme class at the app or root level. See
22-[Getting Started](/docs/getting-started).
33-44-Use `Text` for styled text that should not create heading semantics.
55-66-```tsx
77-<Text>The quick brown fox jumps over the lazy dog.</Text>
88-```
99-1010-## Best practices
1111-1212-| Guidance | Practices |
1313-| -------- | ----------------------------------------------------------------------------------------------------------- |
1414-| Do | Use `fontSize` tokens, such as `'h2'`, instead of arbitrary values so text stays consistent across the app. |
1515-| Don't | Use `Text` for section headings. Use `Heading`, which manages semantic level automatically. |
1616-1717-## Typography
1818-1919-```tsx
2020-<Text fontSize="h2" lineHeight="tight">
2121- Heading-like text
2222-</Text>
2323-```
2424-2525-```tsx
2626-<Text color="critical" fontFamily="mono" fontWeight="bold">
2727- Alert-like inline text
2828-</Text>
2929-```
3030-3131-See the token reference below for valid `color`, `fontFamily`, `fontSize`, `lineHeight`, and
3232-`fontWeight` values.
3333-3434-## Text transform and decoration
3535-3636-```tsx
3737-<Text textTransform="uppercase" textDecoration="underline">
3838- Emphasized text
3939-</Text>
4040-```
4141-4242-## Truncation
4343-4444-```tsx
4545-<Text lineClamp={2}>Long content that should be line clamped.</Text>
4646-```
4747-4848-```tsx
4949-<Text lineClamp>Long content truncated to one line.</Text>
5050-```
5151-5252-```tsx
5353-<Text shouldDisableTrim>Untrimmed text spacing</Text>
5454-```
5555-5656-## Alignment and numeric glyphs
5757-5858-```tsx
5959-<Text textAlign="end" fontVariantNumeric="tabular-nums">
6060- 12121.21
6161-</Text>
6262-```
6363-6464-## Token reference
6565-6666-### `color` tokens
6767-6868-`neutralSubtle`, `neutralBold`, `neutralDisabled`, `neutralBoldInverted`, `positive`, `informative`,
6969-`caution`, `critical`, `inherit`
7070-7171-### `fontFamily` tokens
7272-7373-`sans`, `mono`
7474-7575-### `fontSize` tokens
7676-7777-`xxsmall`, `xsmall`, `small`, `standard`, `medium`, `large`, `xlarge`, `xxlarge`, `h1`, `h2`, `h3`,
7878-`h4`, `h5`, `h6`
7979-8080-### `lineHeight` tokens
8181-8282-`nospace`, `tight`, `loose`
8383-8484-### `fontWeight` tokens
8585-8686-`regular`, `medium`, `bold`, `inherit`
8787-8888-### `textDecoration` values
8989-9090-`none`, `underline`, `line-through`, `inherit`
9191-9292-### `textTransform` values
9393-9494-`none`, `capitalize`, `uppercase`, `lowercase`, `inherit`
9595-9696-## When to use vs Heading
9797-9898-Use `Heading` for actual section headings because it manages semantic level nesting automatically.
9999-100100-Use `Text` with a heading-sized `fontSize` token when content should look like a heading but is not
101101-semantically one, such as a large stat number.
···33description: Action button with tone, size, icon, pending, and disabled states.
44---
5566-import { story } from '../../../../src/button/button.story';
66+`Button` expects the Luke UI theme class at the app or root level. See
77+[Getting Started](/docs/getting-started).
7888-<story.WithControl />
99+```tsx
1010+<Button>Save</Button>
1111+```
9121010-<include>../../../../../../packages/@luke-ui/react/docs/button.md</include>
1313+## Best practices
1414+1515+| Guidance | Practices |
1616+| -------- | -------------------------------------------------------------------------------------------------------------- |
1717+| Do | Use one `tone="primary"` button for the main action in a view. Use `neutral` or `ghost` for secondary actions. |
1818+| Do | Write a label that names the action, such as "Save changes" or "Delete account". Avoid vague labels like "OK". |
1919+| Do | Set `isPending` while an action is in flight so the user can see that work is still happening. |
2020+| Don't | Use `Button` for navigation. If the control only moves the user to another page, use `Link`. |
2121+2222+## Tone
2323+2424+`Button` has four tones: `primary` (default), `neutral`, `critical`, and `ghost`.
2525+2626+<ExampleBlock
2727+ component="button"
2828+ name="tones"
2929+ title="Button — Tones"
3030+ description="Primary, critical, ghost, and neutral tones for different emphasis levels."
3131+/>
3232+3333+## Size
3434+3535+`Button` has two sizes: `medium` (default) and `small`.
3636+3737+<ExampleBlock
3838+ component="button"
3939+ name="sizes"
4040+ title="Button — Sizes"
4141+ description="Small and medium buttons side by side. Use small in dense UIs, medium for most cases."
4242+/>
4343+4444+## Icons
4545+4646+Use `startIcon` and `endIcon` to place an icon before or after the label. The icon inherits the
4747+button size, so the icon does not need its own `size` prop.
4848+4949+```tsx
5050+import { Icon } from '@luke-ui/react/icon';
5151+5252+<Button startIcon={<Icon name="add" aria-hidden />}>Add item</Button>;
5353+```
5454+5555+## Disabled
5656+5757+Disabled buttons cannot be focused or pressed.
5858+5959+```tsx
6060+<Button isDisabled>Save</Button>
6161+```
6262+6363+## Pending
6464+6565+Set `isPending` while an action is in flight. A spinner overlays the label and the button becomes
6666+non-interactive.
6767+6868+<ExampleBlock
6969+ component="button"
7070+ name="pending"
7171+ title="Button — Pending"
7272+ description="Show a pending state with `isPending` while an async action is in progress."
7373+/>
7474+7575+## Full width
7676+7777+Set `isBlock` to make the button fill the inline size of its container.
7878+7979+```tsx
8080+<Button isBlock>Save</Button>
8181+```
8282+8383+## Accessibility
8484+8585+`Button` wraps its children in `Text`, so visible text usually provides the accessible name. You
8686+normally do not need `aria-label`.
8787+8888+The pending spinner is `aria-hidden` and does not announce busy state to screen readers. If screen
8989+reader users need to hear the pending state, change the label text itself, for example to "Saving",
9090+while `isPending` is set.
9191+9292+## Primitive Button
9393+9494+The lower-level `Button` primitive is available when you need full control over children, such as
9595+custom loading states, render-prop children, or non-standard content.
9696+9797+```ts
9898+import { Button } from '@luke-ui/react/button/primitive';
9999+```
100100+101101+The primitive renders a single `<button>` element with no internal wrapper. Its children are direct
102102+flex items, so you manage layout yourself.
103103+104104+## Props
105105+106106+<auto-type-table path="packages/@luke-ui/react/src/button/index.tsx" name="ButtonProps" />
···33description: Compact icon-only action button with an accessible label.
44---
5566-import { story } from '../../../../src/icon-button/icon-button.story';
66+Use `IconButton` for compact actions where an icon can carry the visible UI. Always provide an
77+accessible label.
7888-<story.WithControl />
99+```tsx
1010+<IconButton icon="add" aria-label="Add item" />
1111+```
9121010-<include>../../../../../../packages/@luke-ui/react/docs/icon-button.md</include>
1313+## Best practices
1414+1515+| Guidance | Practices |
1616+| -------- | ---------------------------------------------------------------------------- |
1717+| Do | Name the action in `aria-label`, such as "Delete row". Do not name the icon. |
1818+| Do | Use `tone="critical"` for destructive icon actions such as delete. |
1919+2020+## Tone
2121+2222+<ExampleBlock
2323+ component="icon-button"
2424+ name="tones"
2525+ title="Icon Button — Tone"
2626+ description="Ghost and critical tones for icon-only actions."
2727+/>
2828+2929+## Size
3030+3131+<ExampleBlock
3232+ component="icon-button"
3333+ name="size"
3434+ title="Icon Button — Size"
3535+ description="A small icon button for tighter spaces."
3636+/>
3737+3838+## Accessibility
3939+4040+`IconButton` has no visible text label. Always provide `aria-label` or `aria-labelledby`. Without
4141+one, screen readers have no way to announce the button's purpose.
4242+4343+```tsx
4444+<IconButton icon="search" aria-label="Search orders" />
4545+```
4646+4747+## When to use vs Button
4848+4949+Use `IconButton` when the icon alone communicates the action and space is tight, such as in
5050+toolbars, table row actions, or close buttons.
5151+5252+Use `Button` when you need a visible text label. Labels are clearer for unfamiliar users.
5353+5454+## Props
5555+5656+<auto-type-table path="packages/@luke-ui/react/src/icon-button/index.tsx" name="IconButtonProps" />
···33description: Link component for inline and standalone navigation.
44---
5566-import { story } from '../../../../src/link/link.story';
66+`Link` expects the Luke UI theme class at the app or root level. See
77+[Getting Started](/docs/getting-started).
7888-<story.WithControl />
99+```tsx
1010+<Link href="/help">Help center</Link>
1111+```
9121010-<include>../../../../../../packages/@luke-ui/react/docs/link.md</include>
1313+## Best practices
1414+1515+| Guidance | Practices |
1616+| -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
1717+| Do | Use `tone="inverted"` only on dark backgrounds. On light backgrounds, it is hard to read. |
1818+| Do | Use `isStandalone` for links that are not part of a sentence, such as card links and nav items. Leave it `false` for inline links. |
1919+2020+## Tone
2121+2222+`Link` has three tones: `brand` (default), `neutral`, and `inverted`. Use `neutral` for a more
2323+subtle link. Use `inverted` on dark backgrounds.
2424+2525+<ExampleBlock
2626+ component="link"
2727+ name="tones"
2828+ title="Link — Tones"
2929+ description="Brand, neutral, and inverted tones for different backgrounds."
3030+/>
3131+3232+## Standalone
3333+3434+Use `isStandalone` when a link stands on its own. Leave it `false` for links inside paragraph text.
3535+3636+- `isStandalone={true}`: no underline until hover.
3737+- `isStandalone={false}`: underlined inline link style.
3838+3939+<ExampleBlock
4040+ component="link"
4141+ name="standalone"
4242+ title="Link — Standalone"
4343+ description="Use isStandalone for links that stand on their own, or inline links in prose."
4444+/>
4545+4646+## Disabled
4747+4848+<ExampleBlock
4949+ component="link"
5050+ name="disabled"
5151+ title="Link — Disabled"
5252+ description="A disabled link cannot be focused or activated."
5353+/>
5454+5555+## Accessibility
5656+5757+Screen readers announce a disabled link as unavailable, but not why. Put the reason in nearby
5858+visible text instead of relying on disabled state alone.
5959+6060+## When to use vs Button
6161+6262+Use `Link` to navigate to a new URL or route. Use `Button` for in-page actions, such as saving,
6363+submitting, or opening a dialog.
6464+6565+## Props
6666+6767+<auto-type-table path="packages/@luke-ui/react/src/link/index.tsx" name="LinkProps" />
···33description: Loading placeholder that keeps the same footprint as the final content.
44---
5566-import { story } from '../../../../src/loading-skeleton/loading-skeleton.story';
66+`LoadingSkeleton` expects the Luke UI theme class at the app or root level. See
77+[Getting Started](/docs/getting-started).
7888-<story.WithControl />
99+Use it when loading content should keep the same footprint as the loaded state. Text renders as an
1010+inline skeleton. Elements keep their layout while the skeleton surface is painted over them.
9111010-<include>../../../../../../packages/@luke-ui/react/docs/loading-skeleton.md</include>
1212+<ExampleBlock
1313+ component="loading-skeleton"
1414+ name="text"
1515+ title="Loading Skeleton — Text"
1616+ description="Inline skeleton that mirrors the wrapped text size."
1717+/>
1818+1919+All mounted skeletons use the same pulse timing, even when they mount at different times.
2020+2121+## Best practices
2222+2323+| Guidance | Practices |
2424+| -------- | ------------------------------------------------------------------------------------------------------------------- |
2525+| Do | Wrap real content so the skeleton matches the final size exactly. |
2626+| Don't | Use `LoadingSkeleton` for content whose final size is unknown. Size mismatch causes layout shift when loading ends. |
2727+2828+## Loading state
2929+3030+`isLoading` defaults to `true`. Pass `isLoading={false}` when content is ready.
3131+3232+```tsx
3333+<LoadingSkeleton isLoading={false}>
3434+ <Button>Submit</Button>
3535+</LoadingSkeleton>
3636+```
3737+3838+## Multi-line text
3939+4040+Wrap text directly when the copy spans more than one line. Each line gets its own skeleton shape.
4141+4242+## Element type
4343+4444+`LoadingSkeleton` renders a `span` by default. Use `as` when the surrounding markup needs another
4545+element.
4646+4747+<ExampleBlock
4848+ component="loading-skeleton"
4949+ name="element"
5050+ title="Loading Skeleton — Element"
5151+ description="Render the skeleton as a different element to match the surrounding markup."
5252+/>
5353+5454+## LoadingSkeletonProvider
5555+5656+Use `LoadingSkeletonProvider` when one loading state controls a group of skeletons. The provider
5757+value overrides descendant `isLoading` props.
5858+5959+<ExampleBlock
6060+ component="loading-skeleton"
6161+ name="provider"
6262+ title="Loading Skeleton — Provider"
6363+ description="One loading state controls a group of skeletons."
6464+/>
6565+6666+## Border radius
6767+6868+Use `borderRadius` when the wrapped child has no radius, but the visible control inside it does.
6969+7070+<ExampleBlock
7171+ component="loading-skeleton"
7272+ name="border-radius"
7373+ title="Loading Skeleton — Border radius"
7474+ description="Override the skeleton corner radius for wrapped controls."
7575+/>
7676+7777+## Custom dimensions
7878+7979+Wrap an element with explicit dimensions when you need a placeholder for a fixed shape, such as an
8080+avatar.
8181+8282+<ExampleBlock
8383+ component="loading-skeleton"
8484+ name="custom-dimensions"
8585+ title="Loading Skeleton — Custom dimensions"
8686+ description="Wrap an element with explicit dimensions for a fixed-shape placeholder."
8787+/>
8888+8989+## Accessibility
9090+9191+While loading, skeleton content is hidden from assistive technology and cannot be focused or
9292+clicked. `LoadingSkeleton` sets `aria-hidden`, `inert`, `tabIndex={-1}`, and disables pointer
9393+events.
9494+9595+## Props
9696+9797+<auto-type-table
9898+ path="packages/@luke-ui/react/src/loading-skeleton/index.tsx"
9999+ name="LoadingSkeletonProps"
100100+/>
···33description: Spinner for indeterminate and determinate progress.
44---
5566-import { story } from '../../../../src/loading-spinner/loading-spinner.story';
66+`LoadingSpinner` expects the Luke UI theme class at the app or root level. See
77+[Getting Started](/docs/getting-started).
7888-<story.WithControl />
99+<ExampleBlock
1010+ component="loading-spinner"
1111+ name="basic"
1212+ title="Loading Spinner — Basic"
1313+ description="Indeterminate spinner with a default pending label."
1414+/>
9151010-<include>../../../../../../packages/@luke-ui/react/docs/loading-spinner.md</include>
1616+All mounted indeterminate spinners rotate and pulse in sync, even when they mount at different
1717+times.
1818+1919+## Progress mode
2020+2121+Omit `value` for indeterminate progress. Pass `value` for determinate progress.
2222+2323+<ExampleBlock
2424+ component="loading-spinner"
2525+ name="progress"
2626+ title="Loading Spinner — Progress mode"
2727+ description="Indeterminate and determinate progress spinners."
2828+/>
2929+3030+## Size
3131+3232+<ExampleBlock
3333+ component="loading-spinner"
3434+ name="sizes"
3535+ title="Loading Spinner — Sizes"
3636+ description="Small and medium spinner sizes."
3737+/>
3838+3939+## Color
4040+4141+<ExampleBlock
4242+ component="loading-spinner"
4343+ name="colors"
4444+ title="Loading Spinner — Colors"
4545+ description="Informative and critical spinner colors."
4646+/>
4747+4848+## Accessibility
4949+5050+`aria-label` defaults to `"pending"` when omitted. Override it with what is loading, such as
5151+"Loading profile", for a clearer announcement.
5252+5353+## Props
5454+5555+<auto-type-table
5656+ path="packages/@luke-ui/react/src/loading-spinner/index.tsx"
5757+ name="LoadingSpinnerProps"
5858+/>
···33description: Single-select combobox field with label, validation, and async options.
44---
5566-import { story } from '../../../../src/combobox-field/combobox-field.story';
66+Use `ComboboxField` for a single-select combobox with label, description, error, popover, listbox,
77+and optional async loading built in.
7888-<story.WithControl />
99+<ExampleBlock
1010+ component="combobox-field"
1111+ name="basic"
1212+ title="Combobox Field — Basic"
1313+ description="Single-select combobox with a static list of options."
1414+/>
9151010-<include>../../../../../../packages/@luke-ui/react/docs/combobox-field.md</include>
1616+## Best practices
1717+1818+| Guidance | Practices |
1919+| -------- | ------------------------------------------------------------------------------------------------------- |
2020+| Do | Use `defaultItems` for a static list. Use `items` with `loadingState` when options load asynchronously. |
2121+| Don't | Use `ComboboxField` for multi-select. It is single-select only in v1. |
2222+2323+## API shape
2424+2525+- Root props follow React Aria `ComboBox` naming.
2626+- Composed convenience props include `label`, `description`, `errorMessage`, `necessityIndicator`,
2727+ `size`, and `placeholder`.
2828+- `children` renders items from `items` or `defaultItems`.
2929+- `listBoxProps` and `loadMoreItem` are lower-level escape hatches.
3030+3131+## Selection indicators
3232+3333+The selected option shows a checkmark in the listbox. When the control has a selection, it shows a
3434+clear button before the trigger. Pressing the clear button clears the selection and input text.
3535+3636+The clear button is omitted when the field is disabled or read-only.
3737+3838+```tsx
3939+<ComboboxField defaultValue="ca" defaultItems={countries} label="Country" name="country">
4040+ {(item) => <ComboboxItem>{item.label}</ComboboxItem>}
4141+</ComboboxField>
4242+```
4343+4444+## Required fields
4545+4646+Use `isRequired` with `necessityIndicator` to show that the field is mandatory.
4747+4848+<ExampleBlock
4949+ component="combobox-field"
5050+ name="required"
5151+ title="Combobox Field — Required"
5252+ description="Required combobox with a necessity indicator."
5353+/>
5454+5555+## Validation
5656+5757+Pass field-level validation through React Aria `Form`. Use `errorMessage` to render the validation
5858+message.
5959+6060+```tsx
6161+import { Form } from 'react-aria-components';
6262+6363+<Form validationErrors={{ country: 'Please select a country.' }}>
6464+ <ComboboxField
6565+ name="country"
6666+ label="Country"
6767+ errorMessage={(validation) => validation.validationErrors.join(' ')}
6868+ >
6969+ {(item) => <ComboboxItem>{item.label}</ComboboxItem>}
7070+ </ComboboxField>
7171+</Form>;
7272+```
7373+7474+## Grouped options
7575+7676+Use `ComboboxSection` to group related options inside the listbox.
7777+7878+<ExampleBlock
7979+ component="combobox-field"
8080+ name="grouped"
8181+ title="Combobox Field — Grouped options"
8282+ description="Group related options inside the listbox with ComboboxSection."
8383+/>
8484+8585+## Async options
8686+8787+Pass `loadingState` for built-in loading and empty states. Control the option collection with
8888+`items`.
8989+9090+```tsx
9191+<ComboboxField
9292+ label="Country"
9393+ name="country"
9494+ items={results}
9595+ loadingState={status}
9696+ onInputChange={setQuery}
9797+>
9898+ {(item) => <ComboboxItem>{item.label}</ComboboxItem>}
9999+</ComboboxField>
100100+```
101101+102102+## Infinite scroll
103103+104104+Use `onLoadMore` for automatic sentinel-based loading, or `loadMoreItem` when you need full control
105105+over the load-more row.
106106+107107+```tsx
108108+<ComboboxField
109109+ label="Country"
110110+ name="country"
111111+ items={results}
112112+ loadingState={status}
113113+ onLoadMore={fetchNextPage}
114114+>
115115+ {(item) => <ComboboxItem>{item.label}</ComboboxItem>}
116116+</ComboboxField>
117117+```
118118+119119+## Primitive kit
120120+121121+The individual `Combobox*` building blocks are available for library authors who need a custom
122122+combobox layout.
123123+124124+```ts
125125+import {
126126+ ComboboxInput,
127127+ ComboboxControl,
128128+ ComboboxTextInput,
129129+ ComboboxClearButton,
130130+ ComboboxTrigger,
131131+ ComboboxPopover,
132132+ ComboboxListBox,
133133+ ComboboxItem,
134134+ ComboboxSection,
135135+} from '@luke-ui/react/combobox-field/primitive';
136136+```
137137+138138+### Size propagation
139139+140140+When you set `size` on `ComboboxInput`, it is inherited by `ComboboxControl`, `ComboboxTextInput`,
141141+`ComboboxClearButton`, `ComboboxTrigger`, `ComboboxItem`, and `ComboboxLoadMoreItem`.
142142+143143+You can override the inherited size on an individual child by passing that child an explicit `size`
144144+prop.
145145+146146+## Props
147147+148148+<auto-type-table
149149+ path="packages/@luke-ui/react/src/combobox-field/index.tsx"
150150+ name="ComboboxFieldProps"
151151+/>
···33description: Single-line text input with label, validation, and adornments.
44---
5566-import { story } from '../../../../src/text-field/text-field.story';
66+Use `TextField` for a single text input with label, description, validation, and optional adornments
77+built in.
7888-<story.WithControl />
99+<ExampleBlock
1010+ component="text-field"
1111+ name="basic"
1212+ title="Text Field — Basic"
1313+ description="Single-line text input with label and description."
1414+/>
9151010-<include>../../../../../../packages/@luke-ui/react/docs/text-field.md</include>
1616+## Best practices
1717+1818+| Guidance | Practices |
1919+| -------- | --------------------------------------------------------------------------------------------------------------------------- |
2020+| Do | Use `label` for every field where possible. It works better with assistive technology and autofill than `aria-label` alone. |
2121+| Don't | Use `placeholder` as a label substitute. It disappears after typing and often fails colour contrast requirements. |
2222+2323+## Required fields
2424+2525+Use `isRequired` and `necessityIndicator` to communicate mandatory fields. `'icon'` renders a visual
2626+required marker. `'label'` appends "(required)" to the label text.
2727+2828+<ExampleBlock
2929+ component="text-field"
3030+ name="required"
3131+ title="Text Field — Required"
3232+ description="Icon and label necessity indicators for mandatory fields."
3333+/>
3434+3535+## Validation
3636+3737+Pass field-level validation through React Aria `Form`. Use `errorMessage` to render the validation
3838+message.
3939+4040+```tsx
4141+import { Form } from 'react-aria-components';
4242+4343+<Form validationErrors={{ username: 'Username is not available.' }}>
4444+ <TextField
4545+ name="username"
4646+ label="Username"
4747+ errorMessage={(validation) => validation.validationErrors.join(' ')}
4848+ />
4949+</Form>;
5050+```
5151+5252+## Adornments
5353+5454+Use `adornmentStart` and `adornmentEnd` to place non-editable content inside the input chrome.
5555+Adornments accept any `ReactNode`. If an adornment is interactive, you are responsible for its
5656+semantics.
5757+5858+<ExampleBlock
5959+ component="text-field"
6060+ name="adornments"
6161+ title="Text Field — Adornments"
6262+ description="Start and end adornments inside the input chrome."
6363+/>
6464+6565+## Size
6666+6767+`size` controls height and typography. The HTML numeric `<input size>` attribute is intentionally
6868+omitted because `size` is reserved for the design-system variant.
6969+7070+| Value | Description |
7171+| ---------- | --------------------- |
7272+| `'small'` | Compact input height. |
7373+| `'medium'` | Default input height. |
7474+7575+<ExampleBlock
7676+ component="text-field"
7777+ name="sizes"
7878+ title="Text Field — Sizes"
7979+ description="Small and medium input heights."
8080+/>
8181+8282+## Accessibility
8383+8484+When visual context already communicates purpose, you may omit `label` and provide an accessible
8585+name with `aria-label` or `aria-labelledby` on the field.
8686+8787+```tsx
8888+<TextField aria-label="Search" name="search" placeholder="Search" />
8989+```
9090+9191+## Primitive TextInput
9292+9393+The lower-level `TextInput` primitive is available when you need the input without the label,
9494+description, or error slots that `Field` provides.
9595+9696+```ts
9797+import { TextInput } from '@luke-ui/react/text-field/primitive';
9898+```
9999+100100+## Props
101101+102102+<auto-type-table path="packages/@luke-ui/react/src/text-field/index.tsx" name="TextFieldProps" />
···33description: Emoji rendering with a reliable screen reader label.
44---
5566-import { story } from '../../../../src/emoji/emoji.story';
66+Use `Emoji` when an emoji needs a reliable accessible label.
7788-<story.WithControl />
88+<ExampleBlock
99+ component="emoji"
1010+ name="basic"
1111+ title="Emoji — Basic"
1212+ description="Emoji with accessible label and typography controls."
1313+/>
9141010-<include>../../../../../../packages/@luke-ui/react/docs/emoji.md</include>
1515+`Emoji` accepts all `Text` props except `children` and `elementType`, so it inherits typography
1616+controls such as `fontSize` and `color`.
1717+1818+## Best practices
1919+2020+| Guidance | Practices |
2121+| -------- | ---------------------------------------------------------------------------------------------------------- |
2222+| Do | Write a `label` that describes the emoji's meaning in context, such as "Celebration". |
2323+| Don't | Repeat text that already appears in the surrounding sentence. That creates duplicate screen reader output. |
2424+2525+## Accessibility
2626+2727+Both `emoji` and `label` are required. Screen readers announce the label instead of the raw emoji
2828+character, which is not announced consistently across platforms.
2929+3030+## Props
3131+3232+<auto-type-table path="packages/@luke-ui/react/src/emoji/index.tsx" name="EmojiProps" />
···33description: Semantic heading with automatic level management.
44---
5566-import { story } from '../../../../src/heading/heading.story';
66+Use `Heading` for section headings. It can infer heading level from `HeadingLevels` context, or you
77+can pass `level` directly.
7888-<story.WithControl />
99+```tsx
1010+<Heading>Section title</Heading>
1111+```
9121010-<include>../../../../../../packages/@luke-ui/react/docs/heading.md</include>
1313+```tsx
1414+<Heading level={2}>Explicit h2</Heading>
1515+```
1616+1717+## Best practices
1818+1919+| Guidance | Practices |
2020+| -------- | ------------------------------------------------------------------------------------------------------------- |
2121+| Do | Prefer automatic leveling for nested sections instead of hardcoding every `level`. |
2222+| Don't | Skip heading levels, such as an h2 followed directly by an h4. Screen reader users navigate by heading level. |
2323+2424+## Automatic leveling
2525+2626+`Heading` reads its level from `HeadingLevels` context. Set `base` on the root context. Each nested
2727+`HeadingLevels` advances the next heading level.
2828+2929+<ExampleBlock
3030+ component="heading"
3131+ name="automatic-leveling"
3232+ title="Heading — Automatic leveling"
3333+ description="Heading levels are inferred from nested HeadingLevels context."
3434+/>
3535+3636+The `level` prop overrides context for one heading without changing the nesting depth for siblings
3737+or children.
3838+3939+## Typography
4040+4141+`Heading` accepts all `Text` props except `fontSize`, which is controlled by the heading level. By
4242+default it applies `fontWeight="bold"` and `lineHeight="tight"`.
4343+4444+<ExampleBlock
4545+ component="heading"
4646+ name="typography"
4747+ title="Heading — Typography"
4848+ description="Heading color and font weight can be customized with Text props."
4949+/>
5050+5151+## Props
5252+5353+<auto-type-table path="packages/@luke-ui/react/src/heading/index.tsx" name="HeadingProps" />
···33description: Locale-aware number formatting powered by Intl.NumberFormat.
44---
5566-import { story } from '../../../../src/numeral/numeral.story';
66+`Numeral` formats numbers with `Intl.NumberFormat`. It respects locale from React Aria's
77+`I18nProvider`.
7888-<story.WithControl />
99+<ExampleBlock
1010+ component="numeral"
1111+ name="basic"
1212+ title="Numeral — Basic"
1313+ description="Default locale-aware decimal formatting."
1414+/>
9151010-<include>../../../../../../packages/@luke-ui/react/docs/numeral.md</include>
1616+## Formats
1717+1818+`Numeral` infers `format` from `currency` or `unit` when omitted. Pass `format` explicitly for
1919+`'percent'` or `'decimal'`, or when you want to be direct about currency and unit formatting.
2020+2121+<ExampleBlock
2222+ component="numeral"
2323+ name="formats"
2424+ title="Numeral — Formats"
2525+ description="Percent, currency, unit, and decimal number formats."
2626+/>
2727+2828+## Compact notation
2929+3030+Use `abbreviate` for compact notation.
3131+3232+<ExampleBlock
3333+ component="numeral"
3434+ name="compact"
3535+ title="Numeral — Compact notation"
3636+ description="Short and long compact notation for large numbers."
3737+/>
3838+3939+## Precision
4040+4141+Pass a number for fixed fraction digits, or a `[min, max]` tuple for a range.
4242+4343+<ExampleBlock
4444+ component="numeral"
4545+ name="precision"
4646+ title="Numeral — Precision"
4747+ description="Fixed fraction digits or a minimum and maximum range."
4848+/>
4949+5050+## Constraints
5151+5252+`Numeral` throws in development when:
5353+5454+- both `currency` and `unit` are provided.
5555+- `format="currency"` is used without a `currency` code.
5656+- `format="unit"` is used without a `unit` value.
5757+- `precision` is not a non-negative integer or valid `[min, max]` tuple.
5858+5959+## Props
6060+6161+<auto-type-table path="packages/@luke-ui/react/src/numeral/index.tsx" name="NumeralProps" />
···33description: Styled text with token-driven typography controls.
44---
5566-import { story } from '../../../../src/text/text.story';
66+`Text` expects the Luke UI theme class at the app or root level. See
77+[Getting Started](/docs/getting-started).
7888-<story.WithControl />
99+Use `Text` for styled text that should not create heading semantics.
9101010-<include>../../../../../../packages/@luke-ui/react/docs/text.md</include>
1111+```tsx
1212+<Text>The quick brown fox jumps over the lazy dog.</Text>
1313+```
1414+1515+## Best practices
1616+1717+| Guidance | Practices |
1818+| -------- | ----------------------------------------------------------------------------------------------------------- |
1919+| Do | Use `fontSize` tokens, such as `'h2'`, instead of arbitrary values so text stays consistent across the app. |
2020+| Don't | Use `Text` for section headings. Use `Heading`, which manages semantic level automatically. |
2121+2222+## Typography
2323+2424+<ExampleBlock
2525+ component="text"
2626+ name="typography"
2727+ title="Text — Typography"
2828+ description="Use font size, color, font family, and weight tokens to style text."
2929+/>
3030+3131+See the token reference below for valid `color`, `fontFamily`, `fontSize`, `lineHeight`, and
3232+`fontWeight` values.
3333+3434+## Text transform and decoration
3535+3636+<ExampleBlock
3737+ component="text"
3838+ name="transform"
3939+ title="Text — Transform and decoration"
4040+ description="Apply text transform and decoration to emphasize content."
4141+/>
4242+4343+## Truncation
4444+4545+<ExampleBlock
4646+ component="text"
4747+ name="truncation"
4848+ title="Text — Truncation"
4949+ description="Clamp lines, truncate to one line, or disable cap-height trim."
5050+/>
5151+5252+## Alignment and numeric glyphs
5353+5454+<ExampleBlock
5555+ component="text"
5656+ name="alignment"
5757+ title="Text — Alignment and numeric glyphs"
5858+ description="Align text and set numeric glyph styles such as tabular numerals."
5959+/>
6060+6161+## Token reference
6262+6363+### `color` tokens
6464+6565+`neutralSubtle`, `neutralBold`, `neutralDisabled`, `neutralBoldInverted`, `positive`, `informative`,
6666+`caution`, `critical`, `inherit`
6767+6868+### `fontFamily` tokens
6969+7070+`sans`, `mono`
7171+7272+### `fontSize` tokens
7373+7474+`xxsmall`, `xsmall`, `small`, `standard`, `medium`, `large`, `xlarge`, `xxlarge`, `h1`, `h2`, `h3`,
7575+`h4`, `h5`, `h6`
7676+7777+### `lineHeight` tokens
7878+7979+`nospace`, `tight`, `loose`
8080+8181+### `fontWeight` tokens
8282+8383+`regular`, `medium`, `bold`, `inherit`
8484+8585+### `textDecoration` values
8686+8787+`none`, `underline`, `line-through`, `inherit`
8888+8989+### `textTransform` values
9090+9191+`none`, `capitalize`, `uppercase`, `lowercase`, `inherit`
9292+9393+## When to use vs Heading
9494+9595+Use `Heading` for actual section headings because it manages semantic level nesting automatically.
9696+9797+Use `Text` with a heading-sized `fontSize` token when content should look like a heading but is not
9898+semantically one, such as a large stat number.
9999+100100+## Props
101101+102102+<auto-type-table path="packages/@luke-ui/react/src/text/index.tsx" name="TextProps" />
···33description: SVG icon component backed by the generated spritesheet.
44---
5566-import { story } from '../../../../src/icon/icon.story';
66+`Icon` renders a symbol from the Luke UI spritesheet. It requires an `IconSpritesheetProvider`
77+ancestor. See Setup below.
7888-<story.WithControl />
99+<ExampleBlock
1010+ component="icon"
1111+ name="basic"
1212+ title="Icon — Basic"
1313+ description="Standalone and decorative icons from the spritesheet."
1414+/>
9151010-<include>../../../../../../packages/@luke-ui/react/docs/icon.md</include>
1616+## Best practices
1717+1818+| Guidance | Practices |
1919+| -------- | --------------------------------------------------------------------------------------------------------- |
2020+| Do | Pass `title` when the icon conveys meaning without adjacent text, such as a standalone status icon. |
2121+| Don't | Pass both `aria-hidden` and `title`. A titled icon should be exposed to assistive technology, not hidden. |
2222+2323+## Setup
2424+2525+Wrap your app with `IconSpritesheetProvider`.
2626+2727+- Source asset: `@luke-ui/react/spritesheet.svg`, exported from `./dist/spritesheet.svg`
2828+- Runtime lookup: `<configured-sprite-href>#<icon-name>`
2929+3030+```tsx
3131+<IconSpritesheetProvider href="/assets/spritesheet.svg">
3232+ <App />
3333+</IconSpritesheetProvider>
3434+```
3535+3636+Vite and Storybook should import the spritesheet as a URL.
3737+3838+```ts
3939+import spriteSheetHref from '@luke-ui/react/spritesheet.svg?url&no-inline';
4040+4141+<IconSpritesheetProvider href={spriteSheetHref}>
4242+ <App />
4343+</IconSpritesheetProvider>;
4444+```
4545+4646+The `no-inline` query avoids `data:` URLs, which can break `<use href="...#icon-id">` rendering.
4747+4848+When developing `@luke-ui/react`, generate icons from `packages/@luke-ui/react/icons/*.svg` into
4949+`packages/@luke-ui/react/dist/spritesheet.svg`.
5050+5151+```bash
5252+pnpm --dir packages/@luke-ui/react run generate:icons
5353+```
5454+5555+## How it works
5656+5757+`Icon` renders an `<svg>` that references a symbol in the generated spritesheet with
5858+`<use href="..." />`.
5959+6060+- `name` chooses the symbol id.
6161+- The sprite URL comes from `IconSpritesheetProvider`.
6262+- `viewBox` defaults to the generated icon viewBox.
6363+- Icon fill follows `currentColor`.
6464+6565+```tsx
6666+<Icon name="add" className="myIcon" style={{ color: 'tomato' }} />
6767+```
6868+6969+## Create your own icon
7070+7171+Use `createIcon` for a one-off icon that is not in the generated spritesheet.
7272+7373+<ExampleBlock
7474+ component="icon"
7575+ name="custom"
7676+ title="Icon — Custom"
7777+ description="Create a one-off icon that is not in the generated spritesheet."
7878+/>
7979+8080+## Accessibility
8181+8282+If `title` is provided, the icon is exposed to assistive technology with `role="img"`. If `title` is
8383+omitted, `aria-hidden` defaults to `true`.
8484+8585+## Props
8686+8787+<auto-type-table path="packages/@luke-ui/react/src/icon/index.tsx" name="IconProps" />