diff --git a/.changeset/README.md b/.changeset/README.md index e5b6d8d6..84fd1276 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -1,8 +1,9 @@ # Changesets -Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works -with multi-package repos, or single-package repos to help you version and publish your code. You can -find the full documentation for it [in our repository](https://github.com/changesets/changesets) +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool +that works with multi-package repos, or single-package repos to help you version and publish your +code. You can find the full documentation for it +[in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/AGENTS.md b/AGENTS.md index 0fd62cf3..19085db6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,11 +1,11 @@ -# Luke UI Agent Guide +# Luke UI agent guide -- Use `catalog:` in `package.json` for dependency versions (catalog in `pnpm-workspace.yaml`). Do - not add raw versions. -- See [docs/CONVENTIONS.md](docs/CONVENTIONS.md), [docs/STYLING.md](docs/STYLING.md), and - [docs/TESTING.md](docs/TESTING.md) for conventions, styling, and testing. -- Run tasks through turbo from the repo root (`pnpm run check`, `pnpm run build`, …). Running - package-local scripts directly skips turbo's `generate` dependencies, so generated files - (`.generated/`, `routeTree.gen.ts`, spritesheet) may be missing. +- Use `catalog:` for dependency versions in `package.json`. The catalog lives in + `pnpm-workspace.yaml`. Do not add raw versions. +- Read [docs/CONVENTIONS.md](docs/CONVENTIONS.md), [docs/STYLING.md](docs/STYLING.md), and + [docs/TESTING.md](docs/TESTING.md) before changing code, styles, or tests. +- Run tasks through Turbo from the repo root, for example `pnpm run check` or `pnpm run build`. + Package-local scripts can skip Turbo `generate` dependencies, which may leave generated files + missing. - Scaffold components non-interactively: `pnpm run generate:component --args `. diff --git a/CONTEXT.md b/CONTEXT.md index d02e93f4..c44e2a94 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,56 +1,64 @@ # 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. +Luke UI is a React design system built on `react-aria-components` and `vanilla-extract`. + +Components use three vocabulary tiers. Those tiers define how code is laid out and how each +component is 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. +**Atom**: A component that presents one conceptual unit, such as one piece of text, one icon, or one +number. It may compose `Text` or other atoms internally, but consumers treat it as a single unit. +App developers use atoms 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`, `TextField`, -`ComboboxField`. +**Composed**: A component that combines atoms or primitives into an opinionated, ready-to-drop-in +unit for app developers. _Examples_: `Button`, `IconButton`, `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**. +**Primitive**: A building block for library authors assembling the next composed component. App +developers are not the audience. A primitive 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`), `Combobox*` kit (via `combobox-field/primitive`), `button/primitive`, +`field/primitive`, and the composed-but-internal `Field`. _Avoid_: "base" and "raw". Use +**Primitive**. -**Component creation**: the act of adding a new Atom or Composed component and every public surface -needed for it to exist consistently. It includes source, stories, package docs, hosted docs, recipes -when needed, and export refresh. +**Component creation**: The act of adding the public surface a new Atom or Composed component needs +to exist consistently. That includes source, stories, package docs, hosted docs, recipes when +needed, and generated exports. ## Relationships -- A **Composed** component is built from one or more **Atoms** and/or **Primitives**. -- A **Primitive** is documented in package docs (reachability for library authors and agents) but - omitted from hosted docs and the primary navigation index. -- An **Atom** and a **Composed** component each get a public doc page on both surfaces. -- 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. +- A **Composed** component is built from one or more **Atoms** or **Primitives**. +- A **Primitive** is documented in package docs so library authors and agents can find it. It is + omitted from the hosted docs primary navigation and index. +- The composed `Field` is a **Primitive** by audience, even though it composes other components. It + exists for library authors building `TextField`, `ComboboxField`, and similar components. +- An **Atom** or **Composed** component gets a hosted docs page and a primary package-doc entry. + +## Package docs shape -## Docs prose structure +Each `src//.docs.md` follows the section order from +[ADR-0006](docs/adr/0006-docs-md-structure-standard.md): -Every `.docs.md` file follows the same section order: Usage lead-in, Best Practices table, Feature -sections (ordered by importance to a typical consumer, not alphabetically), Accessibility, then -cross-reference sections last. All prose is authored in the package -(`src//.docs.md`); `apps/docs` MDX files are wiring only — frontmatter, an -interactive demo, and an `` of the generated package doc. Full rationale in -[ADR-0006](docs/adr/0006-docs-md-structure-standard.md). +- Usage lead-in with no explicit `## Usage` heading. +- `## Best Practices` table. +- Feature sections ordered by importance to a typical consumer. +- `## Accessibility` when the component has a user-facing accessibility contract. +- Cross-reference sections last. + +All prose is authored in the package (`src//.docs.md`). `apps/docs` MDX files +only wire the page together with frontmatter, an interactive demo, and an `` for the +generated package doc. Full rationale is in [ADR-0006](docs/adr/0006-docs-md-structure-standard.md). ## Docs rule -Two doc surfaces, two audiences: +Two doc surfaces serve two audiences: -- **Hosted docs** (`apps/docs`) — for app developers. Document what an app developer drops into - their UI. -- **Package docs** (shipped on npm under `packages/@luke-ui/react/docs/`) — for anyone reading the - package off npm, including library authors and coding agents. Document every public export path - because each one is reachable through `package.json#exports`. +- **Hosted docs** (`apps/docs`): for app developers. They document components an app developer can + drop into a UI. +- **Package docs** (shipped on npm under `packages/@luke-ui/react/docs/`): for anyone reading the + package off npm, including library authors and coding agents. They document every public export + path because each one is reachable through `package.json#exports`. | Tier | Hosted docs? | Package docs? | | --------- | ----------------- | ---------------------- | @@ -58,19 +66,18 @@ Two doc surfaces, two audiences: | Composed | yes (primary nav) | yes (primary index) | | Primitive | no | yes (specialist index) | -**Specialist, not noise.** Primitive pages exist in the package docs so library authors and agents -can find them, but they are listed in a separate, de-emphasised "Library authors / advanced" section -of `README.md` and `llms.txt` — never mixed into the primary index alongside atoms and composed -components. The goal is reachability without crowding the main path. - -Don't document what isn't part of the public API. +**Specialist, not noise.** Primitive pages exist in package docs so library authors and agents can +find them. They are listed in a separate, de-emphasised "Library authors / advanced" section in +`README.md` and `llms.txt`, never mixed into the primary index alongside atoms and composed +components. The goal is reachability without crowding the main path. Do not document anything that +is not part of the public API. ## 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 +- [ADR-0001](docs/adr/0001-component-tier-taxonomy.md): Three-tier taxonomy docs rule +- [ADR-0002](docs/adr/0002-primitive-package-path-convention.md): Primitive kits are exported at `[composed]/primitive` -- [ADR-0003](docs/adr/0003-package-docs-surface.md) — Package docs are a separate AI-native surface -- [ADR-0004](docs/adr/0004-styling-utilities-public-api.md) — Styling utilities as public API -- [ADR-0006](docs/adr/0006-docs-md-structure-standard.md) — Standard structure for `.docs.md` prose +- [ADR-0003](docs/adr/0003-package-docs-surface.md): Package docs are a separate AI-native surface +- [ADR-0004](docs/adr/0004-styling-utilities-public-api.md): Styling utilities public API +- [ADR-0006](docs/adr/0006-docs-md-structure-standard.md): Standard structure for `.docs.md` prose files diff --git a/README.md b/README.md index 0adbba37..5824e492 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,35 @@ # Luke UI 5 -React design system using `vanilla-extract`. +Luke UI is a React design system built with `react-aria-components` and `vanilla-extract`. ## Setup - `pnpm install` -- `pnpm dev` - Start dev -- `pnpm build` - Build all -- `pnpm check` - Lint, format, types -- `pnpm test` - Run all tests (unit, Storybook, visual regression) +- `pnpm dev`: start the docs app. +- `pnpm build`: build all packages and apps. +- `pnpm check`: run lint, format, and type checks. +- `pnpm test`: run unit, Storybook, and visual regression tests. ## Stack - **Monorepo**: pnpm + Turbo -- **React**: `react-aria-components` base -- **Styling**: `vanilla-extract` (static CSS) +- **React**: built on `react-aria-components` +- **Styling**: `vanilla-extract` static CSS - **Lint/Format**: `oxlint` + `oxfmt` -## UI Package (`@luke-ui/react`) +## UI package (`@luke-ui/react`) - Tokens: `src/tokens.ts` - Theme: `src/theme/` - Styles: `src/styles/` -- Build: `tsdown` -> `dist/stylesheet.css` +- Build: `tsdown` writes `dist/stylesheet.css` ## Contributing -- Use `pnpm changeset` for versions. +- Use `pnpm changeset` for version changes. - Run `pnpm check` before committing. -## CI Setup +## CI setup - Argos + Storybook GitHub setup: `docs/ARGOS_SETUP.md` - Visual regression test command: `pnpm test` @@ -53,20 +53,20 @@ Components to build: - [ ] SkipTo - [ ] VisuallyHidden -### Media and visuals +### Media visuals - [ ] Avatar - [x] Icon - [ ] Illustration -### Typography and content +### Typography content - [x] Emoji - [x] Heading - [x] Numeral - [x] Text -### Feedback and status indicators +### Feedback status indicators - [ ] Badge - [ ] EmptyState @@ -76,7 +76,7 @@ Components to build: - [ ] Toast - [x] LoadingSpinner -### Layout and structure +### Layout structure - [ ] Breadcrumbs - [ ] Card diff --git a/apps/docs/content/docs/components/actions/button.mdx b/apps/docs/content/docs/components/actions/button.mdx index d9bb6c73..4d1dc5d0 100644 --- a/apps/docs/content/docs/components/actions/button.mdx +++ b/apps/docs/content/docs/components/actions/button.mdx @@ -1,6 +1,6 @@ --- title: Button -description: Button component with size and tone variants. +description: Action button with tone, size, icon, pending, and disabled states. --- import { story } from '../../../../src/button/button.story'; diff --git a/apps/docs/content/docs/components/actions/icon-button.mdx b/apps/docs/content/docs/components/actions/icon-button.mdx index d8f95d75..7babd06a 100644 --- a/apps/docs/content/docs/components/actions/icon-button.mdx +++ b/apps/docs/content/docs/components/actions/icon-button.mdx @@ -1,6 +1,6 @@ --- title: Icon Button -description: Button that renders only an icon. +description: Compact icon-only action button with an accessible label. --- import { story } from '../../../../src/icon-button/icon-button.story'; diff --git a/apps/docs/content/docs/components/actions/link.mdx b/apps/docs/content/docs/components/actions/link.mdx index e5dc8697..86dc3298 100644 --- a/apps/docs/content/docs/components/actions/link.mdx +++ b/apps/docs/content/docs/components/actions/link.mdx @@ -1,6 +1,6 @@ --- title: Link -description: Link component for inline and standalone navigation with tone variants. +description: Link component for inline and standalone navigation. --- import { story } from '../../../../src/link/link.story'; diff --git a/apps/docs/content/docs/components/feedback/loading-skeleton.mdx b/apps/docs/content/docs/components/feedback/loading-skeleton.mdx index 08e3ebc6..8daacbbc 100644 --- a/apps/docs/content/docs/components/feedback/loading-skeleton.mdx +++ b/apps/docs/content/docs/components/feedback/loading-skeleton.mdx @@ -1,6 +1,6 @@ --- title: Loading Skeleton -description: Placeholder that mirrors the layout of loading content, with synchronised pulsing. +description: Loading placeholder that keeps the same footprint as the final content. --- import { story } from '../../../../src/loading-skeleton/loading-skeleton.story'; diff --git a/apps/docs/content/docs/components/feedback/loading-spinner.mdx b/apps/docs/content/docs/components/feedback/loading-spinner.mdx index 802e0882..b044b56d 100644 --- a/apps/docs/content/docs/components/feedback/loading-spinner.mdx +++ b/apps/docs/content/docs/components/feedback/loading-spinner.mdx @@ -1,6 +1,6 @@ --- title: Loading Spinner -description: Spinner component for indeterminate and determinate progress. +description: Spinner for indeterminate and determinate progress. --- import { story } from '../../../../src/loading-spinner/loading-spinner.story'; diff --git a/apps/docs/content/docs/components/forms/combobox-field.mdx b/apps/docs/content/docs/components/forms/combobox-field.mdx index 56f0396c..587a1eac 100644 --- a/apps/docs/content/docs/components/forms/combobox-field.mdx +++ b/apps/docs/content/docs/components/forms/combobox-field.mdx @@ -1,6 +1,6 @@ --- title: Combobox Field -description: Documentation for the Combobox Field component. +description: Single-select combobox field with label, validation, and async options. --- import { story } from '../../../../src/combobox-field/combobox-field.story'; diff --git a/apps/docs/content/docs/components/forms/text-field.mdx b/apps/docs/content/docs/components/forms/text-field.mdx index b139432f..0e7b9aa3 100644 --- a/apps/docs/content/docs/components/forms/text-field.mdx +++ b/apps/docs/content/docs/components/forms/text-field.mdx @@ -1,6 +1,6 @@ --- title: Text Field -description: Composed single-line text input with label and validation. +description: Single-line text input with label, validation, and adornments. --- import { story } from '../../../../src/text-field/text-field.story'; diff --git a/apps/docs/content/docs/components/typography/emoji.mdx b/apps/docs/content/docs/components/typography/emoji.mdx index e0739c3d..ad0d426f 100644 --- a/apps/docs/content/docs/components/typography/emoji.mdx +++ b/apps/docs/content/docs/components/typography/emoji.mdx @@ -1,6 +1,6 @@ --- title: Emoji -description: Accessible emoji rendering with screen reader label. +description: Emoji rendering with a reliable screen reader label. --- import { story } from '../../../../src/emoji/emoji.story'; diff --git a/apps/docs/content/docs/components/typography/heading.mdx b/apps/docs/content/docs/components/typography/heading.mdx index 919bb78b..aa45b905 100644 --- a/apps/docs/content/docs/components/typography/heading.mdx +++ b/apps/docs/content/docs/components/typography/heading.mdx @@ -1,6 +1,6 @@ --- title: Heading -description: Auto-leveling semantic heading with token-based typography. +description: Semantic heading with automatic level management. --- import { story } from '../../../../src/heading/heading.story'; diff --git a/apps/docs/content/docs/components/typography/numeral.mdx b/apps/docs/content/docs/components/typography/numeral.mdx index 9fbd5851..458df5db 100644 --- a/apps/docs/content/docs/components/typography/numeral.mdx +++ b/apps/docs/content/docs/components/typography/numeral.mdx @@ -1,6 +1,6 @@ --- title: Numeral -description: Locale-aware number formatting with Intl.NumberFormat. +description: Locale-aware number formatting powered by Intl.NumberFormat. --- import { story } from '../../../../src/numeral/numeral.story'; diff --git a/apps/docs/content/docs/components/typography/text.mdx b/apps/docs/content/docs/components/typography/text.mdx index 402a1a15..fe6f64d2 100644 --- a/apps/docs/content/docs/components/typography/text.mdx +++ b/apps/docs/content/docs/components/typography/text.mdx @@ -1,6 +1,6 @@ --- title: Text -description: Typography component for rendering body copy and headings with token-driven sizing. +description: Styled text with token-driven typography controls. --- import { story } from '../../../../src/text/text.story'; diff --git a/apps/docs/content/docs/components/visuals/icon.mdx b/apps/docs/content/docs/components/visuals/icon.mdx index 7ae501bb..18acc53f 100644 --- a/apps/docs/content/docs/components/visuals/icon.mdx +++ b/apps/docs/content/docs/components/visuals/icon.mdx @@ -1,6 +1,6 @@ --- title: Icon -description: SVG icon component powered by the generated spritesheet. +description: SVG icon component backed by the generated spritesheet. --- import { story } from '../../../../src/icon/icon.story'; diff --git a/apps/docs/content/docs/getting-started.mdx b/apps/docs/content/docs/getting-started.mdx index 7c59194a..116e5557 100644 --- a/apps/docs/content/docs/getting-started.mdx +++ b/apps/docs/content/docs/getting-started.mdx @@ -1,21 +1,30 @@ --- title: Getting Started -description: Install the package and render your first component. +description: Install Luke UI and render your first component. --- ## Install -For external consumers: +External consumers install the package from the registry. ```bash pnpm add @luke-ui/react ``` -For this monorepo, use workspace dependencies (for example `"@luke-ui/react": "workspace:*"`). +Inside this monorepo, use a workspace dependency instead. -## Use a Component +```json +{ + "dependencies": { + "@luke-ui/react": "workspace:*" + } +} +``` + +## Render a component -Apply the Luke UI theme classes once near the top of your app so components resolve design token CSS variables and scoped reset rules. +Apply the Luke UI theme class near the top of your app. Components use it to resolve design-token +CSS variables and scoped reset rules. ```tsx import lukeUiStyles from '@luke-ui/react/stylesheet.css?url'; @@ -34,7 +43,9 @@ export function App() { } ``` -## Use a Component (Minimal) +## Minimal example + +For small demos, import the stylesheet and render a component directly. ```tsx import lukeUiStyles from '@luke-ui/react/stylesheet.css?url'; @@ -50,7 +61,9 @@ export function Example() { } ``` -## Manual Root Class (Advanced) +## Manual root class + +Use `themeRootClassName` when you need to attach the theme class yourself. ```tsx import { themeRootClassName } from '@luke-ui/react/theme'; @@ -58,11 +71,11 @@ import { themeRootClassName } from '@luke-ui/react/theme';
; ``` -## Next Steps +## Next steps -- Read the [Button](/docs/components/actions/button) docs. -- 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 [Text](/docs/components/typography/text) docs. -- Read the [Icon](/docs/components/visuals/icon) docs. +- Read [Button](/docs/components/actions/button). +- Read [Link](/docs/components/actions/link). +- Read [Loading Spinner](/docs/components/feedback/loading-spinner). +- Read [Combobox Field](/docs/components/forms/combobox-field). +- Read [Text](/docs/components/typography/text). +- Read [Icon](/docs/components/visuals/icon). diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx index 67f22823..855f2dc4 100644 --- a/apps/docs/content/docs/index.mdx +++ b/apps/docs/content/docs/index.mdx @@ -1,9 +1,9 @@ --- title: Design System -description: Documentation for the Luke UI React UI package. +description: Documentation for the Luke UI React package. --- -Use these docs to explore foundations, components, and usage patterns for `@luke-ui/react`. +Use these docs to learn the foundations, components, and usage patterns in `@luke-ui/react`. diff --git a/apps/docs/src/routes/docs/$.tsx b/apps/docs/src/routes/docs/$.tsx index 74acb15a..70d83a8a 100644 --- a/apps/docs/src/routes/docs/$.tsx +++ b/apps/docs/src/routes/docs/$.tsx @@ -29,7 +29,7 @@ export const Route = createFileRoute('/docs/$')({ const loader = createServerFn({ method: 'GET', }) - .inputValidator((slugs) => z.array(z.string()).parse(slugs)) + .validator((slugs) => z.array(z.string()).parse(slugs)) // staticFunctionMiddleware breaks Vite HMR in dev — only apply in prod build. .middleware(import.meta.env.PROD ? [staticFunctionMiddleware] : []) .handler(async ({ data: slugs }) => { diff --git a/docs/ARGOS_SETUP.md b/docs/ARGOS_SETUP.md index 1f2fa6f7..c56312c4 100644 --- a/docs/ARGOS_SETUP.md +++ b/docs/ARGOS_SETUP.md @@ -1,31 +1,31 @@ -# Argos + Storybook GitHub Setup +# Argos + Storybook GitHub setup -The repository uses Argos visual testing through the Storybook Vitest -integration. +The repository uses Argos visual testing through the Storybook Vitest integration. -## Required GitHub Settings +## Required GitHub settings 1. Add repository secret: - - Go to `Settings` -> `Secrets and variables` -> `Actions`. - - Click `New repository secret`. - - Name: `ARGOS_TOKEN` - - Secret: Argos project token from Argos project settings. + +- Go to `Settings` -> `Secrets variables` -> `Actions`. +- Click `New repository secret`. +- Name: `ARGOS_TOKEN` +- Secret: Argos project token from Argos project settings. 2. Enable GitHub Pages workflow deploy: - - Go to `Settings` -> `Pages`. - - Under `Build and deployment`, set `Source` to `GitHub Actions`. -## Where Secret Is Used +- Go to `Settings` -> `Pages`. +- Under `Build deployment`, set `Source` to `GitHub Actions`. + +## Where secret used - Workflow: `.github/workflows/storybook.yml` -- `visual-tests` injects `ARGOS_TOKEN` into - `pnpm --filter @luke-ui/react test`. -- `visual-tests` builds Storybook and runs `pnpm deploy:storybook:argos` - so Argos can add the PR preview/comment. -- Fork PRs do not receive repository secrets. Tests still run, but Argos upload - is skipped automatically. -- `deploy-pages` passes `STORYBOOK_BASE_PATH` from - `actions/configure-pages@v5` to the Storybook build. +- `visual-tests` injects `ARGOS_TOKEN` into `pnpm --filter @luke-ui/react test`. +- `visual-tests` builds Storybook and runs `pnpm deploy:storybook:argos` so Argos can add a PR + preview or comment. +- Fork PRs do not receive repository secrets. Tests still run, but Argos upload is skipped + automatically. +- `deploy-pages` passes `STORYBOOK_BASE_PATH` through `actions/configure-pages@v5` for the Storybook + build. - Turbo config: `turbo.json` - `build:storybook` includes `STORYBOOK_BASE_PATH` in its `env` list. - Vitest config: `packages/@luke-ui/react/vitest.config.ts` @@ -35,31 +35,32 @@ integration. - `deploy:storybook:argos` deploys built Storybook static files to Argos. - Workspace script: `packages/@luke-ui/react/package.json` - `test` runs `tsx scripts/test-visual.ts`. -- The script uses existing `ARGOS_TOKEN` if set. Otherwise it loads local - package `.env.local` when present, enables `ARGOS_UPLOAD=1` only when - `ARGOS_TOKEN` is available, and runs the Storybook Vitest suite. +- The script uses an existing `ARGOS_TOKEN` if set. Otherwise it loads the local package + `.env.local` when present, enables `ARGOS_UPLOAD=1` only when `ARGOS_TOKEN` is available, and runs + the Storybook Vitest suite. -## Verify Configuration +## Verify configuration -1. Push to `main` or run the workflow manually. +1. Push `main` and run the workflow manually. 2. Confirm the `Storybook` workflow succeeds: - - `visual-tests` uploads screenshots and deploys Storybook to Argos. - - `deploy-pages` publishes Storybook on `main`. -3. Open a PR with a small Storybook-visible change and verify Argos adds the PR - comment/check. -## Local Testing +- `visual-tests` uploads screenshots and deploys Storybook to Argos. +- `deploy-pages` publishes Storybook on `main`. + +3. Open a PR with a small Storybook-visible change and verify Argos adds a PR comment or check. + +## Local testing Use `packages/@luke-ui/react/.env.local` for local runs. 1. Set `ARGOS_TOKEN` in `packages/@luke-ui/react/.env.local`. 2. Run `corepack pnpm --filter @luke-ui/react test`. -This command runs the Storybook Vitest suite and enables Argos upload -automatically when `ARGOS_TOKEN` is present. +This command runs the Storybook Vitest suite and enables Argos upload automatically when +`ARGOS_TOKEN` is present. -## Security Notes +## Security notes - Do not commit tokens in source files. -- If the token is ever committed or shared, rotate it in Argos and update the - `ARGOS_TOKEN` GitHub secret. +- If the token is ever committed or shared, rotate it in Argos and update the `ARGOS_TOKEN` GitHub + secret. diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index e7a0f393..13fb91fa 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -2,82 +2,102 @@ ## TypeScript -Strict mode. Use `import type`. Explicit `.js` extensions for local imports. +Use strict TypeScript. Import types with `import type`. Local imports include the explicit `.js` +extension. ## Formatting -Managed by `oxfmt`. Tabs, 2 width, 80 width, single quotes (TS), double quotes (JSX). +`oxfmt` owns formatting. The repo uses tabs with width 2, 100-column wrapping, single quotes in +TypeScript, and double quotes in JSX. ## Naming -- **Components**: `PascalCase` (`Button.tsx`) -- **Props**: `PascalCaseProps` (`ButtonProps`) -- **Files**: `kebab-case` (`icon-button.tsx`) +- **Components**: `PascalCase`, for example `Button.tsx` +- **Props**: `PascalCaseProps`, for example `ButtonProps` +- **Files**: `kebab-case`, for example `icon-button.tsx` - **CSS**: `*.css.ts` - **Stories**: `*.stories.tsx` ## Testing -See [TESTING.md](TESTING.md) for choosing a test type, placement, and how to write tests. The short -version: use the smallest test surface that proves the behavior, colocate tests with the source -they cover, test behavior through public APIs and role-based queries, and start bugfixes with a -failing test. +See [TESTING.md](TESTING.md) for test type, placement, and writing rules. + +Short version: + +- Use the smallest test surface that proves the behaviour. +- Colocate tests with the source they cover. +- Test through public APIs and role-based queries. +- Start bug fixes with a failing test that reproduces the bug. ## Component Pattern -Wrap `react-aria-components`. Use `composeRenderProps` for styling. +Components wrap `react-aria-components` and use `composeRenderProps` for styling. -Components follow a three-tier taxonomy (see `CONTEXT.md` for full definitions): +Components follow the three-tier taxonomy from [CONTEXT.md](../CONTEXT.md): -- **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`, - `TextField`, `ComboboxField`). Gets a doc page. -- **Primitive** — building block for library authors only; documented in package docs but not in - hosted docs. May be a single file (e.g. `text-input`) or a multi-file kit (e.g. `combobox/*`, - `field/*`). +- **Atom**: a single conceptual unit used directly by app developers, such as `Text`, `Link`, + `Icon`, `Heading`, `Numeral`, `Emoji`, or `LoadingSpinner`. Atoms get hosted docs pages. +- **Composed**: an app-developer-facing pattern built from atoms or primitives, such as `Button`, + `IconButton`, `TextField`, or `ComboboxField`. Composed components get hosted docs pages. +- **Primitive**: a building block for library authors. Primitives are documented in package docs, + but not in hosted docs. A primitive may be a single file or a multi-file kit. ## 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. +Composed components export at their bare package path, such as `@luke-ui/react/button`. + +Primitive kits that support a composed component export at `[composed]/primitive`, for example: + +- `@luke-ui/react/text-field/primitive` +- `@luke-ui/react/combobox-field/primitive` +- `@luke-ui/react/field/primitive` +- `@luke-ui/react/button/primitive` ## Exports -Managed by `tsdown` entry globs. Do not hand-edit `package.json#exports`. Create files in paths the -package build already discovers. +`tsdown` entry globs manage package exports. Do not hand-edit `package.json#exports`. Create files +in paths the package build already discovers. ## Docs -`.docs.md` structure and section order are defined in -[ADR-0006](adr/0006-docs-md-structure-standard.md). Docs are read by both humans and coding agents — -write for clarity, not just brevity. Don't compress a sentence to the point it's hard to parse, and -don't add content whose only value is illustrating a point from one particular editing session. -`CONTEXT.md` stays a terse index that links to ADRs; if a section there starts restating an ADR's -content in full, that content belongs in the ADR only. +`.docs.md` files follow the structure in [ADR-0006](adr/0006-docs-md-structure-standard.md). Humans +and coding agents both read these docs, so optimise for clarity, not only brevity. + +Headings use sentence case: capitalise only the first word and proper nouns. + +Do not compress a sentence until the point is hard to parse. Do not add content whose only value is +explaining one editing session. + +`CONTEXT.md` stays a terse index that links to ADRs. If a section starts restating an ADR in full, +the content belongs in the ADR instead. ## Fumadocs stories -Each component's interactive demo on the hosted docs site is a single file, -`apps/docs/src//.story.tsx`, built on `@fumadocs/story/vite/client` (registered -as a Vite plugin in `apps/docs/vite.config.ts`). It defines a narrow `StoryProps` type — -`Pick<Props, 'a' | 'b' | ...>` — listing only the props a control can meaningfully show: -drop event handlers, refs, and other escape hatches (`className`, `style`, `onPress`, obscure ARIA -props). `Pick` orders the resulting controls by the order keys are listed in the pick, not by their -declaration order in `Props` — `@fumadocs/story` reads properties off the resolved type via -`ts-morph`, which preserves the pick's key order — so list the keys in the order they should appear in -the panel. A small `Playground` wrapper renders the real component inside the shared -`StoryWrapper` (from `../lib/story-wrapper`) and is passed directly to -`defineStory({ Component, args: { initial } })`. A generic component (e.g. `ComboboxField`) fixes -`T` to one concrete sample type in its Playground rather than staying generic — see +Each component's interactive hosted-docs demo lives in one file: +`apps/docs/src//.story.tsx`. + +The story uses `@fumadocs/story/vite/client`, which is registered as a Vite plugin in +`apps/docs/vite.config.ts`. + +Define a narrow `StoryProps` type with `Pick<Props, 'a' | 'b'>`. Include only +props that make useful hosted controls. Drop event handlers, refs, escape hatches such as +`className` and `style`, and obscure ARIA props. + +Control order follows the key order inside `Pick`, not the declaration order in `Props`. +`@fumadocs/story` reads the resolved type with `ts-morph` and preserves the pick order, so list keys +in the order they should appear in the panel. + +Render the real component through a small `Playground` wrapper inside the shared +`StoryWrapper` from `../lib/story-wrapper`. Pass it directly to +`defineStory({ Component, args: { initial } })`. + +Generic components should fix the generic to one concrete sample type inside the playground. See `combobox-field.story.tsx`. -Order the picked props to match the `.docs.md` feature-section order, and keep every prop with its own -`##` feature section represented here — when you add a feature section to `.docs.md`, add the prop to -the story's `Pick` in the same pass. `initial` values stay short and legible, same bar as `.docs.md` -code examples — no lorem ipsum. +Keep story props aligned with `.docs.md` feature sections. If you add a feature section for a prop, +add that prop to the story `Pick` in the same change. + +Initial values should be short and legible, like the `.docs.md` examples. Do not use lorem ipsum. -`.mdx` pages import the story directly — `import { story } from '.../.story'` and -`` — there is no separate client file to wire up. +Hosted `.mdx` files are wiring only: frontmatter, an interactive demo, and an `` for +generated package docs. Do not add hand-authored component prose there. diff --git a/docs/STYLING.md b/docs/STYLING.md index b4712549..9754e9ca 100644 --- a/docs/STYLING.md +++ b/docs/STYLING.md @@ -2,41 +2,42 @@ ## Setup -Static CSS foundation. Users import `@luke-ui/react/stylesheet.css`. Apply `themeRootClassName` -(from `@luke-ui/react/theme`) to host element. +Luke UI ships a static CSS foundation. Consumers import `@luke-ui/react/stylesheet.css` and apply +`themeRootClassName` from `@luke-ui/react/theme` to the host element. ## Structure -- `tokens.ts`: Source of truth. -- `styles/vars.css.ts`: Theme variables. -- `styles/reset.css.ts`: Scoped reset (to `.luke-ui-reset`). -- `recipes/`: Shareable component recipes exported via `@luke-ui/react/recipes`. +- `tokens.ts`: token source of truth. +- `styles/vars.css.ts`: theme variables. +- `styles/reset.css.ts`: reset scoped to `.luke-ui-reset`. +- `recipes/`: component recipes exported through `@luke-ui/react/recipes`. -## CSS Cascade Layers +## CSS cascade layers -All styles are placed in +All styles live in [cascade layers](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers) -to guarantee a predictable override order regardless of source order or specificity. +so override order does not depend on source order or specificity. -| Layer | Purpose | -| ----------- | ------------------------------------------------------------ | -| `reset` | Normalize browser defaults (box-sizing, margins, etc.) | -| `theme` | Design token custom properties and base typographic defaults | -| `recipes` | Component styles (variants, compound variants) | -| `utilities` | One-off overrides; highest-priority escape hatch | +| Layer | Purpose | +| ----------- | -------------------------------------------------- | +| `reset` | Browser defaults such as box sizing and margins | +| `theme` | Design token custom properties and base typography | +| `recipes` | Component styles, variants, and compound variants | +| `utilities` | One-off overrides and layout escape hatches | -Use `styleInLayer` / `recipeInLayer` / `globalStyleInLayer` from `styles/layered-style.css.ts`. -These helpers prevent accidentally writing styles outside a layer. +Use `styleInLayer`, `recipeInLayer`, and `globalStyleInLayer` from `styles/layered-style.css.ts`. +These helpers keep styles inside a named layer. -Overrides that must beat component recipes go in the `utilities` layer, not `!important`. The -exception is styles that must also beat consumers' un-layered and inline styles — layers can't do -that. `LoadingSkeleton` uses `!important` (within the `utilities` layer) for exactly this: it forces -placeholder styles onto arbitrary wrapped children. +Overrides that must beat component recipes belong in the `utilities` layer, not in `!important`. -**Reduced-motion gotcha**: the global `prefers-reduced-motion` rule lives in the `reset` layer — the -lowest — so it cannot disable animations declared in `recipes` or `utilities`. Handle reduced motion -per animated recipe with an `@media (prefers-reduced-motion: reduce)` override (see -`recipes/loading-skeleton.css.ts`). +The exception is styles that must also beat consumers' un-layered or inline styles. Layers cannot +beat those. `LoadingSkeleton` uses `!important` inside the `utilities` layer for this reason: it +must force placeholder styles onto arbitrary wrapped children. + +Reduced motion needs recipe-level handling. The global `prefers-reduced-motion` rule lives in the +`reset` layer, so it cannot disable animations declared in `recipes` or `utilities`. Animated +recipes should add their own `@media (prefers-reduced-motion: reduce)` override. See +`recipes/loading-skeleton.css.ts`. ## Recipes API @@ -46,18 +47,18 @@ Recipes are public and can be imported from `@luke-ui/react/recipes`. import { button, link } from '@luke-ui/react/recipes'; ``` -## Styling Utilities +## Styling utilities Styling utilities are public API, exported from `@luke-ui/react/styles`. They provide token-aware, -type-safe layout and styling helpers for cases where component props are too narrow. +type-safe layout helpers for cases where component props are too narrow. -The current implementation is vendored `rainbow-sprinkles` in `@luke-ui/rainbow-sprinkles`. It -places all generated classes in the `utilities` cascade layer and supports responsive conditions and -pseudo-state conditions out of the box. +The current implementation vendors `rainbow-sprinkles` in `@luke-ui/rainbow-sprinkles`. It places +generated classes in the `utilities` layer and supports responsive values plus `hover` and +`focus-visible` conditions. -### `createSprinkles()` — layout utility +### `createSprinkles()` layout utility -`createSprinkles(props)` returns `{ className, style }`. Spread both onto any element: +`createSprinkles(props)` returns `{ className, style }`. Spread both onto the element. ```tsx import { createSprinkles } from '@luke-ui/react/styles'; @@ -70,19 +71,19 @@ const layout = createSprinkles({ return (
- … + ...
); ``` -Token-backed properties use the design-token scale (e.g. `padding: 'large'`). Enum-like properties -use CSS-native values (e.g. `display: 'flex'`). Numeric flex properties use string primitives -(`flexGrow: '1'`). +Token-backed properties use the design-token scale, for example `padding: 'large'`. Enum-like +properties use CSS-native values, for example `display: 'flex'`. Numeric flex properties use string +primitives, for example `flexGrow: '1'`. ### Responsive values Use object notation keyed by breakpoint names. Values cascade from smaller to larger breakpoints, so -only overrides need to be specified: +only overrides need to be specified. ```tsx const responsive = createSprinkles({ @@ -94,20 +95,24 @@ const responsive = createSprinkles({ ### Pseudo-state conditions -Use condition objects for `hover` and `focus-visible` on supported properties: +Use condition objects for `hover` and `focus-visible` states. ```tsx const interactive = createSprinkles({ padding: 'medium', - backgroundColor: { default: 'neutral', hover: 'neutralHover', focusVisible: 'input' }, + backgroundColor: { + default: 'neutral', + hover: 'neutralHover', + focusVisible: 'input', + }, }); ``` ### React Aria Components `render` prop -When you need to style the underlying DOM element directly, combine `createSprinkles` with RAC's -`render` prop. Use `mergeProps` from `@luke-ui/react/utils` to merge the provided DOM props with -`createSprinkles()` output so `className` and `style` are concatenated correctly: +When you need to style the underlying DOM element directly, combine `createSprinkles` with React +Aria Components' `render` prop. Use `mergeProps` from `@luke-ui/react/utils` so `className` and +`style` are merged correctly. ```tsx import { mergeProps } from '@luke-ui/react/utils'; @@ -125,14 +130,6 @@ const buttonBox = createSprinkles({ padding: 'medium' }); ; ``` -### Shorthands - -`px` and `py` expand to both inline or block padding sides. `p` expands to all four sides: - -```tsx -const padded = createSprinkles({ px: 'large', py: 'small' }); -``` - ### Available properties The v1 surface covers: @@ -145,22 +142,21 @@ The v1 surface covers: - **Padding**: `padding`, `paddingInline`, `paddingBlock`, `paddingInlineStart`, `paddingInlineEnd`, `paddingBlockStart`, `paddingBlockEnd` - **Overflow**: `overflow`, `overflowX`, `overflowY`, `textOverflow` -- **Pseudo-state**: `backgroundColor` (hover, focus-visible) -- **Shorthands**: `p`, `px`, `py` +- **Pseudo-state**: `backgroundColor` with `hover` and `focus-visible` -Margin utilities are excluded from the initial surface. CSS-native values are used throughout (e.g. -`flex-start` rather than `start`). +Margin utilities are excluded from the initial surface. -Keep `@luke-ui/react/styles` as a separate export path from `@luke-ui/react/recipes`. Recipes are -for component-specific styles; styles are for general layout utilities. +Use CSS-native values throughout, for example `flex-start` instead of `start`. Keep +`@luke-ui/react/styles` separate from `@luke-ui/react/recipes`. Recipes are component-specific +styles. Styles are general layout utilities. ## Implementation -- Use only CSS logical properties (e.g. `margin-inline-start`, `block-size`, `inset-inline`), not - physical (`margin-left`, `height`, `left`/`right`). -- Align variant names with public props (`size`, `tone`). +- Use CSS logical properties such as `margin-inline-start`, `block-size`, and `inset-inline`. Do not + use physical properties such as `margin-left`, `height`, `left`, or `right`. +- Align variant names with public props, such as `size` and `tone`. - Boolean props use `is*` or `should*`. ## Limitations -No styling utility implementation or runtime theme context yet. +Styling utilities do not yet have runtime theme context. diff --git a/docs/TESTING.md b/docs/TESTING.md index c7c94db3..8cba1351 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -1,81 +1,76 @@ # Testing -How to choose, place, and write tests in this repo. +This guide explains how to choose, place, and write tests in this repo. -## Choosing a test type +## Choosing test type -Use the smallest test surface that proves the behavior. +Use the smallest test surface that proves the behaviour. -- **Unit tests** (`*.test.ts`): pure logic, generators, scripts, docs tooling, package metadata, - and non-React utilities. -- **Storybook play tests**: React component behavior that belongs in a real story. For - `@luke-ui/react` components, stories are the component tests; do not add separate `*.test.tsx` - component tests unless Storybook cannot exercise the behavior cleanly. +- **Unit tests** (`*.test.ts`): pure logic, generators, scripts, docs tooling, package metadata, and + non-React utilities. +- **Storybook play tests**: React component behaviour that belongs in a real story. For + `@luke-ui/react` components, stories are component tests. Do not add separate `*.test.tsx` + component tests unless Storybook cannot exercise the behaviour cleanly. - **Storybook visual tests**: public UI states and visual variants worth reviewing for regressions. - **Browser Vitest tests** (`*.browser.test.{ts,tsx}`): non-component DOM logic that needs real - browser APIs and does not fit a story — including style-contract tests for CSS recipes (see - below). + browser APIs and does not fit a story, including CSS recipe style-contract tests. ## Placement -Tests colocate with the source file they cover (`foo.test.ts` beside `foo.ts`); no `__tests__` -directories except for suites that don't map to a single file (e.g. e2e). Never add DOM shims -(happy-dom, jsdom) — DOM-dependent tests run in a real browser (see `vitest.config.ts`), preferring -real APIs over stubs. +Colocate tests with the source file they cover, for example `foo.test.ts` beside `foo.ts`. + +Do not add `__tests__` directories unless the suite does not map to one source file. + +Do not add DOM shims such as happy-dom or jsdom. DOM-dependent tests run in a real browser through +`vitest.config.ts`, using real browser APIs instead of stubs. ## Write the test first -For bugfixes, always start with a failing test that reproduces the bug, and watch it fail for the -right reason before touching the fix. The test is what proves the fix and keeps the bug from -returning. +For bug fixes, start with a failing test that reproduces the bug. Watch it fail for the right reason +before changing the implementation. That test proves the fix and keeps the bug from returning. -For features, prefer writing the test first when the behavior is specifiable up front. Exploratory -UI work may need the component sketched before a story or play test makes sense — that's fine, but -the tests land in the same change as the behavior they cover. +For features, prefer a test first when the behaviour can be specified up front. Exploratory UI work +may need a sketched component before a story play test makes sense. That is fine, but the test +should land in the same change as the behaviour it covers. -## Test behavior, not implementation +## Test behaviour, not implementation -A test should survive any refactor that preserves behavior. Exercise the code the way its consumers -do — through the public API for modules, through roles and interactions for components — and assert -the outcome a consumer can observe. +Tests should survive refactors that preserve behaviour. Exercise code the way consumers do: through +public API modules, roles, and user interactions. Assert outcomes the consumer can observe. -Never assert on internals: private functions, call counts of the repo's own modules, generated -class names, or the text of CSS selectors. If a test can only be written by reaching into -internals, that's a signal the module's interface is missing something, not that the test needs a -back door. +Do not assert internals such as private functions, call counts inside repo modules, generated class +names, or CSS selector text. If a test only works by reaching into internals, the module interface +is probably missing something. -Mock only at true system boundaries (network, clock, external processes), and prefer real -implementations everywhere else — the same philosophy as running DOM tests in a real browser. -Filesystem-dependent tests use temp directories and the real `fs` rather than mocks. +Mock only true system boundaries such as network, clock, or external processes. Prefer real +implementations everywhere else. Filesystem-dependent tests should use temp directories and real +`fs`, not mocks. -## Behavior tests: queries and interactions +## Behaviour tests -Behavior tests — play functions and anything else simulating a user — find elements the way -assistive technology does: +Behaviour tests include Storybook play functions and any test that simulates a user. -- Query by **role with accessible name** (`getByRole('combobox', { name: 'Country' })`), falling - back to label or visible text when no role fits. -- **No test IDs, no CSS selectors.** This is a component library: if an element can't be found by - role or accessible name, assistive-technology users can't find it either. Fix the component, not - the test. -- Interact through **`userEvent` only** (click, keyboard, tab). Never `fireEvent`, manual event - dispatch, or mutating attributes/state to fake an interaction the user would perform. +- Query by role and accessible name, for example `getByRole('combobox', { name: 'Country' })`. Fall + back to label or visible text only when no role fits. +- Do not use test IDs or CSS selectors. In a component library, if an element cannot be found by + role or accessible name, assistive-technology users probably cannot find it either. Fix the + component, not the test. +- Interact through `userEvent` only. Do not use `fireEvent`, manual event dispatch, or attribute and + state mutation to fake interactions. ## Declarative over imperative -State what the behavior is; don't narrate a journey. +State the behaviour under test. Do not narrate every step in the journey. -- **Name and assert one behavior at a time.** Each test (or `step()`, below) states a single - behavior — "selecting an option closes the popover" — and asserts that outcome. Don't add - checkpoint assertions after every interaction; steps exist only to reach the state being - asserted. -- **Set up state with props, not interactions.** Start from the state under test - (`defaultValue`, `isReadOnly`) instead of scripting clicks to arrive there. Interactions appear - only when the interaction itself is the behavior under test. -- **Map behaviors to `step()`, not to new stories.** Keep one story per meaningful state or props - combination; inside its play function, group each behavior in a named `step()` from - `storybook/test`. Steps report individually without adding sidebar entries or visual-test - snapshots of identical-looking states. +- Name and assert one behaviour at a time. Each test or `step()` should name a single behaviour, + such as "selecting an option closes the popover". Use setup interactions only to reach the state + being asserted. +- Set up state with props rather than interactions when possible, for example `defaultValue` or + `isReadOnly`. Use interactions only when the interaction itself is the behaviour under test. +- Map behaviours to `step()`, not to extra stories. Keep one story per meaningful state-prop + combination. Inside the play function, group behaviours with named `step()` calls from + `storybook/test`. Steps report individually without adding duplicate sidebar entries or visual + snapshots. ```ts play: async ({ canvasElement, step }) => { @@ -85,23 +80,25 @@ play: async ({ canvasElement, step }) => { await step('selecting an option closes the popover and fills the input', async () => { await userEvent.click(combobox); await userEvent.click(within(document.body).getByRole('option', { name: 'Australia' })); + await expect(combobox).toHaveValue('Australia'); await expect(combobox).toHaveAttribute('aria-expanded', 'false'); }); -}, +}; ``` ## Style-contract tests for CSS recipes -Recipes (`src/recipes/*.css.ts`) have no roles or user interactions — their contract is "given a -DOM structure in a given state, the element computes these styles". Tests for them are the one -place raw DOM construction and `querySelector` are appropriate: there is no user to impersonate. - -- Assert **computed styles** (via `getComputedStyle`, resolving tokens to concrete values) — the - outcome a user sees. Never assert generated class names or selector strings; those are the - implementation. -- Build the DOM the recipe documents (e.g. a control containing an input and a trigger button), - not incidental markup that happens to pass. -- Every state a recipe test covers must also exist as a story on at least one consuming component, - so the recipe is exercised against real component markup in visual tests. If the story is - missing, add it in the same change. +Recipes in `src/recipes/*.css.ts` have no roles or user interactions. Their contract is: given this +DOM structure in this state, the element computes these styles. + +For recipe tests, raw DOM construction and `querySelector` are appropriate because there is no user +to impersonate. + +- Assert computed styles with `getComputedStyle`, resolving tokens to concrete values. Assert the + outcome the user sees. +- Do not assert generated class names or selector strings. +- Build DOM recipe documents, such as a control containing an input and trigger button. Do not rely + on incidental markup that happens to pass. +- Every recipe state covered by a test must also exist in a story on at least one consuming + component. If the story is missing, add it in the same change. diff --git a/docs/adr/0001-component-tier-taxonomy.md b/docs/adr/0001-component-tier-taxonomy.md index 46b07bc6..be0c09a7 100644 --- a/docs/adr/0001-component-tier-taxonomy.md +++ b/docs/adr/0001-component-tier-taxonomy.md @@ -1,16 +1,42 @@ -# Component tier taxonomy: atom / composed / primitive +# 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. +Luke UI classifies components into three tiers: **Atom**, **Composed**, and **Primitive**. The tier +decides whether the component appears in hosted docs and how it is described in source and package +docs. -- **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`, `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 hosted docs (app-dev surface), but they are documented in package docs so library authors and coding agents can reach them. See ADR-0003. +## Decision -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. Thin presets of a single composed component (e.g. the removed `CloseButton`, which only pinned `IconButton`'s `icon` and `aria-label`) don't earn a Composed-tier module of their own — they fail the deletion test, and the pattern belongs in the wrapped component's docs instead (see issue #41). +- **Atoms** present one conceptual unit. App developers use them directly. Examples: `Text`, `Link`, + `Icon`, `LoadingSpinner`, `Heading`, `Emoji`, and `Numeral`. Atoms get hosted docs. +- **Composed** components combine atoms or primitives into a ready-to-drop-in pattern for app + developers. Examples: `Button`, `IconButton`, `TextField`, and `ComboboxField`. Composed + components get hosted docs. +- **Primitives** are building blocks for library authors who are assembling the next composed + component. Examples: `TextInput`, the `Combobox*` kit, `button/primitive`, and the `Field` kit. + Primitives do not get hosted docs, but they are documented in package docs so library authors and + coding agents can find them. -The rule in one line: **hosted docs target app developers; package docs cover every public export so library authors and agents can reach them.** +In one line: hosted docs target app developers. Package docs cover every public export path. + +## Rejected options + +We rejected a two-tier "composed vs primitive" model because it could not classify atoms cleanly. +`Text` was labelled "primitive" in its own JSDoc, but it is nothing like `TextInput` in terms of +audience or use. + +We also rejected treating the composed `Field` as app-developer-facing docs content. It composes +other components, but its audience is library authors. App developers reach it through `TextField`, +`ComboboxField`, and similar wrappers. + +Thin presets of one composed component do not earn a new Composed-tier module. The removed +`CloseButton` only pinned `IconButton`'s `icon` and `aria-label`. It failed the deletion test, so +the pattern belongs in the wrapped component's docs instead. See issue #41. ## 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. +- Source JSDoc should use "atom", "composed", and "primitive" consistently. +- Do not use "primitive" as a generic label for anything that is not a plain HTML element. +- `docs/CONVENTIONS.md` should describe the three-tier taxonomy, not a two-way "primitive and + composed" split. +- Hosted docs stay focused on app-developer-facing atoms and composed components. +- Package docs remain the reachability surface for public primitive exports. diff --git a/docs/adr/0002-primitive-package-path-convention.md b/docs/adr/0002-primitive-package-path-convention.md index 2826b6fb..404d2235 100644 --- a/docs/adr/0002-primitive-package-path-convention.md +++ b/docs/adr/0002-primitive-package-path-convention.md @@ -1,21 +1,38 @@ # 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. +When a primitive kit supports a composed component, export it at `[composed]/primitive` instead of a +bare top-level path. -Examples: +## Decision -- `@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` +Use paths like: -This extends the convention already established by `@luke-ui/react/button/primitive` and `@luke-ui/react/field/primitive`. +- `@luke-ui/react/text-field/primitive` for `TextInput` +- `@luke-ui/react/combobox-field/primitive` for `ComboboxInput`, `ComboboxControl`, + `ComboboxListBox`, and related parts +- `@luke-ui/react/field/primitive` for `Field`, the primitive field div, `FieldLabel`, `FieldError`, + and `FieldDescription` -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. +This follows the convention already used by `@luke-ui/react/button/primitive` and +`@luke-ui/react/field/primitive`. -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. +The path makes the audience visible. `combobox-field/primitive` reads as "the primitive kit that +underpins `ComboboxField`". The API remains public without moving source folders. + +## Rejected options + +We rejected top-level paths such as `./combobox` or `./text-input` because they do not signal +audience. The import line looks the same as an atom or composed component. + +We rejected a source-folder reshuffle such as `primitives/`, `composed/`, and `atoms/` at the +package root. That would churn internal imports without changing the public API shape. + +We rejected making primitives fully internal by removing them from `package.json#exports`. The +package is public, and external consumers may need to assemble their own composed wrappers. ## 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. +- There is intentionally no top-level `./field` export. +- Consumers who need field anatomy import from `./field/primitive`. +- Primitive paths are lower-level escape hatches for library authors. +- New primitive kits must follow the `[composed]/primitive` convention. diff --git a/docs/adr/0003-package-docs-surface.md b/docs/adr/0003-package-docs-surface.md index 9185ee6f..01a07604 100644 --- a/docs/adr/0003-package-docs-surface.md +++ b/docs/adr/0003-package-docs-surface.md @@ -1,19 +1,55 @@ # Package docs are a separate AI-native surface -`@luke-ui/react` ships its own per-export documentation under `packages/@luke-ui/react/docs/`, generated from JSDoc and authored prose, separate from the hosted Fumadocs site at `apps/docs`. +`@luke-ui/react` ships per-export documentation under `packages/@luke-ui/react/docs/`. Those docs +are generated from JSDoc, TypeScript types, and authored prose. They are separate from the hosted +Fumadocs site in `apps/docs`. -Two doc surfaces, two audiences. Hosted docs target app developers building UIs; their navigation and content shape are tuned for human discovery. Package docs target anyone reading the package off npm — library authors and coding agents — and document every public export path. Primitives are documented in package docs but listed in a de-emphasised "Library authors" section of `README.md` and `llms.txt`; hosted docs continue to omit primitives entirely (see ADR-0001). +## Decision -We rejected the hold-the-line option (no primitive docs anywhere) because agents reaching for `text-field/primitive` need its type contract; expecting them to read source only doesn't match the AI-native goal. We rejected a single unified surface because navigation tuned for app developers crowds out library-author content, and tuning the other way clutters the marketing site. +Luke UI has two docs surfaces: + +- **Hosted docs** target app developers building UIs. Their navigation and page shape are tuned for + human discovery. +- **Package docs** target anyone reading the package off npm, including library authors and coding + agents. They document every public export path. + +Primitives are documented in package docs, but listed in a de-emphasised "Library authors" section +in `README.md` and `llms.txt`. Hosted docs continue to omit primitives. + +## Rejected options + +We rejected documenting no primitives anywhere. Agents and library authors who reach for +`text-field/primitive` need the type contract without reverse engineering source. + +We rejected one unified docs surface. Navigation tuned for app developers crowds out library-author +content. Navigation tuned for library authors clutters the hosted site. ## Consequences -- JSDoc and TypeScript types are authoritative for API tables. Co-located prose sources are renamed `*.docs.mdx` → `*.docs.md` and contain prose only; Props tables are generated from types via `ts-morph`. For atom and composed components, important inherited props from `react-aria-components` are re-declared on the package's own interfaces with full JSDoc so they appear in the local "own props" table; long-tail inherited props are covered by a single "Extends `react-aria-components` ``" pointer. -- Generated artifacts under `packages/@luke-ui/react/docs/` are **not committed**; they are produced on demand by `pnpm --filter @luke-ui/react generate:docs` (or automatically via `turbo generate`) and are listed in `.gitignore`. The aggregated `llms-full.md` is built into `dist/docs/` and is also not committed. CI validates the generator stays healthy by running `generate:docs` against the source tree, but does not enforce an exact match against committed files. -- The hosted `apps/docs` site embeds the generated package doc inside each component page's MDX via Fumadocs's `` directive, so parsing happens at MDX compile time and the hosted page reflects the same Markdown that ships on npm. Story-driven interactivity (``) sits above the included Markdown, not interleaved. A small remark plugin in `source.config.ts` strips the leading `# Title` from MDX trees so the included content doesn't duplicate the frontmatter-driven ``. -- Page shape auto-adapts by export shape, not tier. Single-export paths render as a component-style page; multi-export paths render as an enumerated overview. Tier appears as a label and as de-emphasis in the index, not as a separate page template. -- Scope is tiered: full pages for the 17 component-shaped exports, overview pages for `recipes`, `theme`, `tokens`, `utils`, and index-only entries (no dedicated `.md` file) for `stylesheet.css`, `spritesheet.svg`, `package.json`. -- A single `generate:docs` script (in `scripts/generate-docs.ts`, alongside `generate-color-tokens.ts` and `build-icons.ts`) emits both `README.md`'s index and `llms.txt`, with a flag controlling whether the "Library authors" section is included; hosted `apps/docs` calls the same shared builder for its own `/llms.txt` route. -- The package `README.md` is hand-authored; it is the front door, not an enumeration. It pitches, links to `docs/` and `llms.txt`, and is the only docs file the generator does not own. -- The package `LICENSE` is a hand-committed copy of the workspace-root MIT license; sync drift risk is negligible. -- First npm publish is deferred. `version` stays at `0.0.0` until a separate release-engineering initiative; V1 success is verified by `npm pack --dry-run` against the `package.json#files` allowlist `["dist", "docs", "README.md", "LICENSE"]`. +- JSDoc and TypeScript types are authoritative for API tables. +- Co-located prose sources are `*.docs.md` files. They contain prose only. +- Prop tables are generated from types with `ts-morph`. +- Atom and composed component interfaces should re-declare important inherited + `react-aria-components` props with full JSDoc. Long-tail inherited props are covered by a single + "Extends `react-aria-components` ``" pointer. +- Generated files under `packages/@luke-ui/react/docs/` are not committed. They are produced by + `pnpm --filter @luke-ui/react generate:docs`, or by `turbo generate`. +- `dist/docs/llms-full.md` is built output and is not committed. +- CI validates that the generator runs. It does not compare generated files to a committed snapshot. +- Hosted docs embed generated package docs into component MDX pages with Fumadocs ``. + Story-driven interactivity sits above the included Markdown. +- `source.config.ts` strips leading generated titles from included Markdown so the hosted page does + not duplicate its frontmatter title. +- Page shape follows export shape, not tier. Single-export paths render component-style pages. + Multi-export paths render overview pages. +- Tier appears as index labelling, not as a separate page template. +- Full pages cover component-shaped exports. Overview pages cover `recipes`, `theme`, `tokens`, and + `utils`. +- `stylesheet.css`, `spritesheet.svg`, and `package.json` are index-only entries. +- A single `generate:docs` script emits the package README index and `llms.txt`. The hosted app + calls the same shared builder for its `/llms.txt` route. +- The package `README.md` is hand-authored. It is the front door, not a generated enumeration. +- The package `LICENSE` is a hand-committed copy of the workspace MIT license. +- First npm publish is deferred. `version` stays at `0.0.0` until a separate release-engineering + initiative. V1 package readiness is verified with `npm pack --dry-run` against the + `package.json#files` allowlist: `["dist", "docs", "README.md", "LICENSE"]`. diff --git a/docs/adr/0004-styling-utilities-public-api.md b/docs/adr/0004-styling-utilities-public-api.md index c05e98bd..a1c72615 100644 --- a/docs/adr/0004-styling-utilities-public-api.md +++ b/docs/adr/0004-styling-utilities-public-api.md @@ -1,41 +1,40 @@ -# Styling utilities as public API +# Styling utilities are public API -Styling utilities are public API, exported from `@luke-ui/react/styles` as named -exports. They provide token-aware, type-safe layout helpers for cases where -component props are too narrow. +Styling utilities are public API, exported as named exports from `@luke-ui/react/styles`. They +provide token-aware, type-safe layout helpers for cases where component props are too narrow. ## Decision -Rainbow Sprinkles was chosen over vanilla-extract Sprinkles. Rainbow Sprinkles -uses `assignInlineVars` to emit dynamic CSS custom properties at runtime rather -than generating static classes for every token-value combination. This avoids -class-name bloat when the token scale grows and keeps the generated CSS bundle -small — the static utilities layer defines only property-level classes, not -value-level classes. The trade-off is that values are applied via inline -`style`, which raises specificity but is acceptable for the `utilities` cascade -layer (the highest-priority escape hatch). - -## Rejected alternatives - -- **Polymorphic `Box` component**: rejected because it introduces the same - polymorphism issues that make React Aria Components' `render` prop preferable. -- **Spreading style props onto every component**: rejected because it would bloat - component APIs and blur the boundary between component-specific recipes and - consumer layout utilities. -- **Library-author only API**: rejected because consumers repeatedly need safe - layout composition without inline styles, as shown by the middle-truncation - story example. +Use Rainbow Sprinkles instead of vanilla-extract Sprinkles. + +Rainbow Sprinkles uses `assignInlineVars` to emit dynamic CSS custom properties at runtime. It does +not generate a static class for every token-value combination. That keeps the generated CSS bundle +small as the token scale grows, because the static utilities layer defines property-level classes +rather than value-level classes. + +The tradeoff is that values are applied through inline `style`, which raises specificity. That is +acceptable because the utilities layer is already the highest-priority escape hatch. + +## Rejected options + +- **Polymorphic `Box` component**: rejected because it introduces the same polymorphism issues that + make React Aria Components' `render` prop preferable. +- **Style props on every component**: rejected because it would bloat component APIs and blur the + boundary between component-specific recipes and consumer layout utilities. +- **Library-author-only API**: rejected because consumers repeatedly need safe layout composition + without writing inline styles. ## Consequences -- `@luke-ui/react/styles` exports `createSprinkles()` as the public layout utility function. -- The initial surface covers layout, flex-item behavior, sizing, token-backed gaps - and padding, overflow, and text overflow. Margin utilities are excluded. -- Values are CSS-native, property names are logical where possible. -- Responsive values and pseudo-state conditions (`hover`, `focus-visible`) are in - scope for v1. -- Object notation is used for responsive values; array notation may be added later. -- Rainbow Sprinkles condition syntax is used for pseudo-states. -- Utilities live in the `utilities` cascade layer, below recipes. -- Layer helpers (`styleInLayer`, `recipeInLayer`) remain internal. -- No React render-prop helper is added until a real repeated need appears. +- `@luke-ui/react/styles` exports `createSprinkles()` as a public layout utility. +- The initial surface covers layout, flex-item behaviour, sizing, token-backed gaps, token-backed + padding, overflow, and text overflow. +- Margin utilities are excluded. +- Values are CSS-native. +- Property names are logical where possible. +- Responsive values and pseudo-state conditions (`hover`, `focus-visible`) are in scope for v1. +- Responsive values use object notation. Array notation may be added later. +- Pseudo-states use Rainbow Sprinkles condition syntax. +- Utilities live in the `utilities` cascade layer. +- Layer helpers such as `styleInLayer` and `recipeInLayer` remain internal. +- No React component wrapper is added for this API. diff --git a/docs/adr/0005-component-creation-plan-module.md b/docs/adr/0005-component-creation-plan-module.md index ed2654c0..ec224116 100644 --- a/docs/adr/0005-component-creation-plan-module.md +++ b/docs/adr/0005-component-creation-plan-module.md @@ -1,8 +1,20 @@ # Component creation uses a plan module -Component creation is modeled as a module whose interface returns a plan for the files and checks needed to add an Atom or Composed component. Turbo/Plop remains an adapter that applies the plan, rather than the place where component creation rules live. This keeps the interface test surface on Luke UI's component creation rules, while the adapter stays thin and replaceable. +Component creation is modelled as a module that returns a plan for the files and checks needed to +add an Atom or Composed component. -## Considered Options +Turbo and Plop stay as the adapter that applies the plan. They are not where the component creation +rules live. -- **Keep all logic in the Turbo generator**: rejected because it makes Plop the test surface and spreads Atom, Composed, docs, story, recipe, and export-placement rules through one shallow script. -- **Create files directly without a plan**: rejected because it makes dry-run tests and fixture assertions harder, and it hides the exact public surfaces a component creation input should produce. +## Decision + +Keep Luke UI's component creation rules in a testable plan module. The adapter can stay thin and +replaceable. + +## Rejected options + +- **Keep all logic in the Turbo generator**: rejected because it makes Plop the test surface and + spreads Atom, Composed, docs, story, recipe, and export rules through one shallow script. +- **Create files directly without a plan**: rejected because it makes dry-run tests and fixture + assertions harder. It also hides the exact public surfaces a component creation input should + produce. diff --git a/docs/adr/0006-docs-md-structure-standard.md b/docs/adr/0006-docs-md-structure-standard.md index fde16b3a..d9f44865 100644 --- a/docs/adr/0006-docs-md-structure-standard.md +++ b/docs/adr/0006-docs-md-structure-standard.md @@ -1,70 +1,62 @@ # Standard structure for `.docs.md` prose files -Every `src//.docs.md` file follows the same section shape and order. A reader — -human or agent — can then predict where to find something regardless of which component they're -reading. This extends [ADR-0003](0003-package-docs-surface.md), which established the two-surface -split and the "prose only" rule but not the internal shape of that prose. +Every `src//.docs.md` file follows the same section order. Readers can then +predict where to find usage, props, accessibility notes, and cross-references regardless of which +component they are reading. -We considered leaving the shape as tribal knowledge — copy a sibling file and match its style. We -rejected this because an unwritten convention can't catch drift: `button.docs.md` had no headings at -all while every other component used one `##` per feature, and nothing flagged it. +This extends [ADR-0003](0003-package-docs-surface.md), which established the two-surface docs split +and the "prose only" rule for `.docs.md` files. -## Section order +## Decision -1. **Usage lead-in** — unheaded prose and/or minimal code examples. Do not write an explicit - `## Usage` heading; `render-page.ts` injects one when assembling the generated package doc, so an - authored heading would render as a duplicate. -2. **`## Best Practices`** — a two-column `| Guidance | Practices |` table: `Guidance` holds a plain - `Do` or `Don't`, `Practices` holds the sentence. This isn't a `| Do | Don't |` pairing, which would - force every row to carry both a positive and a negative example and pad rows when a topic only - warrants one. Rows aren't paired — list as many `Do`s and `Don't`s as useful, in any order and - count. No colour/pill styling for now — plain text in the `Guidance` column. Scaffolded by default - for every new component; delete it only if there's no useful guidance to give. -3. **Feature sections** — one `##` heading per prop or concept (e.g. `## Size`, `## Tone`, - `## Validation`). Order these by importance to a typical consumer — required or most-frequently-used - props first, niche/advanced ones last. This is an editorial judgment call, not a mechanical rule: - do not sort alphabetically and do not default to the order props happen to be declared in the - TypeScript interface. -4. **`## Accessibility`** — scaffolded by default for every new component. Delete it if the component - has nothing beyond default semantics to call out (e.g. `text.docs.md` has none because `Text` - renders plain text with default semantics). -5. **Cross-reference sections, always last** — - - `## Primitive {Name}` when a composed component has a same-subpath single-component primitive - counterpart, where `{Name}` is the primitive's own exported name (e.g. `## Primitive Button` in - `button.docs.md`, pointing at `button/primitive`; `## Primitive TextInput` in `text-field.docs.md`, - since `text-field/primitive` exports `TextInput`, not `TextField`). - - `## Primitive kit` when the same-subpath primitive is a multi-export kit rather than a single - component (e.g. `combobox-field/primitive`, which exports `ComboboxInput`, `ComboboxControl`, and - others). - - `## When to use vs {Name}` when a component is easily confused with a sibling composed component - (e.g. `## When to use vs Button` in `icon-button.docs.md`). +Use this section order: -This order and the heading names apply to every component doc in the package — atoms, composed -components, and any component or utility documented under the same subpath export. +1. **Usage lead-in**: unheaded prose and minimal code examples. Do not write an explicit `## Usage` + heading. `render-page.ts` injects that heading when it assembles the generated package doc. +2. **`## Best Practices`**: a two-column `| Guidance | Practices |` table. `Guidance` contains plain + `Do` or `Don't`. `Practices` contains the sentence. This is not a paired `| Do | Don't |` table. + Add only rows that carry useful guidance. +3. **Feature sections**: one `##` heading per prop or concept, such as `## Size`, `## Tone`, or + `## Validation`. Order sections by importance to a typical consumer. Do not sort alphabetically + or follow TypeScript declaration order by default. +4. **`## Accessibility`**: include this when the component has user-facing accessibility behaviour + to explain. Delete it when the component has nothing beyond default semantics to call out. +5. **Cross-reference sections**: always last. -## Docs are authored in the package, not in `apps/docs` +Cross-reference section names: -All prose content is authored in `packages/@luke-ui/react/src//.docs.md`, -co-located with the component's source. `apps/docs` MDX files (e.g. -`apps/docs/content/docs/components/actions/button.mdx`) never contain hand-authored prose — they are -wiring only: frontmatter (`title`/`description`), an interactive `` demo, -and an `` of the generated package doc. This was already true in practice; this ADR makes it -an explicit rule so it doesn't regress. +- `## Primitive {Name}` for a composed component with a same-subpath, single-component primitive + counterpart. `{Name}` is the primitive export name. Example: `## Primitive TextInput` in + `text-field.docs.md`. +- `## Primitive kit` for a same-subpath primitive kit with multiple exports. Example: + `combobox-field/primitive`. +- `## When to use vs {Name}` for an easily confused sibling component. + +These names apply to component docs in the package: atoms, composed components, and component +utilities documented under the same subpath export. + +## Rejected option + +We rejected leaving the shape as tribal knowledge. "Copy a sibling file" did not catch drift. +`button.docs.md` had no headings while other component docs used one `##` heading per feature, and +nothing flagged the mismatch. + +## Docs are authored in the package + +All component prose is authored in `packages/@luke-ui/react/src//.docs.md`, +next to the component source. + +Hosted docs MDX files under `apps/docs` are wiring only. They contain frontmatter, the interactive +`` demo, and an `` for generated package docs. ## Consequences -- The component-creation generator (`packages/turbo-generators/src/component-creation-plan.ts`, - `renderPackageDocs()`) scaffolds `## Best Practices` and `## Accessibility` placeholders for every - new component, and no longer writes an explicit `## Usage` heading — `render-page.ts` already injects - one, so the old scaffold produced a duplicate. -- Placeholder/unfinished content in `.docs.md` must be plain visible text (e.g. italicized), never an - HTML comment (``). This content gets included in Fumadocs MDX pages. MDX doesn't support HTML - comments, only `{/* ... */}` JS-style ones, and those would render as literal text on the - plain-markdown surface (npm/agents). A visible placeholder works on both surfaces, and it's the safer - failure mode too: a hidden comment left behind by accident renders an empty section with no trace, - where a visible one is easy to spot. -- This standard applies to atom and composed components, the only tiers with a `.docs.md` file. - Primitives (e.g. `button/primitive`) have no `.docs.md` and no `## Usage` heading at all, per - ADR-0003. -- This ADR doesn't retroactively rewrite existing components other than Button; auditing them against - this standard is separate follow-up work. +- The component generator scaffolds `## Best Practices` and `## Accessibility` placeholders for new + component docs. +- Primitives have no `.docs.md` file and no authored `## Usage` heading, per ADR-0003. +- Placeholder content in `.docs.md` must be visible text, not HTML comments. Generated Markdown is + included in Fumadocs MDX and also read as plain Markdown by npm users and agents. +- Visible placeholders are easier to catch than hidden comments. A forgotten hidden comment can + render as an empty section with no clue. +- This standard applies to Atom and Composed component docs. A separate audit can update older files + that do not yet match it. diff --git a/packages/@luke-ui/rainbow-sprinkles/src/create-runtime-fn.ts b/packages/@luke-ui/rainbow-sprinkles/src/create-runtime-fn.ts index f1ae5dc9..52c3f8b3 100644 --- a/packages/@luke-ui/rainbow-sprinkles/src/create-runtime-fn.ts +++ b/packages/@luke-ui/rainbow-sprinkles/src/create-runtime-fn.ts @@ -4,7 +4,6 @@ import type { DynamicConditionalProperty, DynamicProperty, RuntimeFnReturn, - ShorthandProperty, SprinkleProperties, SprinklesProps, StaticConditionalProperty, @@ -112,10 +111,6 @@ function handleEntry( cache: Map, condition?: string, ): string { - if ('mappings' in propertyConfig) { - return ''; - } - const propName = (propertyConfig as DynamicProperty).name; const staticScale = (propertyConfig as StaticProperty).staticScale as | ReadonlyArray @@ -197,9 +192,6 @@ function assignVars( propValue: unknown, cache: Map, ): void { - if ('mappings' in propertyConfig) { - return; - } if (!hasDynamic(propertyConfig)) { return; } @@ -271,7 +263,6 @@ export function createRuntimeFn 'mappings' in cssConfig[property]!); type PropertyCache = { class: Map; @@ -284,34 +275,16 @@ export function createRuntimeFn = {}; const className: Array = []; const otherProps: Record = {}; - const shorthands: Record = {}; - const nonShorthands = { ...props }; - let hasShorthands = false; - - for (const shorthand of shorthandNames) { - const value = props[shorthand]; - if (value != null) { - const sprinkle = cssConfig[shorthand]! as ShorthandProperty; - hasShorthands = true; - for (const propMapping of sprinkle.mappings) { - shorthands[propMapping] = value; - if (nonShorthands[propMapping] == null) { - delete nonShorthands[propMapping]; - } - } - } - } - const finalProps = hasShorthands ? { ...shorthands, ...nonShorthands } : props; - const finalPropsKeys = Object.keys(finalProps); - for (const property of finalPropsKeys) { + const propsKeys = Object.keys(props); + for (const property of propsKeys) { if (!propertiesSet.has(property)) { otherProps[property] = props[property]; continue; } const propertyConfig = cssConfig[property]; - const propValue = finalProps[property]; - if (!propertyConfig || 'mappings' in propertyConfig) { + const propValue = props[property]; + if (!propertyConfig) { continue; } let classCache: Map | undefined; diff --git a/packages/@luke-ui/rainbow-sprinkles/src/define-properties.ts b/packages/@luke-ui/rainbow-sprinkles/src/define-properties.ts index 5bb2cbaf..32308b02 100644 --- a/packages/@luke-ui/rainbow-sprinkles/src/define-properties.ts +++ b/packages/@luke-ui/rainbow-sprinkles/src/define-properties.ts @@ -6,7 +6,6 @@ import type { ConfigStaticProperties, DefinePropertiesReturn, MakeConfig, - ShorthandProperty, SprinkleProperties, } from './types.js'; @@ -140,32 +139,22 @@ type DefinePropertiesOptions = CommonOptions & { defaultCondition?: string; dynamicProperties?: ConfigDynamicProperties; staticProperties?: ConfigStaticProperties; - shorthands?: Record>; }; export function defineProperties< const Dyn extends ConfigDynamicProperties | undefined = undefined, const Stat extends ConfigStaticProperties | undefined = undefined, const Cond extends ConfigConditions | undefined = undefined, - const Short extends Record> | undefined = undefined, >(options: { '@layer'?: string; conditions?: Cond; defaultCondition?: string; dynamicProperties?: Dyn; staticProperties?: Stat; - shorthands?: Short; -}): DefinePropertiesReturn>; +}): DefinePropertiesReturn>; export function defineProperties(options: DefinePropertiesOptions): DefinePropertiesReturn { - const { conditions, dynamicProperties, staticProperties, shorthands, defaultCondition } = options; - const config: SprinkleProperties = shorthands - ? Object.fromEntries( - Object.entries(shorthands).map(([prop, mappings]) => [ - prop, - { mappings } as ShorthandProperty, - ]), - ) - : {}; + const { conditions, dynamicProperties, staticProperties, defaultCondition } = options; + const config: SprinkleProperties = {}; if (dynamicProperties) { for (const dynamicProp of Object.keys(dynamicProperties)) { diff --git a/packages/@luke-ui/rainbow-sprinkles/src/types.ts b/packages/@luke-ui/rainbow-sprinkles/src/types.ts index 8d009071..0a3ab4ef 100644 --- a/packages/@luke-ui/rainbow-sprinkles/src/types.ts +++ b/packages/@luke-ui/rainbow-sprinkles/src/types.ts @@ -138,10 +138,6 @@ export type StaticDynamicConditionalProperty = { dynamicScale: true; }; -export type ShorthandProperty = ReadonlyArray> = { - mappings: Mappings; -}; - export type SprinkleProperties = { [k: string]: | DynamicProperty @@ -153,8 +149,7 @@ export type SprinkleProperties = { | StaticConditionalProperty | StaticConditionalPropertyArray | StaticDynamicConditionalPropertyArray - | StaticDynamicConditionalProperty - | ShorthandProperty; + | StaticDynamicConditionalProperty; }; // Mapped config types produced by defineProperties, preserving literal scale key types. @@ -219,7 +214,6 @@ export type MakeConfig< Dyn extends ConfigDynamicProperties | undefined, Stat extends ConfigStaticProperties | undefined, Cond extends ConfigConditions | undefined, - Short extends Record> | undefined, > = (Dyn extends ConfigDynamicProperties ? { [K in keyof Dyn & string]: DynamicConfigEntry< @@ -237,9 +231,6 @@ export type MakeConfig< Cond extends undefined ? false : true >; } - : Record) & - (Short extends Record> - ? { [K in keyof Short & string]: ShorthandProperty } : Record); export type DefinePropertiesReturn = { @@ -300,9 +291,7 @@ type ChildSprinkle = Sprinkle extends StaticDynamicConditionalProperty : never; type ChildSprinkles> = { - [Prop in keyof Sprinkles]?: Sprinkles[Prop] extends ShorthandProperty - ? ChildSprinkle - : ChildSprinkle; + [Prop in keyof Sprinkles]?: ChildSprinkle; }; export type SprinklesProps> = Args extends [infer L, ...infer R] diff --git a/packages/@luke-ui/react/AGENTS.md b/packages/@luke-ui/react/AGENTS.md index 82b54531..9d88f68f 100644 --- a/packages/@luke-ui/react/AGENTS.md +++ b/packages/@luke-ui/react/AGENTS.md @@ -1,72 +1,74 @@ -# @luke-ui/react Agent Guide +# @luke-ui/react agent guide -- Do not hand-edit `.generated/entries.ts` or `package.json` exports; entries are generated, tsdown - updates exports at build. -- 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. +- Do not hand-edit `.generated/entries.ts` or `package.json#exports`. Entries are generated, and + `tsdown` updates exports during build. +- When adding a component, use `pnpm generate:component` from the repo root. Do not create component + files by hand. The generator updates group barrels, the styles index, and docs wiring. +- Stories (`*.stories.tsx`) are the component tests. Do not add separate `*.test.tsx` component + tests unless Storybook cannot exercise the behaviour. +- React Compiler is enabled. Do not use `useCallback` or `useMemo` unless there is a specific reason + the compiler cannot handle. -## Component Structure +## Component structure -Each component directory contains: +A component directory contains: -- `[component].docs.md` — authored usage guidance consumed by the docs generator -- `[component].stories.tsx` — Storybook stories (also serve as tests) -- `index.tsx` — component implementation -- `primitive/` — optional primitive exports +- `[component].docs.md`: authored usage guidance consumed by the docs generator +- `[component].stories.tsx`: Storybook stories that also serve as tests +- `index.tsx`: component implementation +- `primitive/`: optional primitive exports -## Component Taxonomy +## Component taxonomy -Components follow a three-tier system (Atom/Composed/Primitive). See `docs/CONVENTIONS.md` for +Components follow the Atom, Composed, and Primitive taxonomy. See `docs/CONVENTIONS.md` for definitions. -**Important:** Primitives (exports from `*/primitive/`) are building blocks for library authors. -They are not promoted in beginner app-developer navigation, but they are part of the public API and -should have enough generated documentation for power users and agents. +Primitives exported from `*/primitive/` are building blocks for library authors. They are not +promoted in beginner app-developer navigation, but they are public API and should have enough +generated documentation for power users and agents. -## Documentation Generation +## Documentation generation -JSDoc and TypeScript types drive generated docs under `docs/`. When adding or modifying a component: +JSDoc and TypeScript types drive generated docs under `docs/`. -- Function-level JSDoc on the exported component describes what it is for an app developer. -- The exported `Props` interface carries an `@tier` JSDoc tag (`atom`, `composed`, or `primitive`). -- Each prop has JSDoc and, where defaults exist in the React component destructure, an `@default` +When adding or modifying a component: + +- Function-level JSDoc on the exported component should describe the component for an app developer. +- The exported `Props` interface must carry an `@tier` JSDoc tag: `atom`, `composed`, or + `primitive`. +- Each prop should have JSDoc. If the component destructures a default value, include an `@default` tag. -- For atom and composed components, re-declare important props inherited from - `react-aria-components` on the package's own interface with full JSDoc so they appear in the - generated own-props table. Use the type passthrough pattern: - `isDisabled?: RACButtonProps['isDisabled']`. Do not re-declare every RAC prop; only include - load-bearing props an app developer will reach for. -- The long tail of inherited props is covered by the generated extends pointer. +- Atom and composed components should re-declare important inherited `react-aria-components` props + on the package's own interface with full JSDoc. Use the passthrough pattern, for example + `isDisabled?: RACButtonProps['isDisabled']`. +- Do not re-declare every React Aria Components prop. Re-declare only props an app developer is + likely to reach for. +- Long-tail inherited props are covered by the generated "Extends" pointer. -Co-located prose (`*.docs.md`) explains usage judgement. The generator splices it into the rendered -page. Do not put props tables, import blocks, or component descriptions in `*.docs.md`; those are -generated. +## Generated docs -Run `pnpm --filter @luke-ui/react generate:docs` to regenerate `docs/`. CI validates the generator -stays healthy via `pnpm --filter @luke-ui/react check:docs` (a smoke test, not a stale-file check — -the generated output is ignored). +Generated docs are ignored by Git. After changing JSDoc or `.docs.md` prose, run: -### Dev-loop +```sh +pnpm --filter @luke-ui/react generate:docs +``` -During `pnpm dev` (or `turbo dev`) the docs app does two things: +Use `pnpm --filter @luke-ui/react check:docs` as a smoke test that the generator is healthy. It is +not a stale-file check because generated output is ignored. -1. Watches generated `docs/*.md` and hot-reloads the page when those files change. -2. Watches `src/**/*.{ts,tsx,docs.md}` in this package and re-runs `generate:docs` on change - (debounced ~300ms). +## Dev loop -So the in-dev loop for both prose edits and JSDoc edits is the same: +During `pnpm dev` or `turbo dev`, the docs app does two things: -1. Edit a `[component].docs.md`, an `index.tsx` JSDoc tag (`@default`, `@tier`, prop descriptions), - or a prop type. -2. Save — the generator re-runs, `docs/*.md` updates, the page reloads. +1. Watches generated `docs/*.md` files and hot-reloads pages when they change. +2. Watches `src/**/*.{ts,tsx,docs.md}` in the package and re-runs `generate:docs` on change, + debounced by about 300ms. -If the generator fails (e.g. mid-edit syntax error) the dev server logs the error and keeps running; -the next successful save regenerates. +The loop is the same for prose and JSDoc edits: -For one-off CLI runs: +1. Edit `[component].docs.md`, `index.tsx` JSDoc, or prop types. +2. Save. +3. The generator re-runs, `docs/*.md` updates, and the page reloads. -```sh -pnpm --filter @luke-ui/react generate:docs -``` +If the generator fails during a mid-edit syntax error, the dev server logs the error and keeps +running. The next successful save regenerates the docs. diff --git a/packages/@luke-ui/react/README.md b/packages/@luke-ui/react/README.md index 0b4cb05c..ae1db62d 100644 --- a/packages/@luke-ui/react/README.md +++ b/packages/@luke-ui/react/README.md @@ -1,6 +1,6 @@ # @luke-ui/react -A React design system built on `react-aria-components` and `vanilla-extract`. +Luke UI is a React design system built on `react-aria-components` and `vanilla-extract`. ## Install @@ -10,7 +10,7 @@ pnpm add @luke-ui/react ## Setup -Apply the theme class at your app root and import the stylesheet: +Import the stylesheet and apply the theme class at your app root. ```tsx import '@luke-ui/react/stylesheet.css'; @@ -23,15 +23,19 @@ export function App() { ## Components and docs -This package ships per-export documentation under `docs/`. The full index is in [`docs/llms.txt`](./docs/llms.txt) — readable by humans and by AI agents. +This package ships per-export documentation under `docs/`. The full index is in +[`docs/llms.txt`](./docs/llms.txt), which is readable by humans and AI agents. -Components fall into a [three-tier taxonomy](https://github.com/lukebennett88/luke-ui/blob/main/docs/adr/0001-component-tier-taxonomy.md): +Components follow the +[three-tier taxonomy](https://github.com/lukebennett88/luke-ui/blob/main/docs/adr/0001-component-tier-taxonomy.md): -- **Atoms** — single units (`Text`, `Icon`, `Heading`, …) -- **Composed** — opinionated combinations (`Button`, `TextField`, …) -- **Primitives** — building blocks for library authors (`button/primitive`, `field/primitive`, …) +- **Atoms**: single units such as `Text`, `Icon`, and `Heading` +- **Composed**: opinionated combinations such as `Button` and `TextField` +- **Primitives**: building blocks for library authors, such as `button/primitive` and + `field/primitive` -Atoms and composed components are app-developer-facing. Primitives are documented under `docs/` for library authors but excluded from the primary index. +Atoms and composed components are app-developer-facing. Primitives are documented under `docs/` for +library authors, but excluded from the primary index. ## License diff --git a/packages/@luke-ui/react/src/button/button.docs.md b/packages/@luke-ui/react/src/button/button.docs.md index c3e4530b..5844d9cb 100644 --- a/packages/@luke-ui/react/src/button/button.docs.md +++ b/packages/@luke-ui/react/src/button/button.docs.md @@ -1,22 +1,22 @@ -`Button` expects the Luke UI theme class to be applied at app/root level. See +`Button` expects the Luke UI theme class at the app or root level. See [Getting Started](/docs/getting-started). ```tsx ``` -## Best Practices +## Best practices -| Guidance | Practices | -| -------- | -------------------------------------------------------------------------------------------------------------------- | -| Do | Use one `tone="primary"` button per view for the main action. Use `neutral` or `ghost` for everything else. | -| Do | Write a label that describes the action ("Save changes", "Delete account"), not a vague label like "OK" or "Submit". | -| Do | Set `isPending` for actions that take time, like saving or submitting, so the user knows it's working. | -| Don't | Use `Button` for navigation. If it only takes the user to another page, use `Link` instead. | +| Guidance | Practices | +| -------- | -------------------------------------------------------------------------------------------------------------- | +| Do | Use one `tone="primary"` button for the main action in a view. Use `neutral` or `ghost` for secondary actions. | +| Do | Write a label that names the action, such as "Save changes" or "Delete account". Avoid vague labels like "OK". | +| Do | Set `isPending` while an action is in flight so the user can see that work is still happening. | +| Don't | Use `Button` for navigation. If the control only moves the user to another page, use `Link`. | ## Tone -Four tones: `primary` (default), `neutral`, `critical`, and `ghost`. +`Button` has four tones: `primary` (default), `neutral`, `critical`, and `ghost`. ```tsx @@ -32,7 +32,7 @@ Four tones: `primary` (default), `neutral`, `critical`, and `ghost`. ## Size -Two sizes: `medium` (default) and `small`. +`Button` has two sizes: `medium` (default) and `small`. ```tsx @@ -40,9 +40,8 @@ Two sizes: `medium` (default) and `small`. ## Icons -Use `startIcon` and `endIcon` to place an icon before or after the label. Icon -size is inherited from the button's own `size`, so no `size` prop is needed on -the icon itself. +Use `startIcon` and `endIcon` to place an icon before or after the label. The icon inherits the +button size, so the icon does not need its own `size` prop. ```tsx import { Icon } from '@luke-ui/react/icon'; @@ -52,7 +51,7 @@ import { Icon } from '@luke-ui/react/icon'; ## Disabled -Disabled buttons can't be focused or pressed. +Disabled buttons cannot be focused or pressed. ```tsx @@ -60,8 +59,8 @@ Disabled buttons can't be focused or pressed. ## Pending -Set `isPending` while an action is in flight. A spinner overlays the label and -the button becomes non-interactive. +Set `isPending` while an action is in flight. A spinner overlays the label and the button becomes +non-interactive. ```tsx @@ -77,20 +76,21 @@ Set `isBlock` to make the button fill the inline size of its container. ## Accessibility -`Button` wraps its children in `Text`, so it always has a visible accessible -name — no `aria-label` is needed. The `isPending` spinner is `aria-hidden` and -does not announce a busy state to screen readers; if the pending state needs to -be conveyed audibly, change the label text itself (e.g. "Saving…") while -`isPending` is set. +`Button` wraps its children in `Text`, so visible text usually provides the accessible name. You +normally do not need `aria-label`. + +The pending spinner is `aria-hidden` and does not announce busy state to screen readers. If screen +reader users need to hear the pending state, change the label text itself, for example to "Saving", +while `isPending` is set. ## Primitive Button -A lower-level `Button` primitive is available when you need full control over -children: custom loading states, render-prop children, or non-standard content. +The lower-level `Button` primitive is available when you need full control over children, such as +custom loading states, render-prop children, or non-standard content. ```ts import { Button } from '@luke-ui/react/button/primitive'; ``` -The primitive renders a single `
``` -- For a more subtle link, use `neutral`. -- On a dark background, use `inverted`. +Use `neutral` for a more subtle link. Use `inverted` on dark backgrounds. ## Standalone -A `Link` can be used on its own as `isStandalone` or inline within a sentence or -paragraph (default: `isStandalone={false}`). +Use `isStandalone` when a link stands on its own. Leave it `false` for links inside paragraph text. ```tsx @@ -57,10 +55,10 @@ paragraph (default: `isStandalone={false}`). ## Accessibility -Screen readers announce a disabled link as unavailable but not why — put the -reason in nearby visible text rather than relying on the disabled state alone. +Screen readers announce a disabled link as unavailable, but not why. Put the reason in nearby +visible text instead of relying on disabled state alone. ## When to use vs Button -Use `Link` to navigate to a new URL or route. Use `Button` for in-page actions -like saving, submitting, or opening a dialog. +Use `Link` to navigate to a new URL or route. Use `Button` for in-page actions, such as saving, +submitting, or opening a dialog. diff --git a/packages/@luke-ui/react/src/loading-skeleton/loading-skeleton.docs.md b/packages/@luke-ui/react/src/loading-skeleton/loading-skeleton.docs.md index 75c76dbe..b542d7b0 100644 --- a/packages/@luke-ui/react/src/loading-skeleton/loading-skeleton.docs.md +++ b/packages/@luke-ui/react/src/loading-skeleton/loading-skeleton.docs.md @@ -2,7 +2,7 @@ [Getting Started](/docs/getting-started). Use it when loading content should keep the same footprint as the loaded state. Text renders as an -inline skeleton. Elements keep their layout while a skeleton surface is painted over them. +inline skeleton. Elements keep their layout while the skeleton surface is painted over them. ```tsx {user?.name ?? 'Placeholder name'} @@ -16,16 +16,16 @@ inline skeleton. Elements keep their layout while a skeleton surface is painted All mounted skeletons use the same pulse timing, even when they mount at different times. -## Best Practices +## Best practices -| Guidance | Practices | -| -------- | --------------------------------------------------------------------------------------------------------------------- | -| Do | Wrap the real content so the skeleton matches its final size exactly. | -| Don't | Use `LoadingSkeleton` for content whose final size is unknown — a size mismatch causes layout shift when it resolves. | +| Guidance | Practices | +| -------- | ------------------------------------------------------------------------------------------------------------------- | +| Do | Wrap real content so the skeleton matches the final size exactly. | +| Don't | Use `LoadingSkeleton` for content whose final size is unknown. Size mismatch causes layout shift when loading ends. | ## Loading state -`isLoading` defaults to `true`. Pass `isLoading={false}` when the content is ready. +`isLoading` defaults to `true`. Pass `isLoading={false}` when content is ready. ```tsx @@ -35,7 +35,7 @@ All mounted skeletons use the same pulse timing, even when they mount at differe ## Multi-line text -Wrap text directly when copy spans more than one line. Each line gets its own skeleton shape. +Wrap text directly when the copy spans more than one line. Each line gets its own skeleton shape. ```tsx
@@ -61,7 +61,7 @@ element. ## LoadingSkeletonProvider Use `LoadingSkeletonProvider` when one loading state controls a group of skeletons. The provider -value overrides each descendant `isLoading` prop. +value overrides descendant `isLoading` props. ```tsx @@ -95,5 +95,6 @@ avatar. ## Accessibility -While loading, the skeleton is hidden from assistive technology and cannot be focused or clicked. It -sets `aria-hidden`, `inert`, `tabIndex={-1}`, and disables pointer events. +While loading, skeleton content is hidden from assistive technology and cannot be focused or +clicked. `LoadingSkeleton` sets `aria-hidden`, `inert`, `tabIndex={-1}`, and disables pointer +events. diff --git a/packages/@luke-ui/react/src/loading-spinner/loading-spinner.docs.md b/packages/@luke-ui/react/src/loading-spinner/loading-spinner.docs.md index ed2aa00a..cfe51498 100644 --- a/packages/@luke-ui/react/src/loading-spinner/loading-spinner.docs.md +++ b/packages/@luke-ui/react/src/loading-spinner/loading-spinner.docs.md @@ -1,5 +1,5 @@ -`LoadingSpinner` expects the Luke UI theme class to be applied at app/root level. -See [Getting Started](/docs/getting-started). +`LoadingSpinner` expects the Luke UI theme class at the app or root level. See +[Getting Started](/docs/getting-started). ```tsx @@ -10,7 +10,7 @@ times. ## Progress mode -Omit `value` for indeterminate progress. Pass `value` for determinate. +Omit `value` for indeterminate progress. Pass `value` for determinate progress. ```tsx @@ -30,5 +30,5 @@ Omit `value` for indeterminate progress. Pass `value` for determinate. ## Accessibility -`aria-label` defaults to `"pending"` when omitted. Override it with what's -loading (e.g. "Loading profile") for a clearer announcement. +`aria-label` defaults to `"pending"` when omitted. Override it with what is loading, such as +"Loading profile", for a clearer announcement. diff --git a/packages/@luke-ui/react/src/numeral/numeral.docs.md b/packages/@luke-ui/react/src/numeral/numeral.docs.md index e58df466..90083d8d 100644 --- a/packages/@luke-ui/react/src/numeral/numeral.docs.md +++ b/packages/@luke-ui/react/src/numeral/numeral.docs.md @@ -1,5 +1,5 @@ -Formatting is done via `Intl.NumberFormat` and respects the locale from React -Aria's `I18nProvider`. +`Numeral` formats numbers with `Intl.NumberFormat`. It respects locale from React Aria's +`I18nProvider`. ```tsx @@ -7,8 +7,8 @@ Aria's `I18nProvider`. ## Formats -`format` is derived from `currency` and `unit` when omitted. Pass it explicitly -for `'percent'` or `'decimal'` without a currency or unit. +`Numeral` infers `format` from `currency` or `unit` when omitted. Pass `format` explicitly for +`'percent'` or `'decimal'`, or when you want to be direct about currency and unit formatting. ```tsx @@ -28,6 +28,8 @@ for `'percent'` or `'decimal'` without a currency or unit. ## Compact notation +Use `abbreviate` for compact notation. + ```tsx ``` @@ -52,7 +54,7 @@ Pass a number for fixed fraction digits, or a `[min, max]` tuple for a range. `Numeral` throws in development when: -- `currency` and `unit` are both provided. +- both `currency` and `unit` are 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. diff --git a/packages/@luke-ui/react/src/styles/utilities.css.ts b/packages/@luke-ui/react/src/styles/utilities.css.ts index 1a2100d9..e8cb2d7f 100644 --- a/packages/@luke-ui/react/src/styles/utilities.css.ts +++ b/packages/@luke-ui/react/src/styles/utilities.css.ts @@ -50,11 +50,6 @@ const responsiveProperties = defineProperties({ paddingInlineStart: spaceScale, rowGap: spaceScale, }, - shorthands: { - p: ['padding'], - px: ['paddingInlineStart', 'paddingInlineEnd'], - py: ['paddingBlockStart', 'paddingBlockEnd'], - }, staticProperties: { alignItems: ['flex-start', 'center', 'flex-end', 'baseline', 'stretch'], blockSize: ['auto', '100%', 'fit-content', 'min-content', 'max-content'], diff --git a/packages/@luke-ui/react/src/styles/utilities.stories.tsx b/packages/@luke-ui/react/src/styles/utilities.stories.tsx index 9ecc4842..7e0cfc19 100644 --- a/packages/@luke-ui/react/src/styles/utilities.stories.tsx +++ b/packages/@luke-ui/react/src/styles/utilities.stories.tsx @@ -145,34 +145,6 @@ export const WithRenderProp = meta.story({ }, }); -/** - * Shorthands expand to multiple properties. `px` sets both inline padding - * sides; `py` sets both block padding sides. - */ -export const Shorthands = meta.story({ - render: () => { - const padding = createSprinkles({ px: 'large', py: 'small' }); - return ( -
-
- Large horizontal padding, small vertical padding -
-
- ); - }, -}); - /** * Overflow and sizing utilities control content clipping and element * dimensions. Use `textOverflow: 'ellipsis'` with `inlineSize` constraints diff --git a/packages/@luke-ui/react/src/text-field/text-field.docs.md b/packages/@luke-ui/react/src/text-field/text-field.docs.md index 426bd718..ca02f8c5 100644 --- a/packages/@luke-ui/react/src/text-field/text-field.docs.md +++ b/packages/@luke-ui/react/src/text-field/text-field.docs.md @@ -1,3 +1,6 @@ +Use `TextField` for a single text input with label, description, validation, and optional adornments +built in. + ```tsx ``` -## Best Practices +## Best practices -| Guidance | Practices | -| -------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| Do | Use `label` for every field where possible — it's picked up by more assistive tech and autofill heuristics than `aria-label` alone. | -| Don't | Rely on `placeholder` as a label substitute — it disappears once the user types and often fails color-contrast requirements. | +| Guidance | Practices | +| -------- | --------------------------------------------------------------------------------------------------------------------------- | +| Do | Use `label` for every field where possible. It works better with assistive technology and autofill than `aria-label` alone. | +| Don't | Use `placeholder` as a label substitute. It disappears after typing and often fails colour contrast requirements. | ## Required fields -Use `isRequired` and `necessityIndicator` to communicate mandatory fields. -`'icon'` renders a visual required marker; `'label'` appends "(required)" to the -label text. +Use `isRequired` and `necessityIndicator` to communicate mandatory fields. `'icon'` renders a visual +required marker. `'label'` appends "(required)" to the label text. ```tsx @@ -30,10 +32,13 @@ label text. ## Validation +Pass field-level validation through React Aria `Form`. Use `errorMessage` to render the validation +message. + ```tsx import { Form } from 'react-aria-components'; -
+ ` attribute -is intentionally omitted; `size` is reserved for the design-system variant. +`size` controls height and typography. The HTML numeric `` attribute is intentionally +omitted because `size` is reserved for the design-system variant. | Value | Description | | ---------- | --------------------- | @@ -78,8 +83,8 @@ is intentionally omitted; `size` is reserved for the design-system variant. ## Accessibility -When visual context already communicates purpose, omit `label` and provide an -accessible name with `aria-label` or `aria-labelledby` on the field itself. +When visual context already communicates purpose, you may omit `label` and provide an accessible +name with `aria-label` or `aria-labelledby` on the field. ```tsx @@ -87,8 +92,8 @@ accessible name with `aria-label` or `aria-labelledby` on the field itself. ## Primitive TextInput -A lower-level `TextInput` primitive is available when you need the input -without the label, description, or error slots that `Field` provides. +The lower-level `TextInput` primitive is available when you need the input without the label, +description, or error slots that `Field` provides. ```ts import { TextInput } from '@luke-ui/react/text-field/primitive'; diff --git a/packages/@luke-ui/react/src/text/text.docs.md b/packages/@luke-ui/react/src/text/text.docs.md index a04e3a1a..135a94eb 100644 --- a/packages/@luke-ui/react/src/text/text.docs.md +++ b/packages/@luke-ui/react/src/text/text.docs.md @@ -1,15 +1,18 @@ -`Text` expects the Luke UI theme class to be applied at app/root level. See [Getting Started](/docs/getting-started). +`Text` expects the Luke UI theme class at the app or root level. See +[Getting Started](/docs/getting-started). + +Use `Text` for styled text that should not create heading semantics. ```tsx The quick brown fox jumps over the lazy dog. ``` -## Best Practices +## Best practices -| Guidance | Practices | -| -------- | ------------------------------------------------------------------------------------------------------------------- | -| Do | Use `fontSize` tokens (e.g. `'h2'`) instead of arbitrary values, so text stays consistent with the rest of the app. | -| Don't | Use `Text` for section headings — use `Heading`, which manages semantic level automatically. | +| Guidance | Practices | +| -------- | ----------------------------------------------------------------------------------------------------------- | +| Do | Use `fontSize` tokens, such as `'h2'`, instead of arbitrary values so text stays consistent across the app. | +| Don't | Use `Text` for section headings. Use `Heading`, which manages semantic level automatically. | ## Typography @@ -25,8 +28,8 @@ ``` -See Token reference below for every valid `color`, `fontFamily`, `fontSize`, -`lineHeight`, and `fontWeight` value. +See the token reference below for valid `color`, `fontFamily`, `fontSize`, `lineHeight`, and +`fontWeight` values. ## Text transform and decoration @@ -62,7 +65,8 @@ See Token reference below for every valid `color`, `fontFamily`, `fontSize`, ### `color` tokens -`neutralSubtle`, `neutralBold`, `neutralDisabled`, `neutralBoldInverted`, `positive`, `informative`, `caution`, `critical`, `inherit` +`neutralSubtle`, `neutralBold`, `neutralDisabled`, `neutralBoldInverted`, `positive`, `informative`, +`caution`, `critical`, `inherit` ### `fontFamily` tokens @@ -70,7 +74,8 @@ See Token reference below for every valid `color`, `fontFamily`, `fontSize`, ### `fontSize` tokens -`xxsmall`, `xsmall`, `small`, `standard`, `medium`, `large`, `xlarge`, `xxlarge`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6` +`xxsmall`, `xsmall`, `small`, `standard`, `medium`, `large`, `xlarge`, `xxlarge`, `h1`, `h2`, `h3`, +`h4`, `h5`, `h6` ### `lineHeight` tokens @@ -90,7 +95,7 @@ See Token reference below for every valid `color`, `fontFamily`, `fontSize`, ## When to use vs Heading -Use `Heading` for actual section headings — it manages semantic level and -nesting automatically. Use `Text` with a heading-sized `fontSize` token when -the content looks like a heading but isn't semantically one (e.g. a large -stat number). +Use `Heading` for actual section headings because it manages semantic level nesting automatically. + +Use `Text` with a heading-sized `fontSize` token when content should look like a heading but is not +semantically one, such as a large stat number. diff --git a/packages/turbo-generators/README.md b/packages/turbo-generators/README.md index f4d989ff..8d2c221c 100644 --- a/packages/turbo-generators/README.md +++ b/packages/turbo-generators/README.md @@ -4,13 +4,11 @@ Custom generators for `turbo generate`. ## Generators -- `component`: Scaffolds Atom and Composed `@luke-ui/react` components, - package docs prose, Storybook stories, hosted docs wrappers, hosted docs - controls, and structural docs navigation. +- `component`: Scaffolds Atom or Composed `@luke-ui/react` components, package docs prose, Storybook + stories, hosted docs wrappers, hosted docs controls, and structural docs navigation. -The component generator asks for name, tier, docs group, and styling. Primitive -creation is intentionally excluded until it can be modeled with a parent -Composed component. +The component generator asks for name, tier, docs group, and styling. Primitive creation is +intentionally excluded until it can be modelled through its parent Composed component. ## Usage diff --git a/vite.config.ts b/vite.config.ts index a7c6a687..487f290c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -19,6 +19,7 @@ export default defineConfig({ jsxSingleQuote: false, overrides: [{ files: ['**/*.css.ts'], options: { sortImports: { sortSideEffects: false } } }], printWidth: 100, + proseWrap: 'always', quoteProps: 'as-needed', semi: true, singleAttributePerLine: false,