diff --git a/AGENTS.md b/AGENTS.md index 14880869..e19ddfc2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,8 +10,7 @@ - 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 can leave generated files missing. -- Scaffold components non-interactively: - `pnpm run generate:component --args `. +- Scaffold components non-interactively: `pnpm run generate:component --args `. - When you change code, update or delete the docs that describe it in the same change. This includes comments, JSDoc, MDX files in `apps/docs/content/docs/`, `README.md`, package READMEs, and files in `docs/`. See [docs/DOCUMENTATION.md](docs/DOCUMENTATION.md#keeping-docs-current). diff --git a/README.md b/README.md index 2357d3f4..88713bd0 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,8 @@ Useful repo commands: ## Docs - [Conventions](docs/CONVENTIONS.md): repo-wide coding conventions. -- [Components](docs/COMPONENTS.md): component tiers, package paths, and generator rules. +- [Components](docs/COMPONENTS.md): component and primitive structure, package paths, and generator + rules. - [Dependencies](docs/DEPENDENCIES.md): the catalog, the release quarantine, and Renovate. - [Documentation](docs/DOCUMENTATION.md): what to document, writing style, examples, and MDX structure. diff --git a/apps/docs/content/docs/components/actions/button.mdx b/apps/docs/content/docs/components/actions/button.mdx index 5165e65a..f8dd1b36 100644 --- a/apps/docs/content/docs/components/actions/button.mdx +++ b/apps/docs/content/docs/components/actions/button.mdx @@ -87,7 +87,7 @@ announce the pending status. ## Primitive -`Button` is the composed component for application UI. It wraps its label for truncation, provides +`Button` is the normal component for application UI. It wraps its label for truncation, provides icon slots, and supplies the pending spinner. Use the -[button primitive](/components/primitives/button) when you build a custom control and need to own -the child layout or loading treatment. +[button primitive](/components/primitives/button) when you need to own the child layout or loading +treatment. diff --git a/apps/docs/content/docs/components/primitives/button.mdx b/apps/docs/content/docs/components/primitives/button.mdx index 23db3119..b8dfaa88 100644 --- a/apps/docs/content/docs/components/primitives/button.mdx +++ b/apps/docs/content/docs/components/primitives/button.mdx @@ -1,11 +1,11 @@ --- title: Button primitive -description: Lower-level button styles and behaviour for custom composed controls. -source: packages/@luke-ui/react/src/button/primitive +description: Lower-level button styles and behaviour for custom control layouts. +source: packages/@luke-ui/react/src/primitives/button reactAria: https://react-spectrum.adobe.com/react-aria/Button.html props: - name: ButtonProps - path: packages/@luke-ui/react/src/button/primitive/index.tsx + path: packages/@luke-ui/react/src/primitives/button/index.tsx --- Use the button primitive to build a custom button pattern. It provides Luke UI button styles and @@ -16,9 +16,9 @@ application actions, use [`Button`](/components/actions/button). ## Composition -Use the primitive when a composed control needs custom children, render-prop children, or its own -loading layout. The composed `Button` wraps its label for truncation, adds start and end icon slots, -and supplies a pending spinner. The primitive does not. +Use the primitive when you need custom children, render-prop children, or your own loading layout. +`Button` wraps its label for truncation, adds start and end icon slots, and supplies a pending +spinner. The primitive does not. ## Appearance and size diff --git a/apps/docs/content/docs/components/primitives/checkbox.mdx b/apps/docs/content/docs/components/primitives/checkbox.mdx index 36744f08..c3097e4f 100644 --- a/apps/docs/content/docs/components/primitives/checkbox.mdx +++ b/apps/docs/content/docs/components/primitives/checkbox.mdx @@ -1,25 +1,25 @@ --- title: Checkbox primitive -description: Lower-level checkbox anatomy for custom composed form controls. -source: packages/@luke-ui/react/src/checkbox/primitive +description: Lower-level checkbox anatomy for custom form control layouts. +source: packages/@luke-ui/react/src/primitives/checkbox reactAria: https://react-spectrum.adobe.com/react-aria/Checkbox.html props: - name: CheckboxProps - path: packages/@luke-ui/react/src/checkbox/primitive/index.tsx + path: packages/@luke-ui/react/src/primitives/checkbox/index.tsx heading: Checkbox - name: CheckboxContentProps - path: packages/@luke-ui/react/src/checkbox/primitive/index.tsx + path: packages/@luke-ui/react/src/primitives/checkbox/index.tsx heading: CheckboxContent - name: CheckboxControlProps - path: packages/@luke-ui/react/src/checkbox/primitive/index.tsx + path: packages/@luke-ui/react/src/primitives/checkbox/index.tsx heading: CheckboxControl - name: CheckboxIndicatorProps - path: packages/@luke-ui/react/src/checkbox/primitive/index.tsx + path: packages/@luke-ui/react/src/primitives/checkbox/index.tsx heading: CheckboxIndicator --- -Use the Checkbox primitive when a composed control needs a custom label layout or extra content. For -normal application forms, use [`Checkbox`](/components/forms/checkbox). +Use the Checkbox primitive when you need a custom label layout or extra content. For normal +application forms, use [`Checkbox`](/components/forms/checkbox). diff --git a/apps/docs/content/docs/components/primitives/combobox.mdx b/apps/docs/content/docs/components/primitives/combobox.mdx index d4a74a6c..fdee22f3 100644 --- a/apps/docs/content/docs/components/primitives/combobox.mdx +++ b/apps/docs/content/docs/components/primitives/combobox.mdx @@ -1,47 +1,47 @@ --- title: Combobox primitives description: Lower-level parts for composing a searchable single-select combobox. -source: packages/@luke-ui/react/src/combobox-field/primitive +source: packages/@luke-ui/react/src/primitives/combobox reactAria: https://react-spectrum.adobe.com/react-aria/ComboBox.html props: - name: ComboboxRootProps - path: packages/@luke-ui/react/src/combobox-field/primitive/root.tsx + path: packages/@luke-ui/react/src/primitives/combobox/root.tsx heading: ComboboxRoot - name: ComboboxInputGroupProps - path: packages/@luke-ui/react/src/combobox-field/primitive/input-group.tsx + path: packages/@luke-ui/react/src/primitives/combobox/input-group.tsx heading: ComboboxInputGroup - name: ComboboxInputProps - path: packages/@luke-ui/react/src/combobox-field/primitive/input.tsx + path: packages/@luke-ui/react/src/primitives/combobox/input.tsx heading: ComboboxInput - name: ComboboxClearButtonProps - path: packages/@luke-ui/react/src/combobox-field/primitive/clear-button.tsx + path: packages/@luke-ui/react/src/primitives/combobox/clear-button.tsx heading: ComboboxClearButton - name: ComboboxTriggerProps - path: packages/@luke-ui/react/src/combobox-field/primitive/trigger.tsx + path: packages/@luke-ui/react/src/primitives/combobox/trigger.tsx heading: ComboboxTrigger - name: ComboboxPopoverProps - path: packages/@luke-ui/react/src/combobox-field/primitive/popover.tsx + path: packages/@luke-ui/react/src/primitives/combobox/popover.tsx heading: ComboboxPopover - name: ComboboxListBoxProps - path: packages/@luke-ui/react/src/combobox-field/primitive/listbox.tsx + path: packages/@luke-ui/react/src/primitives/combobox/listbox.tsx heading: ComboboxListBox - name: ComboboxItemProps - path: packages/@luke-ui/react/src/combobox-field/primitive/item.tsx + path: packages/@luke-ui/react/src/primitives/combobox/item.tsx heading: ComboboxItem - name: ComboboxLoadMoreItemProps - path: packages/@luke-ui/react/src/combobox-field/primitive/item.tsx + path: packages/@luke-ui/react/src/primitives/combobox/item.tsx heading: ComboboxLoadMoreItem - name: ComboboxSectionProps - path: packages/@luke-ui/react/src/combobox-field/primitive/section.tsx + path: packages/@luke-ui/react/src/primitives/combobox/section.tsx heading: ComboboxSection - name: ComboboxEmptyStateProps - path: packages/@luke-ui/react/src/combobox-field/primitive/empty-state.tsx + path: packages/@luke-ui/react/src/primitives/combobox/empty-state.tsx heading: ComboboxEmptyState --- Use these primitives when [`ComboboxField`](/components/forms/combobox-field) does not fit your -layout or loading UI. They expose the control, popover, listbox, and option parts used by the -composed field. Application code should usually use `ComboboxField`. +layout or loading UI. They expose the control, popover, listbox, and option parts used by +`ComboboxField`. Start with `ComboboxField` when it fits. diff --git a/apps/docs/content/docs/components/primitives/field.mdx b/apps/docs/content/docs/components/primitives/field.mdx index 24d8335c..8ff625a8 100644 --- a/apps/docs/content/docs/components/primitives/field.mdx +++ b/apps/docs/content/docs/components/primitives/field.mdx @@ -1,40 +1,40 @@ --- title: Field primitive description: Shared label, description, and validation parts for custom fields. -source: packages/@luke-ui/react/src/field/primitive +source: packages/@luke-ui/react/src/primitives/field props: - name: FieldProps - path: packages/@luke-ui/react/src/field/primitive/index.tsx + path: packages/@luke-ui/react/src/primitives/field/index.tsx heading: Field - name: FieldLabelProps - path: packages/@luke-ui/react/src/field/primitive/label.tsx + path: packages/@luke-ui/react/src/primitives/field/label.tsx heading: FieldLabel - name: FieldDescriptionProps - path: packages/@luke-ui/react/src/field/primitive/description.tsx + path: packages/@luke-ui/react/src/primitives/field/description.tsx heading: FieldDescription - name: FieldErrorProps - path: packages/@luke-ui/react/src/field/primitive/error.tsx + path: packages/@luke-ui/react/src/primitives/field/error.tsx heading: FieldError --- Use the field primitives to build a custom field with Luke UI labels, descriptions, and validation -messages. Application code should usually use a composed field such as -[`TextField`](/components/forms/text-field) or [`ComboboxField`](/components/forms/combobox-field). +messages. Start with [`TextField`](/components/forms/text-field) or +[`ComboboxField`](/components/forms/combobox-field) when one of those components fits. -## Choose a composed field or primitives +## Choose a component or primitives | Guidance | Practices | | -------- | -------------------------------------------------------------------------------------------------------------------- | -| Do | Use a composed field such as [`TextField`](/components/forms/text-field) for a supported control. | +| Do | Use [`TextField`](/components/forms/text-field) or another supported field when it fits. | | Do | Use field primitives when a custom control needs a different documented arrangement. | | Don't | Assume adjacent parts create semantics. Connect the label, description, and control through their public HTML props. | ## Anatomy -`Field` arranges a label, control, description, and validation message. Composed fields connect -these parts for their supported controls. +`Field` arranges a label, control, description, and validation message. Field components such as +`TextField` connect these parts for their supported controls. For a custom structure, use `FieldLabel`, `FieldDescription`, and `FieldError` with the field control. Connect each part through its public HTML props, as the primary example shows. diff --git a/apps/docs/content/docs/components/primitives/input-group.mdx b/apps/docs/content/docs/components/primitives/input-group.mdx index eb782c15..f82e70bb 100644 --- a/apps/docs/content/docs/components/primitives/input-group.mdx +++ b/apps/docs/content/docs/components/primitives/input-group.mdx @@ -1,19 +1,19 @@ --- title: InputGroup primitives description: Lower-level parts for composing a text input with content at either end. -source: packages/@luke-ui/react/src/text-field/primitive +source: packages/@luke-ui/react/src/primitives/input-group props: - name: InputGroupProps - path: packages/@luke-ui/react/src/text-field/primitive/index.tsx + path: packages/@luke-ui/react/src/primitives/input-group/index.tsx heading: InputGroup - name: InputGroupInputProps - path: packages/@luke-ui/react/src/text-field/primitive/index.tsx + path: packages/@luke-ui/react/src/primitives/input-group/index.tsx heading: InputGroupInput - name: InputGroupPrefixProps - path: packages/@luke-ui/react/src/text-field/primitive/index.tsx + path: packages/@luke-ui/react/src/primitives/input-group/index.tsx heading: InputGroupPrefix - name: InputGroupSuffixProps - path: packages/@luke-ui/react/src/text-field/primitive/index.tsx + path: packages/@luke-ui/react/src/primitives/input-group/index.tsx heading: InputGroupSuffix --- diff --git a/apps/docs/content/docs/components/typography/heading.mdx b/apps/docs/content/docs/components/typography/heading.mdx index 4c8ac69c..1af2d2f8 100644 --- a/apps/docs/content/docs/components/typography/heading.mdx +++ b/apps/docs/content/docs/components/typography/heading.mdx @@ -26,6 +26,9 @@ children. Explicit h2 ``` +To read the current level in a custom heading-like component, use `useHeadingLevel`. Unlike a nested +`HeadingLevels`, it does not advance the level. + Do not skip levels, such as an h2 followed by an h4. Someone who uses a screen reader navigates a page by heading level. diff --git a/apps/docs/content/docs/docs/composition.mdx b/apps/docs/content/docs/docs/composition.mdx index f92dfe47..609524d0 100644 --- a/apps/docs/content/docs/docs/composition.mdx +++ b/apps/docs/content/docs/docs/composition.mdx @@ -1,33 +1,25 @@ --- title: Composition and customisation -description: Choose a Luke UI component, primitive, recipe, or token for custom interface patterns. +description: Choose a Luke UI component, primitive, or token for custom interface patterns. --- Start with a semantic component when its purpose matches the task. It handles the intended behaviour, accessibility, and visual treatment together. Compose from a documented primitive only when a component's props do not fit. -## Start with the primary API +## Start with the component API -Atoms and composed components are Luke UI's primary API for application code. They handle common -complexity for you and avoid boilerplate. Primitives are public too, but are the secondary API for -custom composition. The `/primitive` path makes that lower-level audience visible. - -| Tier | Audience | Import path | Use it for | -| --------- | --------------- | -------------------------------------- | -------------------------------------------------------------------------- | -| Atom | App developers | `@luke-ui/react/` | One complete unit, such as `Text`, `Link`, or `Icon`. | -| Composed | App developers | `@luke-ui/react/` | A ready-to-use pattern, such as `Button`, `TextField`, or `ComboboxField`. | -| Primitive | Library authors | `@luke-ui/react//primitive` | A custom composed component that needs lower-level behaviour or structure. | +Luke UI components handle common complexity together. Use a primitive only when a component's props +do not fit a custom composition. ```tsx import { Button } from '@luke-ui/react/button'; -import { InputGroup } from '@luke-ui/react/text-field/primitive'; +import { InputGroup } from '@luke-ui/react/primitives/input-group'; ``` -Use a composed component for ordinary application UI. For example, `Button` includes its standard -label, icon slots, and pending treatment. Use its primitive only to build a related composed -component with its own structure. For example, use a different child layout or loading treatment. -Keep Luke UI button behaviour and styling. +`Button` includes its standard label, icon slots, and pending treatment. Use the button primitive +when you need a different child layout or loading treatment while keeping Luke UI button behaviour +and styling. Each primitive page documents the structure and accessibility responsibilities for that primitive: the label, description, state, and control relationships it expects. Preserve or author those @@ -47,15 +39,15 @@ primitives when `ComboboxField` does not fit your control, popover, or loading U title="Composition and customisation — Build a custom composition" /> -Give the custom component the same accessibility care as a composed component. Preserve its -documented structure. Provide an accessible name. Keep keyboard and focus behaviour intact. Use an -atom or normal React element for surrounding content when no primitive is needed. +Give the custom component the same accessibility care as a normal component. Preserve its documented +structure. Provide an accessible name. Keep keyboard and focus behaviour intact. Use a normal React +element for surrounding content when no primitive is needed. ## Render a different DOM component -The Button primitive exposes `render` when you need a small implementation detail that the primary -API does not provide. Spread the supplied DOM props onto the expected `; -``` - Use public semantic variables from `@luke-ui/react/theme` for custom surfaces, typography, colour, radius, and depth. They follow the active theme and colour mode. They do not couple the component to selectors, generated palette values, or theme implementation details. @@ -112,6 +90,6 @@ import { vars } from '@luke-ui/react/theme'; />; ``` -Read [Styling](/docs/styling) for recipes, semantic variables, and layout utilities. Read the +Read [Styling](/docs/styling) for semantic variables and layout utilities. Read the [primitive documentation](/components/primitives/button) before you build a custom component from a specific primitive. diff --git a/apps/docs/content/docs/docs/installation.mdx b/apps/docs/content/docs/docs/installation.mdx index 1dfe7d77..21ac8330 100644 --- a/apps/docs/content/docs/docs/installation.mdx +++ b/apps/docs/content/docs/docs/installation.mdx @@ -5,10 +5,11 @@ description: Install Luke UI, apply a bundled theme, and render a component. ## Install Luke UI -Install the package in your application. +Install Luke UI and React Aria Components. Luke UI uses the application's React Aria Components +instance so shared contexts remain compatible. ```bash -pnpm add @luke-ui/react +pnpm add @luke-ui/react react-aria-components ``` ## Set up the theme @@ -41,7 +42,7 @@ must use a specific mode. Without it, Luke UI follows the system preference. - Learn how component APIs, recipes, utilities, and tokens fit together. + Learn how component APIs, utilities, and tokens fit together. Build responsive structure with Box. @@ -50,6 +51,6 @@ must use a specific mode. Without it, Luke UI follows the system preference. Understand how the theme root, identity, and tokens fit together. - Browse the components available to application developers. + Browse the components available to developers. diff --git a/apps/docs/content/docs/docs/layout.mdx b/apps/docs/content/docs/docs/layout.mdx index aba17d1b..2bfe343e 100644 --- a/apps/docs/content/docs/docs/layout.mdx +++ b/apps/docs/content/docs/docs/layout.mdx @@ -79,6 +79,6 @@ defines its spacing scale and typography styles in source. See the Box example, props, and custom div rendering contract. - Learn where layout utilities sit beside components and recipes. + Learn where layout utilities sit beside components. diff --git a/apps/docs/content/docs/docs/styling.mdx b/apps/docs/content/docs/docs/styling.mdx index 64cfbf3d..71056f04 100644 --- a/apps/docs/content/docs/docs/styling.mdx +++ b/apps/docs/content/docs/docs/styling.mdx @@ -1,8 +1,7 @@ --- title: Styling description: - Choose component props, layout utilities, recipes, variables, or themes. Do not override - internals. + Choose component props, layout utilities, variables, or themes. Do not override internals. --- Luke UI ships static CSS. It does not inject styles at runtime. Import the shared stylesheet and a @@ -14,17 +13,17 @@ Work from the most specific public API to the broadest one. 1. Use a component's props for its supported appearance, size, state, and behaviour. 2. Use layout utilities for spacing, sizing, and responsive structure around components. -3. Build custom UI with public recipes and semantic variables when the component is not provided. +3. Use semantic variables for application-owned custom UI when no component fits. 4. Author a custom theme when the product needs a different visual foundation everywhere. Read [Composition](/docs/composition) when the choice is between a component and a documented primitive rather than between styling mechanisms. -| Guidance | Practices | -| -------- | ---------------------------------------------------------------------------------------------------- | -| Do | Use a supported component prop for its appearance, size, state, or behaviour. | -| Do | Use a layout utility or recipe for its element contract, and `vars` for application-owned custom UI. | -| Don't | Target generated selectors or implementation-state attributes such as `data-pressed`. | +| Guidance | Practices | +| -------- | ------------------------------------------------------------------------------------------ | +| Do | Use a supported component prop for its appearance, size, state, or behaviour. | +| Do | Use a layout utility for its element contract, and `vars` for application-owned custom UI. | +| Don't | Target generated selectors or implementation-state attributes such as `data-pressed`. | Do not override component recipe selectors or implementation-state attributes such as `data-hovered`, `data-pressed`, or `data-focus-visible`. They are not supported styling hooks and @@ -44,13 +43,7 @@ defines its type and spacing steps in source. responsive structure around components. They do not set semantic colour, typography, or pseudo states. Read [Layout](/docs/layout) for the full API. -### Build custom UI with public recipes and variables - -Recipes from `@luke-ui/react/recipes` are public, component-specific styling APIs. Use one when a -custom composition follows that recipe's documented element contract. Do not copy or target its -generated selectors. - - +### Use semantic variables for custom UI The public `vars` token contract lets an application-owned element follow the active theme. diff --git a/apps/docs/src/components/components-index.tsx b/apps/docs/src/components/components-index.tsx index 956faf1f..81d08128 100644 --- a/apps/docs/src/components/components-index.tsx +++ b/apps/docs/src/components/components-index.tsx @@ -1,6 +1,5 @@ import { Box } from '@luke-ui/react/box'; -import { Heading } from '@luke-ui/react/heading'; -import { HeadingLevels } from '@luke-ui/react/heading-context'; +import { Heading, HeadingLevels } from '@luke-ui/react/heading'; import { Card, Cards } from 'fumadocs-ui/components/card'; import type { JSX } from 'react'; import type { ComponentIndexGroup } from '../generated/components-index.generated.js'; diff --git a/apps/docs/src/components/example-block.tsx b/apps/docs/src/components/example-block.tsx index 5add4896..0114296b 100644 --- a/apps/docs/src/components/example-block.tsx +++ b/apps/docs/src/components/example-block.tsx @@ -1,9 +1,8 @@ import { Box } from '@luke-ui/react/box'; -import { Button } from '@luke-ui/react/button'; +import { Button, buttonRecipe } from '@luke-ui/react/button'; import { Icon } from '@luke-ui/react/icon'; import { LoadingSkeleton } from '@luke-ui/react/loading-skeleton'; import { LoadingSpinner } from '@luke-ui/react/loading-spinner'; -import { button } from '@luke-ui/react/recipes'; import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock'; import type { ComponentType, JSX, ReactNode } from 'react'; import { Suspense, use, useId, useState } from 'react'; @@ -48,7 +47,7 @@ function ExampleContent({ mode, src, title }: ExampleBlockProps): JSX.Element { actions={ - + Open in playground diff --git a/apps/docs/src/components/not-found.tsx b/apps/docs/src/components/not-found.tsx index af7a5ca7..88768529 100644 --- a/apps/docs/src/components/not-found.tsx +++ b/apps/docs/src/components/not-found.tsx @@ -1,5 +1,5 @@ +import { buttonRecipe } from '@luke-ui/react/button'; import { Heading } from '@luke-ui/react/heading'; -import { button } from '@luke-ui/react/recipes'; import { Text } from '@luke-ui/react/text'; import { DocsLink } from './docs-link.js'; import { SiteNav } from './site-nav.js'; @@ -17,7 +17,7 @@ export function NotFound() { The page you are looking for might have been removed, had its name changed, or is temporarily unavailable. - + Back to Home diff --git a/apps/docs/src/components/playground/icon-toggle-button-group.tsx b/apps/docs/src/components/playground/icon-toggle-button-group.tsx index 3082af5d..86823cb7 100644 --- a/apps/docs/src/components/playground/icon-toggle-button-group.tsx +++ b/apps/docs/src/components/playground/icon-toggle-button-group.tsx @@ -1,6 +1,6 @@ +import { buttonRecipe } from '@luke-ui/react/button'; import type { IconName } from '@luke-ui/react/icon'; import { Icon } from '@luke-ui/react/icon'; -import { button } from '@luke-ui/react/recipes'; import type { ComponentProps } from 'react'; import type { Selection } from 'react-aria-components/GridList'; import { ToggleButton } from 'react-aria-components/ToggleButton'; @@ -53,7 +53,7 @@ export function IconToggleButtonGroup({ {options.map(({ icon, label: optionLabel, value: optionValue }) => ( ({ > {options.map(({ label: optionLabel, value: optionValue }) => ( { return ; diff --git a/apps/docs/src/examples/checkbox-primitive/basic.tsx b/apps/docs/src/examples/checkbox-primitive/basic.tsx index 6fed4f64..9ab1e069 100644 --- a/apps/docs/src/examples/checkbox-primitive/basic.tsx +++ b/apps/docs/src/examples/checkbox-primitive/basic.tsx @@ -3,8 +3,8 @@ import { CheckboxContent, CheckboxControl, CheckboxIndicator, -} from '@luke-ui/react/checkbox/primitive'; -import { FieldDescription, FieldError } from '@luke-ui/react/field/primitive'; +} from '@luke-ui/react/primitives/checkbox'; +import { FieldDescription, FieldError } from '@luke-ui/react/primitives/field'; export default () => { return ( diff --git a/apps/docs/src/examples/combobox-field/basic.tsx b/apps/docs/src/examples/combobox-field/basic.tsx index 7ec021cc..ce968643 100644 --- a/apps/docs/src/examples/combobox-field/basic.tsx +++ b/apps/docs/src/examples/combobox-field/basic.tsx @@ -1,6 +1,6 @@ import { Box } from '@luke-ui/react/box'; import { ComboboxField } from '@luke-ui/react/combobox-field'; -import { ComboboxItem } from '@luke-ui/react/combobox-field/primitive'; +import { ComboboxItem } from '@luke-ui/react/primitives/combobox'; type Fruit = { id: string; label: string }; diff --git a/apps/docs/src/examples/combobox-field/grouped.tsx b/apps/docs/src/examples/combobox-field/grouped.tsx index 3a36f96e..b5a1e5f6 100644 --- a/apps/docs/src/examples/combobox-field/grouped.tsx +++ b/apps/docs/src/examples/combobox-field/grouped.tsx @@ -1,5 +1,5 @@ import { ComboboxField } from '@luke-ui/react/combobox-field'; -import { ComboboxItem, ComboboxSection } from '@luke-ui/react/combobox-field/primitive'; +import { ComboboxItem, ComboboxSection } from '@luke-ui/react/primitives/combobox'; export default () => { return ( diff --git a/apps/docs/src/examples/combobox-field/required.tsx b/apps/docs/src/examples/combobox-field/required.tsx index f83ac2b2..a981b3b7 100644 --- a/apps/docs/src/examples/combobox-field/required.tsx +++ b/apps/docs/src/examples/combobox-field/required.tsx @@ -1,5 +1,5 @@ import { ComboboxField } from '@luke-ui/react/combobox-field'; -import { ComboboxItem } from '@luke-ui/react/combobox-field/primitive'; +import { ComboboxItem } from '@luke-ui/react/primitives/combobox'; const countries = [ { id: 'australia', label: 'Australia' }, diff --git a/apps/docs/src/examples/combobox-field/validation.tsx b/apps/docs/src/examples/combobox-field/validation.tsx index 0f1ccded..dc6a0ac9 100644 --- a/apps/docs/src/examples/combobox-field/validation.tsx +++ b/apps/docs/src/examples/combobox-field/validation.tsx @@ -1,5 +1,5 @@ import { ComboboxField } from '@luke-ui/react/combobox-field'; -import { ComboboxItem } from '@luke-ui/react/combobox-field/primitive'; +import { ComboboxItem } from '@luke-ui/react/primitives/combobox'; const countries = [ { id: 'australia', label: 'Australia' }, diff --git a/apps/docs/src/examples/combobox-primitive/basic.tsx b/apps/docs/src/examples/combobox-primitive/basic.tsx index 4f235618..47fd9f1c 100644 --- a/apps/docs/src/examples/combobox-primitive/basic.tsx +++ b/apps/docs/src/examples/combobox-primitive/basic.tsx @@ -1,3 +1,4 @@ +import { Icon } from '@luke-ui/react/icon'; import { ComboboxInput, ComboboxInputGroup, @@ -6,8 +7,7 @@ import { ComboboxPopover, ComboboxRoot, ComboboxTrigger, -} from '@luke-ui/react/combobox-field/primitive'; -import { Icon } from '@luke-ui/react/icon'; +} from '@luke-ui/react/primitives/combobox'; export default () => { return ( diff --git a/apps/docs/src/examples/composition/amount-field.tsx b/apps/docs/src/examples/composition/amount-field.tsx index 0bb22c85..88e87bb1 100644 --- a/apps/docs/src/examples/composition/amount-field.tsx +++ b/apps/docs/src/examples/composition/amount-field.tsx @@ -1,5 +1,9 @@ -import { Field } from '@luke-ui/react/field/primitive'; -import { InputGroup, InputGroupInput, InputGroupPrefix } from '@luke-ui/react/text-field/primitive'; +import { Field } from '@luke-ui/react/primitives/field'; +import { + InputGroup, + InputGroupInput, + InputGroupPrefix, +} from '@luke-ui/react/primitives/input-group'; export default () => { return ( diff --git a/apps/docs/src/examples/composition/animated-button.tsx b/apps/docs/src/examples/composition/animated-button.tsx index d4d58ac2..f67266ff 100644 --- a/apps/docs/src/examples/composition/animated-button.tsx +++ b/apps/docs/src/examples/composition/animated-button.tsx @@ -1,4 +1,4 @@ -import { Button } from '@luke-ui/react/button/primitive'; +import { Button } from '@luke-ui/react/primitives/button'; import { mergeProps } from '@luke-ui/react/utils'; export default () => { diff --git a/apps/docs/src/examples/field-primitive/basic.tsx b/apps/docs/src/examples/field-primitive/basic.tsx index 4d9d26bd..927c824f 100644 --- a/apps/docs/src/examples/field-primitive/basic.tsx +++ b/apps/docs/src/examples/field-primitive/basic.tsx @@ -1,5 +1,5 @@ -import { Field, FieldDescription, FieldLabel } from '@luke-ui/react/field/primitive'; -import { InputGroup, InputGroupInput } from '@luke-ui/react/text-field/primitive'; +import { Field, FieldDescription, FieldLabel } from '@luke-ui/react/primitives/field'; +import { InputGroup, InputGroupInput } from '@luke-ui/react/primitives/input-group'; export default () => { return ( diff --git a/apps/docs/src/examples/forms/validation.tsx b/apps/docs/src/examples/forms/validation.tsx index 943e1442..b0fe7bb0 100644 --- a/apps/docs/src/examples/forms/validation.tsx +++ b/apps/docs/src/examples/forms/validation.tsx @@ -2,7 +2,7 @@ import { Box } from '@luke-ui/react/box'; import { Button } from '@luke-ui/react/button'; import { Checkbox } from '@luke-ui/react/checkbox'; import { ComboboxField } from '@luke-ui/react/combobox-field'; -import { ComboboxItem } from '@luke-ui/react/combobox-field/primitive'; +import { ComboboxItem } from '@luke-ui/react/primitives/combobox'; import { TextField } from '@luke-ui/react/text-field'; import type { SubmitEvent } from 'react'; import { useState } from 'react'; diff --git a/apps/docs/src/examples/heading/automatic-leveling.tsx b/apps/docs/src/examples/heading/automatic-leveling.tsx index f40e0706..43fbd9a0 100644 --- a/apps/docs/src/examples/heading/automatic-leveling.tsx +++ b/apps/docs/src/examples/heading/automatic-leveling.tsx @@ -1,6 +1,5 @@ import { Box } from '@luke-ui/react/box'; -import { Heading } from '@luke-ui/react/heading'; -import { HeadingLevels } from '@luke-ui/react/heading-context'; +import { Heading, HeadingLevels } from '@luke-ui/react/heading'; export default () => { return ( diff --git a/apps/docs/src/examples/icon/size-context.tsx b/apps/docs/src/examples/icon/size-context.tsx index eacedc7e..fad60527 100644 --- a/apps/docs/src/examples/icon/size-context.tsx +++ b/apps/docs/src/examples/icon/size-context.tsx @@ -1,6 +1,5 @@ import { Box } from '@luke-ui/react/box'; -import { Icon } from '@luke-ui/react/icon'; -import { IconSizeProvider } from '@luke-ui/react/icon-size-context'; +import { Icon, IconSizeProvider } from '@luke-ui/react/icon'; export default () => { return ( diff --git a/apps/docs/src/examples/input-group-primitive/basic.tsx b/apps/docs/src/examples/input-group-primitive/basic.tsx index fb864964..d74031f1 100644 --- a/apps/docs/src/examples/input-group-primitive/basic.tsx +++ b/apps/docs/src/examples/input-group-primitive/basic.tsx @@ -3,7 +3,7 @@ import { InputGroupInput, InputGroupPrefix, InputGroupSuffix, -} from '@luke-ui/react/text-field/primitive'; +} from '@luke-ui/react/primitives/input-group'; export default () => { return ( diff --git a/apps/docs/src/lib/generate-props-pages.test.ts b/apps/docs/src/lib/generate-props-pages.test.ts index 062fa0ba..cb31554a 100644 --- a/apps/docs/src/lib/generate-props-pages.test.ts +++ b/apps/docs/src/lib/generate-props-pages.test.ts @@ -50,13 +50,13 @@ test('renders a multi-entry Props page with a heading per entry', () => { const frontmatter = parseComponentFrontmatter(`--- title: Field primitive description: Shared label, description, and validation parts for custom fields. -source: packages/@luke-ui/react/src/field/primitive +source: packages/@luke-ui/react/src/primitives/field props: - name: FieldProps - path: packages/@luke-ui/react/src/field/primitive/index.tsx + path: packages/@luke-ui/react/src/primitives/field/index.tsx heading: Field - name: FieldLabelProps - path: packages/@luke-ui/react/src/field/primitive/label.tsx + path: packages/@luke-ui/react/src/primitives/field/label.tsx heading: FieldLabel --- `); @@ -64,7 +64,7 @@ props: expect(renderPropsPage(frontmatter)).toBe(`--- title: Field primitive description: Shared label, description, and validation parts for custom fields. -source: packages/@luke-ui/react/src/field/primitive +source: packages/@luke-ui/react/src/primitives/field --- {/* Generated by scripts/generate-props-pages.ts. Do not edit. */} @@ -73,12 +73,12 @@ source: packages/@luke-ui/react/src/field/primitive ### Field - + ### FieldLabel `); diff --git a/apps/docs/src/samples/styling/primitive.tsx b/apps/docs/src/samples/styling/primitive.tsx index beca417d..dc556663 100644 --- a/apps/docs/src/samples/styling/primitive.tsx +++ b/apps/docs/src/samples/styling/primitive.tsx @@ -1,4 +1,4 @@ -import { Button } from '@luke-ui/react/button/primitive'; +import { Button } from '@luke-ui/react/primitives/button'; export function SaveShortcutButton() { return ( diff --git a/apps/docs/src/samples/styling/recipe.tsx b/apps/docs/src/samples/styling/recipe.tsx deleted file mode 100644 index 77ab14d8..00000000 --- a/apps/docs/src/samples/styling/recipe.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { button } from '@luke-ui/react/recipes'; - -export function SaveButton() { - return ( - - ); -} diff --git a/apps/docs/vite.config.ts b/apps/docs/vite.config.ts index 165b47d5..c165077b 100644 --- a/apps/docs/vite.config.ts +++ b/apps/docs/vite.config.ts @@ -158,6 +158,11 @@ export default defineConfig(async () => { netlify(), ]), resolve: { + alias: { + '#recipe-engine': fileURLToPath( + new URL('../../packages/@luke-ui/react/src/styles/recipe-engine.ts', import.meta.url), + ), + }, tsconfigPaths: true, }, server: { diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md index 41fe8721..9f62cc16 100644 --- a/docs/COMPONENTS.md +++ b/docs/COMPONENTS.md @@ -2,55 +2,35 @@ Use this guide when you add or change a public component in `@luke-ui/react`. -## Component tiers +## Components and primitives -Luke UI uses three component tiers. The tier decides the export path, docs page, and audience. +Luke UI exposes one normal component API. Start with the component that fits the use case. -| Tier | Audience | Rule | Examples | -| --------- | --------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Atom | App developers | Presents one conceptual unit. It may compose other atoms internally, but consumers treat it as one unit. | `Text`, `Link`, `Icon`, `Heading`, `Numeral`, `Emoji`, `LoadingSpinner` | -| Composed | App developers | Combines atoms and primitives into an opinionated, ready-to-use pattern. | `Button`, `IconButton`, `TextField`, `ComboboxField` | -| Primitive | Library authors | Provides lower-level public API for composed components or custom components. | `button/primitive`, `field/primitive`, `text-field/primitive`, `combobox-field/primitive` | - -A primitive can be one component, such as `Field`, or a group of related components, such as the -input-group and combobox primitives. - -Do not call atoms or primitives "base" or "raw" in source comments or docs. Use the tier name. - -`Field` is a primitive even though it composes other components. App developers reach field UI -through wrappers such as `TextField` and `ComboboxField`. - -## Package paths - -Atoms and composed components export from their bare package path: +When the component API does not cover a custom composition, drop down to a primitive under +`@luke-ui/react/primitives/*`. Primitives describe a lower-level composition API, not implementation +simplicity. Foundational components such as `Text`, `Icon`, `Heading`, and `Box` remain normal +component entrypoints. ```ts import { Button } from '@luke-ui/react/button'; import { Text } from '@luke-ui/react/text'; +import { InputGroup } from '@luke-ui/react/primitives/input-group'; +import { Field, FieldLabel } from '@luke-ui/react/primitives/field'; +import { ComboboxRoot } from '@luke-ui/react/primitives/combobox'; ``` -Primitives that support a composed component export from `[composed]/primitive`: - -```ts -import { InputGroup } from '@luke-ui/react/text-field/primitive'; -import { Field, FieldLabel } from '@luke-ui/react/field/primitive'; -import { ComboboxRoot } from '@luke-ui/react/combobox-field/primitive'; -``` - -Do not add top-level primitive paths such as `@luke-ui/react/field` or `@luke-ui/react/input-group`. -The `/primitive` segment makes the lower-level audience visible. - -Keep primitives public when consumers need them to build custom composed components. Do not make a -primitive internal only to simplify the export map. +Do not add a root `@luke-ui/react/primitives` barrel. Import each primitive entrypoint explicitly. ## Component creation -Use the generator for new atoms and composed components: +Use the component generator for new normal component entrypoints: ```sh -pnpm run generate:component --args +pnpm run generate:component --args ``` +Primitive scaffolding is not handled by the component generator. + The component creation rules live in `packages/turbo-generators/src/component-creation-plan.ts`. Turbo and Plop are adapters that apply the plan. Keep new creation rules in the plan module so dry-run tests can prove which files, exports, stories, docs, and checks a component needs. diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index c8e9d99f..16f1d80a 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -62,7 +62,8 @@ documented resolved props to that element. Keep `render` and `elementType` mutua Do not add generic polymorphic props, `as`, or `asChild` without a demonstrated need. Apply this rule to public component APIs, not internal prop handling. -See [COMPONENTS.md](COMPONENTS.md) for component tiers, package paths, and generator rules. +See [COMPONENTS.md](COMPONENTS.md) for component and primitive structure, package paths, and +generator rules. ## Styling diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md index 8daec7d2..e5620f9a 100644 --- a/docs/DOCUMENTATION.md +++ b/docs/DOCUMENTATION.md @@ -49,7 +49,7 @@ one, cut it. Keep the detail that changes the reader's code: - `Button` sizes a nested `Icon`, so an icon needs no `size` prop. -- A composed field takes no plain `ref`, so `inputRef` is the only way to reach the control. +- A field component takes no plain `ref`, so `inputRef` is the only way to reach the control. - `solid.pressed` reuses the `solid.hover` colour, so a custom pressed state needs depth, finish, or a transform. @@ -57,7 +57,7 @@ Cut the detail that only explains the mechanism: - `Icon` renders an `` that references a symbol in the generated spritesheet. - A token path cannot be both a string leaf and the parent of `hover` and `pressed`. -- Which internal components a composed component renders. +- Which internal modules a component imports. ### Other technologies @@ -80,9 +80,8 @@ Do not write "you can do this, but it is not recommended". Either recommend it o ### Internal distinctions Expose an internal architectural distinction only when it reaches the public API or a developer's -choice. The atom versus composed split does not: it is maintainer architecture, so do not teach it -in public docs. Primitives do, because they use `/primitive` export paths and target library -authors. See [COMPONENTS.md](COMPONENTS.md). +choice. Primitives do, because they use `@luke-ui/react/primitives/*` entrypoints when the normal +component API does not fit. See [COMPONENTS.md](COMPONENTS.md). Do not document an export that is not public API. @@ -157,17 +156,16 @@ public documentation rules above apply to it. JSDoc and TypeScript types drive t When adding or changing a component: -- Write function-level JSDoc on the exported component that describes it for an app developer. -- Put an `@tier` JSDoc tag on the exported `Props` type: `atom`, `composed`, or `primitive`. +- Write function-level JSDoc on the exported component that describes it for a developer. - Document every public prop. Include `@default` when the component destructures a default value. - Keep a straightforward prop description to one concise sentence. Add explanation when a constraint, choice, caveat, or non-obvious behaviour affects how the prop is used. Do not optimise JSDoc for line count. - Do not restate the prop name. `endIcon` needs "Icon shown after the label", not "The end icon". -- On atom and composed components, redeclare important inherited `react-aria-components` props with - useful JSDoc, using the passthrough pattern such as `isDisabled?: RacButtonProps['isDisabled']`. - Redeclare only the props an app developer is likely to reach for. Point a long-tail inherited prop - at the upstream React Aria component through the page's `reactAria` frontmatter link. +- On components, redeclare important inherited `react-aria-components` props with useful JSDoc, + using the passthrough pattern such as `isDisabled?: RacButtonProps['isDisabled']`. Redeclare only + the props a developer is likely to reach for. Point a long-tail inherited prop at the upstream + React Aria component through the page's `reactAria` frontmatter link. A code comment explains the code, not its history. Luke UI is pre-1.0, so no comment carries a prior state. @@ -286,9 +284,9 @@ a page feel shorter. ## Where documentation lives -The hosted docs app in `apps/docs` is the primary docs surface for app developers and library -authors. Authored guides live under `apps/docs/content/docs/docs/`, at `/docs/`. Component -guides live under `apps/docs/content/docs/components/`, at `/components//`. +The hosted docs app in `apps/docs` is the primary docs surface for developers. Authored guides live +under `apps/docs/content/docs/docs/`, at `/docs/`. Component guides live under +`apps/docs/content/docs/components/`, at `/components//`. Do not add generated package docs or `*.docs.md` files under `packages/@luke-ui/react/src/`. @@ -301,7 +299,7 @@ The package README links to the hosted docs. Fumadocs provides: ## Component docs -Atoms and composed components get hosted docs pages in the primary component navigation. +Components get hosted docs pages in the primary component navigation. The components landing page at `/components` lists every component guide, grouped by category. The list is generated from the guides themselves. `scripts/generate-components-index.ts` reads each @@ -310,12 +308,11 @@ that file through `src/components/components-index.tsx`. A new component appears with no further edit. `components-index-generator.test.ts` fails when the generated file drifts from the guides on disk. -Primitives are public API for library authors. Document every primitive export path in hosted docs. -Primitive pages live in the "Primitives" section under components. +Primitives are public API. Document every primitive export path in hosted docs. Primitive pages live +in the "Primitives" section under components. -Component pages may link to their related primitive pages, but should not carry the full primitive -API reference. Keep primitive pages separate from the primary app-developer component path unless -they become app-developer-facing. +Component pages may link to their related primitive pages when the normal component API does not +fit. Keep primitive pages at the end of the Components area. ## MDX page structure diff --git a/docs/STYLING.md b/docs/STYLING.md index 567789b8..063accaa 100644 --- a/docs/STYLING.md +++ b/docs/STYLING.md @@ -10,35 +10,42 @@ with no class and no JS required. Neither step injects styles at runtime. ## Structure +- `styles/index.css.ts`: stylesheet graph in cascade order — layers, reset, theme root, style + modules, utilities. - `styles/reset.css.ts`: reset scoped to `.luke-ui-reset`. - `styles/theme-root.css.ts`: base typography and text colour scoped to `.luke-ui-theme`. -- `recipes/`: component recipes exported from `@luke-ui/react/recipes`. -- `recipes/recipe.ts`: the internal `recipe()` engine shared by every component recipe, plus the +- `styles/modules.css.ts`: the committed stylesheet registry. It explicitly imports every colocated + `recipe.css.ts` and `styles.css.ts` that participates in the shipped stylesheet, plus primitive + and overlay style modules. Keep the list in code-point order by path for deterministic output. + Named layers make cross-layer priority explicit. Specificity and source order still matter within + a layer. +- `styles/recipe.ts`: the internal `recipe()` engine shared by every component recipe, plus the `RecipeSelection` helper that derives a recipe's variant type. -- `recipes/input-states.ts`: the shared field control-state selectors (`inputStates`, +- `styles/input-states.ts`: the shared field control-state selectors (`inputStates`, `composeInputStateSelectors`, `descendantDisabledSelector`) field recipes compose. It is named `.ts`, not `.css.ts`, because it emits no CSS. Each field recipe's `.css.ts` module composes its plain data and functions. -- `recipes/invalid-indicator.ts`: the shared invalid-state `exclamationTriangle` icon, rendered as a +- `styles/invalid-indicator.ts`: the shared invalid-state `exclamationTriangle` icon, rendered as a CSS mask in two sizes. `invalidIndicatorIcon` (plus `invalidIndicatorIconForcedColors`) is the - in-control icon `combobox.css.ts` applies under its own invalid selector's `::after` — the border - stays at its resting 1px there, since the icon is already the non-colour cue. It renders as the - pseudo-element's own last DOM child, so the recipe gives its trailing affordances (the combobox - clear button and trigger) a flex `order` ahead of the icon's default `order: 0`, so it lands right - after the field's text content and before them, matching the Spectrum reference this ordering is - drawn from. `invalidMessageIcon` is the smaller, message-leading variant `field.css.ts` draws on - its `message` slot, switched on by `checkbox.css.ts` alone: `Checkbox`'s own box has no room for - an in-control icon without floating past the label, so its icon moves to the message and its box - keeps a `2px` border as its own non-colour cue instead. Named `.ts` for the same reason as - `input-states.ts`: it emits no CSS of its own, only plain style-rule data each recipe composes. -- `input-group.css.ts` draws the same glyph, but as a real `Icon` element on its own - `invalidIndicator` slot rather than a mask: `InputGroup` (`text-field/primitive/`) reads React - Aria's `Group` `isInvalid` render prop and renders the icon itself, so an invalid control cannot - be composed without a non-colour cue. The recipe owns only the icon's colour and margins — `Icon` - owns its box, and `IconSizeProvider` (`INPUT_GROUP_ICON_SIZE`) owns its per-size step — and gives - the `suffix` slot the same `order: 1` for the same Spectrum ordering. Combobox's control is not a - plain `Group` with that state to hand, so it stays CSS-driven. -- `recipes/mobile-overlay.css.ts`: the scrim, tray, and dialog styles `MobileOverlay` renders for + in-control icon `primitives/combobox/styles.css.ts` applies under its own invalid selector's + `::after` — the border stays at its resting 1px there, since the icon is already the non-colour + cue. It renders as the pseudo-element's own last DOM child, so the style gives its trailing + affordances (the combobox clear button and trigger) a flex `order` ahead of the icon's default + `order: 0`, so it lands right after the field's text content and before them, matching the + Spectrum reference this ordering is drawn from. `invalidMessageIcon` is the smaller, + message-leading variant `primitives/field/recipe.css.ts` draws on its `message` slot, switched on + by `primitives/checkbox/recipe.css.ts` alone: `Checkbox`'s own box has no room for an in-control + icon without floating past the label, so its icon moves to the message and its box keeps a `2px` + border as its own non-colour cue instead. Named `.ts` for the same reason as `input-states.ts`: it + emits no CSS of its own, only plain style-rule data each recipe composes. +- `primitives/input-group/recipe.css.ts` draws the same glyph, but as a real `Icon` element on its + own `invalidIndicator` slot rather than a mask: `InputGroup` (`primitives/input-group/`) reads + React Aria's `Group` `isInvalid` render prop and renders the icon itself, so an invalid control + cannot be composed without a non-colour cue. The recipe owns only the icon's colour and margins — + `Icon` owns its box, and `IconSizeProvider` (`INPUT_GROUP_ICON_SIZE`) owns its per-size step — and + gives the `suffix` slot the same `order: 1` for the same Spectrum ordering. Combobox's control is + not a plain `Group` with that state to hand, so it stays CSS-driven. +- `overlays/mobile-overlay.css.ts`: the scrim, tray, and dialog styles `MobileOverlay` renders for the mobile combobox tray, based on Apache-2.0 React Spectrum's `Tray.tsx` and `tray/index.css`. - `overlays/`: the private mobile tray plumbing. `mobile-overlay.tsx` wraps React Aria's `ModalOverlay`, `Modal`, and `Dialog` for the combobox tray. `use-is-mobile-device.ts` reads the @@ -154,8 +161,8 @@ explicit mode. ## Cascade layers -All styles live in CSS cascade layers so override order does not depend on source order or -specificity. +All styles live in named CSS cascade layers. Layer order makes cross-layer priority explicit. +Specificity and source order still decide conflicts within a layer. | Layer | Purpose | | ----------- | --------------------------------------------------- | @@ -166,8 +173,8 @@ specificity. Use `styleInLayer` and `globalStyleInLayer` from `styles/layered-style.css.ts` to place a plain Vanilla Extract style for a recipe with no variants in a named layer (see -`recipes/loading-skeleton.css.ts`). A variant-driven recipe instead calls `recipe()` from -`recipes/recipe.ts`, which wraps every base, variant, and compound-variant style it is given in the +`loading-skeleton/styles.css.ts`). A variant-driven recipe instead calls `recipe()` from +`styles/recipe.ts`, which wraps every base, variant, and compound-variant style it is given in the `recipes` layer. A recipe can still pre-build a static `base` with `styleInLayer('recipes', …)` and hand the resulting class string to `recipe()`, which passes a string value through unchanged rather than wrapping it again. @@ -188,22 +195,25 @@ skeleton. Reduced-motion handling belongs near the animation. 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` for an example. +`loading-skeleton/styles.css.ts` for an example. ## Recipes -Recipes are public and can be imported from `@luke-ui/react/recipes`. - -```ts -import { button, link } from '@luke-ui/react/recipes'; -``` +Public recipes export from the component or primitive entrypoint that owns the styling contract, for +example `buttonRecipe` from `@luke-ui/react/button` or `inputGroupRecipe` from +`@luke-ui/react/primitives/input-group`. Recipes are component-specific. Keep them separate from general layout utilities. -Every recipe is built with the internal `recipe()` engine from `recipes/recipe.ts`. It is not part -of the public package entry. Component authors inside `@luke-ui/react` use it to define a new -recipe. Consumers only ever call the built recipe functions it returns (`button`, `text`, and so -on). `recipe()` wraps every base, variant, and compound-variant style it is given in the `recipes` +Colocate recipe files beside their owner: + +- `recipe.css.ts` — public recipe contract +- `styles.css.ts` — private implementation styling + +Every recipe is built with the internal `recipe()` engine from `styles/recipe.ts`. It is not part of +the public package entry. Component authors inside `@luke-ui/react` use it to define a new recipe. +Consumers call the built recipe functions it returns (`buttonRecipe`, `textRecipe`, and so on). +`recipe()` wraps every base, variant, and compound-variant style it is given in the `recipes` cascade layer itself, so a recipe author does not add layering by hand. ### Single-part recipes @@ -212,7 +222,7 @@ A single-part recipe takes `base`, `variants`, `defaultVariants`, and `compoundV returns a function that takes a variant selection and returns one class string: ```ts -export const button = recipe({ +export const buttonRecipe = recipe({ base, defaultVariants: { appearance: 'solid', size: 'medium', tone: 'neutral' }, variants: { @@ -224,7 +234,7 @@ export const button = recipe({ }); ``` -See `recipes/button.css.ts` for the full recipe this abbreviates. +See `primitives/button/recipe.css.ts` for the full recipe this abbreviates. ### Slotted recipes @@ -233,24 +243,25 @@ value maps to per-slot styles, and the built recipe takes a variant selection an function per slot, each accepting an optional extra class to merge: ```tsx -export const combobox = recipe({ - slots: { inputGroup: '…', root: '…', textInput: '…' /* … */ }, - variants: {/* per-slot styles keyed by variant value */}, +export const fieldRecipe = recipe({ + slots: { label: {}, message: {}, root: {} }, + variants: { tone: { description: { message: {} } } }, } as const satisfies SlottedConfigInput); -const { root, inputGroup } = combobox({ size: 'medium' }); +const { label, message, root } = fieldRecipe({ tone: 'description' });
-
…
+ +

We will send your receipt here.

; ``` -See `recipes/combobox.css.ts` for a complete slotted recipe. Apply +See `primitives/field/recipe.css.ts` for a complete public slotted recipe. Apply `as const satisfies SlottedConfigInput` at the definition site: `as const` preserves the literal slot names and variant values `recipe()` infers, and `satisfies` type-checks every slot and variant style against `StyleRule` where it is written. -Compound variants are single-part only: `button` and `text` both use `compoundVariants` on their -single-part config. A slotted config has no `compoundVariants` field. +Compound variants are single-part only: `buttonRecipe` and `textRecipe` both use `compoundVariants` +on their single-part config. A slotted config has no `compoundVariants` field. ### Deriving variant types @@ -258,7 +269,7 @@ Never hand-maintain a recipe's variant type. Derive it from the built recipe wit `RecipeSelection`: ```ts -export type ButtonVariants = RecipeSelection; +export type ButtonRecipeVariants = RecipeSelection; ``` Do not cast a hand-written variant interface onto a recipe's selection parameter. If the exported @@ -267,9 +278,9 @@ with the recipe. ### Shared input-state selectors -Field-style recipes (`input-group.css.ts`, `combobox.css.ts`) share one definition of what -"hovered", "focused", "disabled", "invalid", and "read-only" mean for a control, from -`recipes/input-states.ts`: +`InputGroup` and Combobox styling (`primitives/input-group/recipe.css.ts`, +`primitives/combobox/styles.css.ts`) share one definition of what "hovered", "focused", "disabled", +"invalid", and "read-only" mean for a control, from `styles/input-states.ts`: ```ts import { diff --git a/docs/TESTING.md b/docs/TESTING.md index e93effdc..f61ed9ed 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -18,7 +18,7 @@ Use the smallest surface that can falsify the intention. Do not add a new test flavour for a component. Recipe tests are infrastructure tests, not a component-testing category. If a recipe implementation needs a browser to test its own machinery, -keep that test under `src/recipes/` and exclude it from these component rules. +keep that test under `src/styles/` and exclude it from these component rules. ## Component tests diff --git a/packages/@luke-ui/react/.storybook/main.ts b/packages/@luke-ui/react/.storybook/main.ts index aede408a..2030372d 100644 --- a/packages/@luke-ui/react/.storybook/main.ts +++ b/packages/@luke-ui/react/.storybook/main.ts @@ -34,6 +34,10 @@ export default defineMain({ config.resolve ??= {}; const existingAliases = Array.isArray(config.resolve.alias) ? config.resolve.alias : []; config.resolve.alias = [ + { + find: '#recipe-engine', + replacement: resolve(srcDir, 'styles/recipe-engine.ts'), + }, { find: /^@luke-ui\/react\/spritesheet\.svg(\?.*)?$/, replacement: `${resolve(distDir, 'spritesheet.svg')}$1`, diff --git a/packages/@luke-ui/react/AGENTS.md b/packages/@luke-ui/react/AGENTS.md index ec2823c5..a51d633b 100644 --- a/packages/@luke-ui/react/AGENTS.md +++ b/packages/@luke-ui/react/AGENTS.md @@ -1,9 +1,13 @@ # @luke-ui/react agent guide -- Do not hand-edit `.generated/entries.ts` or `package.json#exports`. Entries are generated, and - `tsdown` updates exports during build. +- Do not hand-edit `.generated/entries.ts` or `package.json#exports`. `vp pack` generates entries + and updates exports during build. The `stylesheet` build entry is excluded from the public export + map via `exports.exclude` in `vite.config.ts`. Vanilla Extract serializes recipes to + `#recipe-engine`; pack, Vitest, Storybook, and the docs app alias that specifier to source. Pack + then bundles a relative runtime chunk. The specifier is not a public package subpath. - 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. + files by hand. The generator updates the style-module registry, conformance manifest, and docs + wiring. - Read [`docs/TESTING.md`](../../docs/TESTING.md) before adding or changing component tests. It is the only normative testing guide. Component tests use the shared browser renderer; stories are documentation and render/a11y fixtures, not assertion files. @@ -18,7 +22,11 @@ A component directory contains: - `[component].browser.test.tsx`: component behaviour, conformance, and the integration tripwire - `[component].visual.test.tsx`: visual regression captures when the component has a visual surface - `index.tsx`: component implementation -- `primitive/`: optional primitive exports +- `recipe.css.ts`: public recipe contract (scaffolded by the generator) +- `styles.css.ts`: private implementation styling when needed + +Lower-level composition APIs live under `src/primitives/*` and export from +`@luke-ui/react/primitives/*`. See [`docs/COMPONENTS.md`](../../docs/COMPONENTS.md). ## Exported prop types @@ -48,17 +56,8 @@ Rules: - Name the internal interface `_ComponentProps` (underscore prefix) and export the Prettified version as `ComponentProps`. -## Component taxonomy - -Components follow the Atom, Composed, and Primitive taxonomy. See -[`docs/COMPONENTS.md`](../../docs/COMPONENTS.md) for definitions. - -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 need hosted docs. See -[`docs/DOCUMENTATION.md`](../../docs/DOCUMENTATION.md). - ## Documentation JSDoc and TypeScript types drive the docs app. The normative documentation rules — what to document, -JSDoc, `@tier`, inherited React Aria props, examples, and writing style — live in +JSDoc, inherited React Aria props, examples, and writing style — live in [`docs/DOCUMENTATION.md`](../../docs/DOCUMENTATION.md). Do not restate them here. diff --git a/packages/@luke-ui/react/README.md b/packages/@luke-ui/react/README.md index d1b47529..3da8da02 100644 --- a/packages/@luke-ui/react/README.md +++ b/packages/@luke-ui/react/README.md @@ -5,9 +5,11 @@ Luke UI is a React design system built on `react-aria-components` and `vanilla-e ## Install ```sh -pnpm add @luke-ui/react +pnpm add @luke-ui/react react-aria-components ``` +Luke UI expects the application to provide a compatible shared `react-aria-components` instance. + ## Setup Import the component stylesheet and one bundled theme stylesheet. Importing a theme stylesheet @@ -42,15 +44,8 @@ AI agents can fetch documentation at: - [llms-full.txt](https://lukebennett88.github.io/luke-ui/llms-full.txt): full docs. - Any docs URL with `.md` appended: per-page Markdown. -Components follow three tiers: - -- Atoms: single units such as `Text`, `Icon`, and `Heading`. -- Composed components: opinionated combinations such as `Button` and `TextField`. -- Primitives: lower-level public APIs for library authors, such as `button/primitive` and - `field/primitive`. - -Atoms and composed components are app-developer-facing. Primitives are documented in hosted docs for -library authors, separate from the primary component path. +Start with the normal component API. Use primitives from `@luke-ui/react/primitives/*` when you need +a custom composition the component API does not cover. ## License diff --git a/packages/@luke-ui/react/package.json b/packages/@luke-ui/react/package.json index 63678cd8..6178b1b3 100644 --- a/packages/@luke-ui/react/package.json +++ b/packages/@luke-ui/react/package.json @@ -22,32 +22,29 @@ "./blockquote": "./dist/blockquote/index.js", "./box": "./dist/box/index.js", "./button": "./dist/button/index.js", - "./button/primitive": "./dist/button/primitive/index.js", "./checkbox": "./dist/checkbox/index.js", - "./checkbox/primitive": "./dist/checkbox/primitive/index.js", "./code": "./dist/code/index.js", "./combobox-field": "./dist/combobox-field/index.js", - "./combobox-field/primitive": "./dist/combobox-field/primitive/index.js", "./em": "./dist/em/index.js", "./emoji": "./dist/emoji/index.js", - "./field/primitive": "./dist/field/primitive/index.js", "./heading": "./dist/heading/index.js", - "./heading-context": "./dist/heading-context/index.js", "./icon": "./dist/icon/index.js", "./icon-button": "./dist/icon-button/index.js", - "./icon-size-context": "./dist/icon-size-context/index.js", "./kbd": "./dist/kbd/index.js", "./link": "./dist/link/index.js", "./loading-skeleton": "./dist/loading-skeleton/index.js", "./loading-spinner": "./dist/loading-spinner/index.js", "./numeral": "./dist/numeral/index.js", + "./primitives/button": "./dist/primitives/button/index.js", + "./primitives/checkbox": "./dist/primitives/checkbox/index.js", + "./primitives/combobox": "./dist/primitives/combobox/index.js", + "./primitives/field": "./dist/primitives/field/index.js", + "./primitives/input-group": "./dist/primitives/input-group/index.js", "./quote": "./dist/quote/index.js", - "./recipes": "./dist/recipes/index.js", "./strong": "./dist/strong/index.js", "./styles": "./dist/styles/index.js", "./text": "./dist/text/index.js", "./text-field": "./dist/text-field/index.js", - "./text-field/primitive": "./dist/text-field/primitive/index.js", "./theme": "./dist/theme/index.js", "./themes/paper": "./dist/themes/paper/index.js", "./themes/tactile": "./dist/themes/tactile/index.js", @@ -66,7 +63,7 @@ "build": "pnpm run build:tsdown", "build:storybook": "storybook build", "build:tsdown": "vp pack", - "check:barrels": "unbarrelify --check --skip src/styles/index.ts --skip src/combobox-field/primitive/index.tsx", + "check:barrels": "unbarrelify --check --skip src/styles/index.ts --skip src/primitives/combobox/index.tsx", "check:format": "vp fmt . --check", "check:lint": "vp lint . --type-aware", "check:story-plays": "tsx scripts/check-story-plays.ts", @@ -103,8 +100,7 @@ "@luke-ui/rainbow-sprinkles": "workspace:*", "@react-aria/utils": "catalog:", "@vanilla-extract/css": "catalog:", - "@vanilla-extract/recipes": "catalog:", - "react-aria-components": "catalog:" + "@vanilla-extract/recipes": "catalog:" }, "devDependencies": { "@arethetypeswrong/core": "catalog:", @@ -133,6 +129,7 @@ "postcss-selector-parser": "catalog:", "publint": "catalog:", "react": "catalog:", + "react-aria-components": "catalog:", "react-dom": "catalog:", "storybook": "catalog:", "tsx": "catalog:", @@ -144,6 +141,7 @@ }, "peerDependencies": { "react": "catalog:", + "react-aria-components": "catalog:", "react-dom": "catalog:" } } diff --git a/packages/@luke-ui/react/src/blockquote/index.tsx b/packages/@luke-ui/react/src/blockquote/index.tsx index a3447273..c5ba78a6 100644 --- a/packages/@luke-ui/react/src/blockquote/index.tsx +++ b/packages/@luke-ui/react/src/blockquote/index.tsx @@ -1,19 +1,17 @@ -import * as styles from '../recipes/blockquote.css.js'; import type { TextProps } from '../text/index.js'; import { Text } from '../text/index.js'; import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { Prettify } from '../types/prettify.js'; import { cx } from '../utils/index.js'; +import { blockquoteRecipe } from './recipe.css.js'; + +export { type BlockquoteRecipeVariants, blockquoteRecipe } from './recipe.css.js'; type _BlockquoteOmit = DistributiveOmit; interface _BlockquoteProps extends _BlockquoteOmit {} -/** - * Props for the `Blockquote` component. - * - * @tier atom - */ +/** Props for the `Blockquote` component. */ export type BlockquoteProps = Prettify<_BlockquoteProps>; /** @@ -23,7 +21,7 @@ export type BlockquoteProps = Prettify<_BlockquoteProps>; export function Blockquote(props: BlockquoteProps) { const { children, className, ...textProps } = props; return ( - + {children} ); diff --git a/packages/@luke-ui/react/src/recipes/blockquote.css.ts b/packages/@luke-ui/react/src/blockquote/recipe.css.ts similarity index 60% rename from packages/@luke-ui/react/src/recipes/blockquote.css.ts rename to packages/@luke-ui/react/src/blockquote/recipe.css.ts index 745a0ccd..88b55fe5 100644 --- a/packages/@luke-ui/react/src/recipes/blockquote.css.ts +++ b/packages/@luke-ui/react/src/blockquote/recipe.css.ts @@ -1,7 +1,7 @@ import { styleInLayer } from '../styles/layered-style.css.js'; +import type { RecipeSelection } from '../styles/recipe.js'; +import { recipe } from '../styles/recipe.js'; import { vars } from '../theme/contract.css.js'; -import type { RecipeSelection } from './recipe.js'; -import { recipe } from './recipe.js'; const base = styleInLayer('recipes', { borderInlineStart: `3px solid ${vars.color.border.decorative}`, @@ -9,8 +9,8 @@ const base = styleInLayer('recipes', { }); /** Vanilla-extract recipe for the `Blockquote` component's left-border accent. */ -export const blockquote = recipe({ +export const blockquoteRecipe = recipe({ base, }); -export type BlockquoteVariants = RecipeSelection; +export type BlockquoteRecipeVariants = RecipeSelection; diff --git a/packages/@luke-ui/react/src/box/index.tsx b/packages/@luke-ui/react/src/box/index.tsx index 7314bcec..2d9ee9b1 100644 --- a/packages/@luke-ui/react/src/box/index.tsx +++ b/packages/@luke-ui/react/src/box/index.tsx @@ -5,13 +5,7 @@ import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { Prettify } from '../types/prettify.js'; import { mergeProps } from '../utils/index.js'; -/** - * Props for `Box`. Layout props accept responsive values keyed by Luke UI breakpoints. - * - * `elementType` and `render` are mutually exclusive ways to choose the rendered element. - * - * @tier atom - */ +/** Props for `Box`. */ export type BoxProps = Prettify<_BoxElementProps | _BoxRenderProps>; /** Applies layout properties to a supported structural element or an element returned by `render`. */ @@ -68,6 +62,7 @@ interface _BoxElementProps extends HTMLAttributes, SprinklesProps { elementType?: BoxElementType; /** Ref to the rendered element. */ ref?: Ref; + /** Use `render` instead of `elementType` to own the rendered element. */ render?: never; } @@ -85,6 +80,7 @@ interface _BoxResolvedRenderProps extends _BoxResolvedRenderOmit { type BoxResolvedRenderProps = Prettify<_BoxResolvedRenderProps>; interface _BoxRenderProps extends _BoxPresentationProps { + /** Use `elementType` instead of `render` for a supported structural element. */ elementType?: never; /** Passes Box's content and presentation props to a caller-owned element. */ render: (props: BoxResolvedRenderProps) => ReactElement; diff --git a/packages/@luke-ui/react/src/button/index.tsx b/packages/@luke-ui/react/src/button/index.tsx index 01ea160d..fe5bae8c 100644 --- a/packages/@luke-ui/react/src/button/index.tsx +++ b/packages/@luke-ui/react/src/button/index.tsx @@ -1,17 +1,20 @@ +export { type ButtonRecipeVariants, buttonRecipe } from '../primitives/button/recipe.css.js'; + import type { JSX, ReactNode } from 'react'; import { LoadingSpinner } from '../loading-spinner/index.js'; -import * as styles from '../recipes/button-composed.css.js'; -import type * as primitiveStyles from '../recipes/button.css.js'; +import type { ButtonProps as PrimitiveButtonProps } from '../primitives/button/index.js'; +import { Button as PrimitiveButton } from '../primitives/button/index.js'; +import type * as primitiveStyles from '../primitives/button/recipe.css.js'; import { Text } from '../text/index.js'; import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { DocumentedPressProps } from '../types/documented-rac-props.js'; import type { Prettify } from '../types/prettify.js'; -import type { ButtonProps as PrimitiveButtonProps } from './primitive/index.js'; -import { Button as PrimitiveButton } from './primitive/index.js'; +import type { ButtonLabelVariants } from './styles.css.js'; +import { buttonContent, buttonLabel, spinnerOverlay } from './styles.css.js'; -interface ComposedButtonRecipeProps extends NonNullable {} +interface ButtonLabelRecipeProps extends NonNullable {} -interface PrimitiveButtonRecipeProps extends NonNullable {} +interface PrimitiveButtonRecipeProps extends NonNullable {} interface ButtonStyleProps { /** @@ -32,7 +35,7 @@ interface ButtonStyleProps { * Shows pending button styles. When true, a spinner overlays the label. * @default false */ - isPending?: ComposedButtonRecipeProps['isPending']; + isPending?: ButtonLabelRecipeProps['isPending']; /** * Sets the button size. * @default 'medium' @@ -56,27 +59,26 @@ type _ButtonOmit = DistributiveOmit< interface _ButtonProps extends _ButtonOmit, ButtonStyleProps, DocumentedPressProps {} -/** - * Composed button with size, tone, appearance, pending, and block options. - * - * @tier composed - */ +/** Props for `Button`. */ export type ButtonProps = Prettify<_ButtonProps>; -/** Composed button. Wraps children in a `Text` for ellipsis truncation. Shows a spinner when `isPending`. */ +/** + * Button with size, tone, appearance, pending, and block options. + * Wraps children in a `Text` for ellipsis truncation. Shows a spinner when `isPending`. + */ export function Button(props: ButtonProps): JSX.Element { const { children, endIcon, isPending, size = 'medium', startIcon, ...restProps } = props; return ( {(renderProps) => ( - + {isPending && ( - + )} - + {startIcon} {typeof children === 'function' ? children(renderProps) : children} diff --git a/packages/@luke-ui/react/src/recipes/button-composed.css.ts b/packages/@luke-ui/react/src/button/styles.css.ts similarity index 80% rename from packages/@luke-ui/react/src/recipes/button-composed.css.ts rename to packages/@luke-ui/react/src/button/styles.css.ts index fdfd339d..a09ba94c 100644 --- a/packages/@luke-ui/react/src/recipes/button-composed.css.ts +++ b/packages/@luke-ui/react/src/button/styles.css.ts @@ -1,7 +1,7 @@ +import type { RecipeSelection } from '../styles/recipe.js'; +import { recipe } from '../styles/recipe.js'; +import { spinnerOverlayBase } from '../styles/spinner-overlay.js'; import { vars } from '../theme/contract.css.js'; -import type { RecipeSelection } from './recipe.js'; -import { recipe } from './recipe.js'; -import { spinnerOverlayBase } from './spinner-overlay.js'; export const buttonContent = recipe({ base: { diff --git a/packages/@luke-ui/react/src/checkbox/index.tsx b/packages/@luke-ui/react/src/checkbox/index.tsx index 92aca147..33b54358 100644 --- a/packages/@luke-ui/react/src/checkbox/index.tsx +++ b/packages/@luke-ui/react/src/checkbox/index.tsx @@ -1,17 +1,18 @@ +export { checkboxRecipe, type CheckboxRecipeVariants } from '../primitives/checkbox/recipe.css.js'; import { useObjectRef } from '@react-aria/utils'; import type { JSX, ReactNode, Ref } from 'react'; import type { CheckboxFieldProps as RacCheckboxFieldProps } from 'react-aria-components/Checkbox'; -import type { FieldErrorProps } from '../field/primitive/index.js'; -import { FieldDescription, FieldError } from '../field/primitive/index.js'; -import type { DistributiveOmit } from '../types/distributive-omit.js'; -import type { Prettify } from '../types/prettify.js'; -import type { CheckboxProps as PrimitiveCheckboxProps } from './primitive/index.js'; +import type { CheckboxProps as PrimitiveCheckboxProps } from '../primitives/checkbox/index.js'; import { CheckboxContent, CheckboxControl, CheckboxIndicator, Checkbox as PrimitiveCheckbox, -} from './primitive/index.js'; +} from '../primitives/checkbox/index.js'; +import type { FieldErrorProps } from '../primitives/field/index.js'; +import { FieldDescription, FieldError } from '../primitives/field/index.js'; +import type { DistributiveOmit } from '../types/distributive-omit.js'; +import type { Prettify } from '../types/prettify.js'; type _CheckboxOmit = DistributiveOmit; @@ -27,7 +28,7 @@ interface _CheckboxProps extends _CheckboxOmit { /** * Forwarded to the underlying `` element. * - * Composed fields take no plain `ref`: `inputRef` is the only way to reach the + * This field takes no plain `ref`: `inputRef` is the only way to reach the * control, so a ref can never silently resolve to a wrapper element instead. * * Widened from React Aria's own `inputRef`, which only takes a ref object, so a @@ -56,11 +57,7 @@ interface _CheckboxProps extends _CheckboxOmit { size?: PrimitiveCheckboxProps['size']; } -/** - * Props for the composed Checkbox. - * - * @tier composed - */ +/** Props for `Checkbox`. */ export type CheckboxProps = Prettify<_CheckboxProps>; /** A labelled checkbox with optional description and validation message. */ diff --git a/packages/@luke-ui/react/src/code/index.tsx b/packages/@luke-ui/react/src/code/index.tsx index 7751befd..534bf976 100644 --- a/packages/@luke-ui/react/src/code/index.tsx +++ b/packages/@luke-ui/react/src/code/index.tsx @@ -1,17 +1,14 @@ -import * as styles from '../recipes/code.css.js'; +export { codeRecipe, type CodeRecipeVariants } from './recipe.css.js'; import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { Prettify } from '../types/prettify.js'; import { cx } from '../utils/index.js'; +import { codeRecipe } from './recipe.css.js'; type _CodeOmit = DistributiveOmit, never>; interface _CodeProps extends _CodeOmit {} -/** - * Props for the `Code` component. - * - * @tier atom - */ +/** Props for the `Code` component. */ export type CodeProps = Prettify<_CodeProps>; /** @@ -19,5 +16,5 @@ export type CodeProps = Prettify<_CodeProps>; */ export function Code(props: CodeProps) { const { className, ...elementProps } = props; - return ; + return ; } diff --git a/packages/@luke-ui/react/src/recipes/code.css.ts b/packages/@luke-ui/react/src/code/recipe.css.ts similarity index 70% rename from packages/@luke-ui/react/src/recipes/code.css.ts rename to packages/@luke-ui/react/src/code/recipe.css.ts index a4f11106..93868be4 100644 --- a/packages/@luke-ui/react/src/recipes/code.css.ts +++ b/packages/@luke-ui/react/src/code/recipe.css.ts @@ -1,7 +1,7 @@ import { styleInLayer } from '../styles/layered-style.css.js'; +import type { RecipeSelection } from '../styles/recipe.js'; +import { recipe } from '../styles/recipe.js'; import { vars } from '../theme/contract.css.js'; -import type { RecipeSelection } from './recipe.js'; -import { recipe } from './recipe.js'; const base = styleInLayer('recipes', { backgroundColor: vars.color.surface.recessed, @@ -16,8 +16,8 @@ const base = styleInLayer('recipes', { }); /** Vanilla-extract recipe for the `Code` component's inline code appearance. */ -export const code = recipe({ +export const codeRecipe = recipe({ base, }); -export type CodeVariants = RecipeSelection; +export type CodeRecipeVariants = RecipeSelection; diff --git a/packages/@luke-ui/react/src/combobox-field/combobox-field.browser.test.tsx b/packages/@luke-ui/react/src/combobox-field/combobox-field.browser.test.tsx index 68ce4633..ddb13406 100644 --- a/packages/@luke-ui/react/src/combobox-field/combobox-field.browser.test.tsx +++ b/packages/@luke-ui/react/src/combobox-field/combobox-field.browser.test.tsx @@ -2,15 +2,15 @@ import { createRef } from 'react'; import { expect, test } from 'vite-plus/test'; import { page, userEvent } from 'vite-plus/test/context'; import { testFieldShapedConformance, testIntegration } from '../conformance/helpers.js'; +import { ComboboxInputGroup } from '../primitives/combobox/input-group.js'; +import { ComboboxInput } from '../primitives/combobox/input.js'; +import { ComboboxItem } from '../primitives/combobox/item.js'; +import { ComboboxRoot } from '../primitives/combobox/root.js'; import { mockScreenWidth } from '../test-utils/mock-screen-width.js'; import { render } from '../test-utils/render.js'; import { waitForOverlayEnter } from '../test-utils/wait-for-overlay-enter.js'; import { componentTestRegistration } from './component-test-registration.js'; import { ComboboxField } from './index.js'; -import { ComboboxInputGroup } from './primitive/input-group.js'; -import { ComboboxInput } from './primitive/input.js'; -import { ComboboxItem } from './primitive/item.js'; -import { ComboboxRoot } from './primitive/root.js'; type CountryItem = { id: string; diff --git a/packages/@luke-ui/react/src/combobox-field/combobox-field.stories.tsx b/packages/@luke-ui/react/src/combobox-field/combobox-field.stories.tsx index d7b172f4..9fd0350a 100644 --- a/packages/@luke-ui/react/src/combobox-field/combobox-field.stories.tsx +++ b/packages/@luke-ui/react/src/combobox-field/combobox-field.stories.tsx @@ -1,6 +1,6 @@ import { Button } from '@luke-ui/react/button'; import { ComboboxField } from '@luke-ui/react/combobox-field'; -import { ComboboxItem, ComboboxSection } from '@luke-ui/react/combobox-field/primitive'; +import { ComboboxItem, ComboboxSection } from '@luke-ui/react/primitives/combobox'; import type { CSSProperties } from 'react'; import { useState } from 'react'; import type { Key } from 'react-aria-components/Breadcrumbs'; diff --git a/packages/@luke-ui/react/src/combobox-field/combobox-field.visual.test.tsx b/packages/@luke-ui/react/src/combobox-field/combobox-field.visual.test.tsx index ce054359..266b2215 100644 --- a/packages/@luke-ui/react/src/combobox-field/combobox-field.visual.test.tsx +++ b/packages/@luke-ui/react/src/combobox-field/combobox-field.visual.test.tsx @@ -2,6 +2,8 @@ import { expect, test } from 'vite-plus/test'; import { page, userEvent } from 'vite-plus/test/context'; import { Icon } from '../icon/index.js'; import { LoadingSpinner } from '../loading-spinner/index.js'; +import { ComboboxItem, ComboboxLoadMoreItem } from '../primitives/combobox/item.js'; +import { ComboboxSection } from '../primitives/combobox/section.js'; import { mockScreenWidth } from '../test-utils/mock-screen-width.js'; import { render, visualAppearances } from '../test-utils/render.js'; import { @@ -13,8 +15,6 @@ import { } from '../test-utils/visual.js'; import { waitForOverlayEnter } from '../test-utils/wait-for-overlay-enter.js'; import { ComboboxField } from './index.js'; -import { ComboboxItem, ComboboxLoadMoreItem } from './primitive/item.js'; -import { ComboboxSection } from './primitive/section.js'; type CountryItem = { id: string; diff --git a/packages/@luke-ui/react/src/combobox-field/index.tsx b/packages/@luke-ui/react/src/combobox-field/index.tsx index 552689b7..2616a325 100644 --- a/packages/@luke-ui/react/src/combobox-field/index.tsx +++ b/packages/@luke-ui/react/src/combobox-field/index.tsx @@ -11,30 +11,30 @@ import { composeRenderProps } from 'react-aria-components/composeRenderProps'; import { useSlottedContext } from 'react-aria-components/slots'; import type { FieldSlotProps } from '../field/compose-field.js'; import { composeField } from '../field/compose-field.js'; -import { Field } from '../field/primitive/index.js'; -import { IconSizeProvider } from '../icon-size-context/index.js'; +import { IconSizeProvider } from '../icon/icon-size-context.js'; import { Icon } from '../icon/index.js'; import { LoadingSpinner } from '../loading-spinner/index.js'; import { MobileOverlay } from '../overlays/mobile-overlay.js'; import { useIsMobileDevice } from '../overlays/use-is-mobile-device.js'; -import * as styles from '../recipes/combobox.css.js'; +import { ComboboxClearButton } from '../primitives/combobox/clear-button.js'; +import { ComboboxEmptyState } from '../primitives/combobox/empty-state.js'; +import { ComboboxInputGroup } from '../primitives/combobox/input-group.js'; +import { ComboboxInput } from '../primitives/combobox/input.js'; +import type { ComboboxLoadMoreItemProps } from '../primitives/combobox/item.js'; +import { ComboboxLoadMoreItem } from '../primitives/combobox/item.js'; +import type { ComboboxListBoxProps } from '../primitives/combobox/listbox.js'; +import { ComboboxListBox } from '../primitives/combobox/listbox.js'; +import type { ComboboxPopoverProps } from '../primitives/combobox/popover.js'; +import { ComboboxPopover } from '../primitives/combobox/popover.js'; +import type { ComboboxRootProps, ComboboxSize } from '../primitives/combobox/root.js'; +import { ComboboxRoot } from '../primitives/combobox/root.js'; +import { comboboxRecipe } from '../primitives/combobox/styles.css.js'; +import { ComboboxTrigger } from '../primitives/combobox/trigger.js'; +import { Field } from '../primitives/field/index.js'; import { COMBOBOX_ICON_SIZE } from '../sizing/combobox-sizing.js'; import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { Prettify } from '../types/prettify.js'; import { cx } from '../utils/index.js'; -import { ComboboxClearButton } from './primitive/clear-button.js'; -import { ComboboxEmptyState } from './primitive/empty-state.js'; -import { ComboboxInputGroup } from './primitive/input-group.js'; -import { ComboboxInput } from './primitive/input.js'; -import type { ComboboxLoadMoreItemProps } from './primitive/item.js'; -import { ComboboxLoadMoreItem } from './primitive/item.js'; -import type { ComboboxListBoxProps } from './primitive/listbox.js'; -import { ComboboxListBox } from './primitive/listbox.js'; -import type { ComboboxPopoverProps } from './primitive/popover.js'; -import { ComboboxPopover } from './primitive/popover.js'; -import type { ComboboxRootProps, ComboboxSize } from './primitive/root.js'; -import { ComboboxRoot } from './primitive/root.js'; -import { ComboboxTrigger } from './primitive/trigger.js'; type ComboboxLoadingState = 'error' | 'filtering' | 'idle' | 'loading' | 'loadingMore' | 'sorting'; @@ -88,11 +88,7 @@ interface _ComboboxFieldProps size?: ComboboxSize; } -/** - * Props for composed `ComboboxField` (searchable single-select). - * - * @tier composed - */ +/** Props for `ComboboxField` (searchable single-select). */ export type ComboboxFieldProps = Prettify<_ComboboxFieldProps>; /** Composes `ComboboxRoot` with label, description, and error slots. */ @@ -220,7 +216,7 @@ function MobileComboboxContent({ const valueId = useId(); const ariaLabelledBy = labelContext?.id == null ? undefined : cx(labelContext.id, valueId); - const comboboxStyles = styles.combobox({ size }); + const comboboxStyles = comboboxRecipe({ size }); const mobileListBoxClassName = composeRenderProps(listBoxProps?.className, (className) => { return comboboxStyles.mobileListBox(className); @@ -314,7 +310,7 @@ function MobileComboboxClearButton({ size }: { size: ComboboxSize }): JSX.Elemen { state.setInputValue(''); }} diff --git a/packages/@luke-ui/react/src/conformance/manifest.ts b/packages/@luke-ui/react/src/conformance/manifest.ts index 727e200c..eaa012e5 100644 --- a/packages/@luke-ui/react/src/conformance/manifest.ts +++ b/packages/@luke-ui/react/src/conformance/manifest.ts @@ -1,12 +1,10 @@ type ConformanceTier = 'universal' | 'field-shaped' | 'none'; type IntegrationTripwire = 'required' | 'none'; type VisualApplicability = 'applicable' | 'none'; -type ComponentTier = 'atom' | 'composed' | 'primitive'; export type ComponentTestManifestEntry = { name: string; path: string; - tier: ComponentTier; conformanceTier: ConformanceTier; integrationTripwire: IntegrationTripwire; visualApplicability: VisualApplicability; @@ -14,40 +12,37 @@ export type ComponentTestManifestEntry = { // Keep this list explicit. `none` is a deliberate exception, not an omission. export const componentTestManifest = [ - ['Blockquote', 'blockquote', 'atom', 'none', 'none', 'none'], - ['Box', 'box', 'atom', 'universal', 'none', 'applicable'], - ['Button', 'button', 'composed', 'universal', 'required', 'applicable'], - ['Button primitive', 'button/primitive', 'primitive', 'none', 'none', 'none'], - ['Checkbox', 'checkbox', 'composed', 'field-shaped', 'required', 'applicable'], - ['Checkbox primitive', 'checkbox/primitive', 'primitive', 'none', 'none', 'none'], - ['Code', 'code', 'atom', 'none', 'none', 'none'], - ['ComboboxField', 'combobox-field', 'composed', 'field-shaped', 'required', 'applicable'], - ['ComboboxField primitive', 'combobox-field/primitive', 'primitive', 'none', 'none', 'none'], - ['Em', 'em', 'atom', 'none', 'none', 'none'], - ['Emoji', 'emoji', 'atom', 'none', 'none', 'applicable'], - ['Field primitive', 'field/primitive', 'primitive', 'none', 'none', 'none'], - ['Heading', 'heading', 'atom', 'none', 'none', 'applicable'], - ['Heading context', 'heading-context', 'primitive', 'none', 'none', 'none'], - ['Icon', 'icon', 'atom', 'none', 'none', 'applicable'], - ['IconButton', 'icon-button', 'composed', 'universal', 'required', 'applicable'], - ['Icon size context', 'icon-size-context', 'primitive', 'none', 'none', 'none'], - ['Kbd', 'kbd', 'atom', 'none', 'none', 'none'], - ['Link', 'link', 'atom', 'universal', 'required', 'applicable'], - ['LoadingSkeleton', 'loading-skeleton', 'atom', 'none', 'none', 'applicable'], - ['LoadingSpinner', 'loading-spinner', 'atom', 'none', 'none', 'applicable'], - ['Numeral', 'numeral', 'atom', 'none', 'none', 'applicable'], - ['Quote', 'quote', 'atom', 'none', 'none', 'none'], - ['Strong', 'strong', 'atom', 'none', 'none', 'none'], - ['Text', 'text', 'atom', 'none', 'none', 'applicable'], - ['TextField', 'text-field', 'composed', 'field-shaped', 'required', 'applicable'], - ['TextField primitive', 'text-field/primitive', 'primitive', 'none', 'none', 'none'], - ['Theme', 'theme', 'primitive', 'none', 'none', 'none'], - ['VisuallyHidden', 'visually-hidden', 'atom', 'none', 'none', 'none'], -].map(([name, path, tier, conformanceTier, integrationTripwire, visualApplicability]) => ({ + ['Blockquote', 'blockquote', 'none', 'none', 'none'], + ['Box', 'box', 'universal', 'none', 'applicable'], + ['Button', 'button', 'universal', 'required', 'applicable'], + ['Button primitive', 'primitives/button', 'none', 'none', 'none'], + ['Checkbox', 'checkbox', 'field-shaped', 'required', 'applicable'], + ['Checkbox primitive', 'primitives/checkbox', 'none', 'none', 'none'], + ['Code', 'code', 'none', 'none', 'none'], + ['ComboboxField', 'combobox-field', 'field-shaped', 'required', 'applicable'], + ['Combobox primitive', 'primitives/combobox', 'none', 'none', 'none'], + ['Em', 'em', 'none', 'none', 'none'], + ['Emoji', 'emoji', 'none', 'none', 'applicable'], + ['Field primitive', 'primitives/field', 'none', 'none', 'none'], + ['Heading', 'heading', 'none', 'none', 'applicable'], + ['Icon', 'icon', 'none', 'none', 'applicable'], + ['IconButton', 'icon-button', 'universal', 'required', 'applicable'], + ['Input group primitive', 'primitives/input-group', 'none', 'none', 'none'], + ['Kbd', 'kbd', 'none', 'none', 'none'], + ['Link', 'link', 'universal', 'required', 'applicable'], + ['LoadingSkeleton', 'loading-skeleton', 'none', 'none', 'applicable'], + ['LoadingSpinner', 'loading-spinner', 'none', 'none', 'applicable'], + ['Numeral', 'numeral', 'none', 'none', 'applicable'], + ['Quote', 'quote', 'none', 'none', 'none'], + ['Strong', 'strong', 'none', 'none', 'none'], + ['Text', 'text', 'none', 'none', 'applicable'], + ['TextField', 'text-field', 'field-shaped', 'required', 'applicable'], + ['Theme', 'theme', 'none', 'none', 'none'], + ['VisuallyHidden', 'visually-hidden', 'none', 'none', 'none'], +].map(([name, path, conformanceTier, integrationTripwire, visualApplicability]) => ({ conformanceTier, integrationTripwire, name, path, - tier, visualApplicability, })) as ReadonlyArray; diff --git a/packages/@luke-ui/react/src/em/index.tsx b/packages/@luke-ui/react/src/em/index.tsx index 296d48f9..9638393c 100644 --- a/packages/@luke-ui/react/src/em/index.tsx +++ b/packages/@luke-ui/react/src/em/index.tsx @@ -1,9 +1,9 @@ -import * as styles from '../recipes/em.css.js'; import type { TextProps } from '../text/index.js'; import { Text } from '../text/index.js'; import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { Prettify } from '../types/prettify.js'; import { cx } from '../utils/index.js'; +import { em } from './styles.css.js'; interface EmStyleProps { /** @@ -21,11 +21,7 @@ type _EmOmit = DistributiveOmit, 'color'>; interface _EmProps extends _EmOmit, EmStyleProps {} -/** - * Props for the `Em` component. - * - * @tier atom - */ +/** Props for the `Em` component. */ export type EmProps = Prettify<_EmProps>; /** @@ -37,7 +33,7 @@ export function Em(props: EmProps) { return ( ; /** diff --git a/packages/@luke-ui/react/src/field/compose-field.ts b/packages/@luke-ui/react/src/field/compose-field.ts index 90ef3b3b..9fc08d50 100644 --- a/packages/@luke-ui/react/src/field/compose-field.ts +++ b/packages/@luke-ui/react/src/field/compose-field.ts @@ -1,7 +1,7 @@ import type { ReactNode } from 'react'; +import type { FieldErrorProps } from '../primitives/field/error.js'; +import type { FieldNecessityIndicator } from '../primitives/field/label.js'; import type { DistributiveOmit } from '../types/distributive-omit.js'; -import type { FieldErrorProps } from './primitive/error.js'; -import type { FieldNecessityIndicator } from './primitive/label.js'; export interface FieldSlotProps { /** Optional helper text shown below the control. */ @@ -17,7 +17,7 @@ export interface FieldSlotProps { type KeysOfUnion = T extends T ? keyof T : never; type FieldSlotKeys = Extract>; -/** Splits the `Field` slot props (label/description/errorMessage/necessityIndicator) off a Composed field's props. */ +/** Splits field slot props (label, description, errorMessage, necessityIndicator) off a field component's props. */ export function composeField( props: T, ): [FieldSlotProps, DistributiveOmit>] { diff --git a/packages/@luke-ui/react/src/heading-context/index.tsx b/packages/@luke-ui/react/src/heading/heading-context.tsx similarity index 97% rename from packages/@luke-ui/react/src/heading-context/index.tsx rename to packages/@luke-ui/react/src/heading/heading-context.tsx index 8b0fe221..809685c2 100644 --- a/packages/@luke-ui/react/src/heading-context/index.tsx +++ b/packages/@luke-ui/react/src/heading/heading-context.tsx @@ -34,6 +34,8 @@ export type HeadingLevelsRenderProps = { /** * Get the current heading level from context. * + * Reads the current level without wrapping another `HeadingLevels`, which would advance it. + * * @param fallback - Level to use when no provider is found (defaults to 2) * @returns An object with the current heading level and element from context, or the fallback value */ diff --git a/packages/@luke-ui/react/src/heading/heading.browser.test.tsx b/packages/@luke-ui/react/src/heading/heading.browser.test.tsx index a5f51971..40916314 100644 --- a/packages/@luke-ui/react/src/heading/heading.browser.test.tsx +++ b/packages/@luke-ui/react/src/heading/heading.browser.test.tsx @@ -1,7 +1,7 @@ import { expect, test } from 'vite-plus/test'; import { render } from '../test-utils/render.js'; import { Text } from '../text/index.js'; -import { Heading } from './index.js'; +import { Heading, HeadingLevels, useHeadingLevel } from './index.js'; test('keeps semantic heading level independent of visual type style', async () => { const { locator } = render( @@ -24,3 +24,26 @@ test('keeps semantic heading level independent of visual type style', async () = expect(getComputedStyle(styled).fontSize).toBe(getComputedStyle(reference).fontSize); expect(getComputedStyle(styled).fontSize).not.toBe(getComputedStyle(defaultH2).fontSize); }); + +test('useHeadingLevel reads the current level without advancing it', async () => { + function CurrentLevel({ label }: { label: string }) { + const { element: Element, level } = useHeadingLevel(); + return {`${label} h${level}`}; + } + + const { locator } = render( + + + + + + , + ); + + expect(locator.getByRole('heading', { level: 2, name: 'current h2' }).element().tagName).toBe( + 'H2', + ); + expect(locator.getByRole('heading', { level: 3, name: 'nested h3' }).element().tagName).toBe( + 'H3', + ); +}); diff --git a/packages/@luke-ui/react/src/heading/index.tsx b/packages/@luke-ui/react/src/heading/index.tsx index 38673603..7d5455f1 100644 --- a/packages/@luke-ui/react/src/heading/index.tsx +++ b/packages/@luke-ui/react/src/heading/index.tsx @@ -1,10 +1,15 @@ -import type { HeadingLevel, HeadingLevelsProps } from '../heading-context/index.js'; -import { HeadingLevels, HeadingPresenceProvider } from '../heading-context/index.js'; import type { TextProps } from '../text/index.js'; import { Text } from '../text/index.js'; import type { Prettify } from '../types/prettify.js'; +import type { HeadingLevel, HeadingLevelsProps } from './heading-context.js'; +import { HeadingLevels, HeadingPresenceProvider } from './heading-context.js'; -export type { HeadingLevel } from '../heading-context/index.js'; +export type { + HeadingLevel, + HeadingLevelsProps, + HeadingLevelsRenderProps, +} from './heading-context.js'; +export { HeadingLevels, useHeadingLevel } from './heading-context.js'; /** Valid heading tag name for Luke UI headings. */ export type HeadingTag = `h${HeadingLevel}`; @@ -13,11 +18,7 @@ interface _HeadingProps extends TextProps { level?: HeadingLevel; } -/** - * Props for `Heading`. - * - * @tier atom - */ +/** Props for `Heading`. */ export type HeadingProps = Prettify<_HeadingProps>; const typographyByLevel = { diff --git a/packages/@luke-ui/react/src/icon-button/index.tsx b/packages/@luke-ui/react/src/icon-button/index.tsx index 96f952e2..c7f307ed 100644 --- a/packages/@luke-ui/react/src/icon-button/index.tsx +++ b/packages/@luke-ui/react/src/icon-button/index.tsx @@ -1,16 +1,19 @@ +export { type IconButtonRecipeVariants, iconButtonRecipe } from './recipe.css.js'; + import type { JSX } from 'react'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; -import type { ButtonProps as PrimitiveButtonProps } from '../button/primitive/index.js'; -import { Button } from '../button/primitive/index.js'; import type { IconName } from '../icon/index.js'; import { Icon } from '../icon/index.js'; -import * as styles from '../recipes/icon-button.css.js'; +import type { ButtonProps as PrimitiveButtonProps } from '../primitives/button/index.js'; +import { Button } from '../primitives/button/index.js'; import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { DocumentedPressProps } from '../types/documented-rac-props.js'; import type { Prettify } from '../types/prettify.js'; import { cx } from '../utils/index.js'; +import type { IconButtonRecipeVariants } from './recipe.css.js'; +import { iconButtonIcon, iconButtonRecipe, iconButtonReset } from './recipe.css.js'; -interface IconButtonRecipeProps extends NonNullable {} +interface IconButtonRecipeProps extends NonNullable {} interface IconButtonStyleProps { /** @@ -27,11 +30,7 @@ interface _IconButtonProps extends _IconButtonOmit, IconButtonStyleProps, Docume icon: IconName; } -/** - * Props for `IconButton`. - * - * @tier composed - */ +/** Props for `IconButton`. */ export type IconButtonProps = Prettify<_IconButtonProps>; /** Button that renders only an icon. */ @@ -43,8 +42,8 @@ export function IconButton(props: IconButtonProps): JSX.Element { {...buttonProps} className={composeRenderProps(props.className, (value) => { return cx( - styles.iconButtonReset, - styles.iconButton({ + iconButtonReset, + iconButtonRecipe({ size, }), value, @@ -53,7 +52,7 @@ export function IconButton(props: IconButtonProps): JSX.Element { isPending={isPending} size={size} > - + ); } diff --git a/packages/@luke-ui/react/src/recipes/icon-button.css.ts b/packages/@luke-ui/react/src/icon-button/recipe.css.ts similarity index 83% rename from packages/@luke-ui/react/src/recipes/icon-button.css.ts rename to packages/@luke-ui/react/src/icon-button/recipe.css.ts index d83c4a69..bf706d8a 100644 --- a/packages/@luke-ui/react/src/recipes/icon-button.css.ts +++ b/packages/@luke-ui/react/src/icon-button/recipe.css.ts @@ -1,7 +1,7 @@ import { styleInLayer } from '../styles/layered-style.css.js'; +import type { RecipeSelection } from '../styles/recipe.js'; +import { recipe } from '../styles/recipe.js'; import { vars } from '../theme/contract.css.js'; -import type { RecipeSelection } from './recipe.js'; -import { recipe } from './recipe.js'; export const iconButtonReset = styleInLayer('recipes', { '@media': { @@ -41,7 +41,7 @@ export const iconButtonIcon = recipe({ }); /** Vanilla-extract recipe for the `IconButton` primitive's styles. */ -export const iconButton = recipe({ +export const iconButtonRecipe = recipe({ variants: { size: { medium: { @@ -55,4 +55,4 @@ export const iconButton = recipe({ }); /** Variant type for the `IconButton` recipe. */ -export type IconButtonVariants = RecipeSelection; +export type IconButtonRecipeVariants = RecipeSelection; diff --git a/packages/@luke-ui/react/src/icon-size-context/index.tsx b/packages/@luke-ui/react/src/icon/icon-size-context.tsx similarity index 100% rename from packages/@luke-ui/react/src/icon-size-context/index.tsx rename to packages/@luke-ui/react/src/icon/icon-size-context.tsx diff --git a/packages/@luke-ui/react/src/icon/index.tsx b/packages/@luke-ui/react/src/icon/index.tsx index 2479ae42..628e5307 100644 --- a/packages/@luke-ui/react/src/icon/index.tsx +++ b/packages/@luke-ui/react/src/icon/index.tsx @@ -1,19 +1,23 @@ +export { type IconRecipeVariants, iconRecipe } from './recipe.css.js'; + import type { JSX, ReactNode, SVGAttributes } from 'react'; import { createContext, useContext } from 'react'; import { iconNames, iconViewBoxes } from '../../.generated/icon-data.js'; -import { useIconSizeContext } from '../icon-size-context/index.js'; -import * as styles from '../recipes/icon.css.js'; import { ICON_VIEWBOX } from '../sizing/icon-sizing.js'; import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { Prettify } from '../types/prettify.js'; import { cx } from '../utils/index.js'; +import { useIconSizeContext } from './icon-size-context.js'; +import type { IconRecipeVariants } from './recipe.css.js'; +import { iconRecipe } from './recipe.css.js'; export type { IconName } from '../../.generated/icon-data.js'; +export { IconSizeProvider } from './icon-size-context.js'; export { iconNames, iconViewBoxes }; const IconSpritesheetContext = createContext(null); -interface IconVariantProps extends NonNullable {} +interface IconVariantProps extends NonNullable {} interface IconStyleProps { /** @@ -48,11 +52,7 @@ interface _IconProps title?: string; } -/** - * Props for the built-in `Icon` component. - * - * @tier atom - */ +/** Props for the built-in `Icon` component. */ export type IconProps = Prettify<_IconProps>; /** Props used by `createIcon` for custom icon components. */ @@ -87,7 +87,7 @@ export function createIcon({ const svgProps: React.SVGProps = { 'aria-hidden': ariaHidden, - className: cx(styles.icon({ size: resolvedSize }), className), + className: cx(iconRecipe({ size: resolvedSize }), className), fill: 'currentColor', focusable: false, id, diff --git a/packages/@luke-ui/react/src/recipes/icon.css.ts b/packages/@luke-ui/react/src/icon/recipe.css.ts similarity index 72% rename from packages/@luke-ui/react/src/recipes/icon.css.ts rename to packages/@luke-ui/react/src/icon/recipe.css.ts index 98d9a1dd..23a38f7f 100644 --- a/packages/@luke-ui/react/src/recipes/icon.css.ts +++ b/packages/@luke-ui/react/src/icon/recipe.css.ts @@ -1,6 +1,6 @@ +import type { RecipeSelection } from '../styles/recipe.js'; +import { recipe } from '../styles/recipe.js'; import { vars } from '../theme/contract.css.js'; -import type { RecipeSelection } from './recipe.js'; -import { recipe } from './recipe.js'; /** Shared size dimensions for Icon and LoadingSpinner (icon-aligned sizing). */ export const iconSizeVariants = { @@ -22,8 +22,8 @@ export const iconSizeVariants = { }, } as const; -/** Vanilla-extract recipe for the `Icon` primitive's styles. */ -export const icon = recipe({ +/** Vanilla-extract recipe for the `Icon` component's styles. */ +export const iconRecipe = recipe({ base: { display: 'inline-flex', flexShrink: 0, @@ -37,4 +37,4 @@ export const icon = recipe({ }); /** Variant type for the `Icon` recipe. */ -export type IconVariants = RecipeSelection; +export type IconRecipeVariants = RecipeSelection; diff --git a/packages/@luke-ui/react/src/kbd/index.tsx b/packages/@luke-ui/react/src/kbd/index.tsx index 02a93ae0..2c11bbd0 100644 --- a/packages/@luke-ui/react/src/kbd/index.tsx +++ b/packages/@luke-ui/react/src/kbd/index.tsx @@ -1,17 +1,15 @@ -import * as styles from '../recipes/kbd.css.js'; +export { type KbdRecipeVariants, kbdRecipe } from './recipe.css.js'; + import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { Prettify } from '../types/prettify.js'; import { cx } from '../utils/index.js'; +import { kbdRecipe } from './recipe.css.js'; type _KbdOmit = DistributiveOmit, never>; interface _KbdProps extends _KbdOmit {} -/** - * Props for the `Kbd` component. - * - * @tier atom - */ +/** Props for the `Kbd` component. */ export type KbdProps = Prettify<_KbdProps>; /** @@ -19,5 +17,5 @@ export type KbdProps = Prettify<_KbdProps>; */ export function Kbd(props: KbdProps) { const { className, ...elementProps } = props; - return ; + return ; } diff --git a/packages/@luke-ui/react/src/recipes/kbd.css.ts b/packages/@luke-ui/react/src/kbd/recipe.css.ts similarity index 79% rename from packages/@luke-ui/react/src/recipes/kbd.css.ts rename to packages/@luke-ui/react/src/kbd/recipe.css.ts index 13c7407f..77c278e9 100644 --- a/packages/@luke-ui/react/src/recipes/kbd.css.ts +++ b/packages/@luke-ui/react/src/kbd/recipe.css.ts @@ -1,8 +1,8 @@ import { styleInLayer } from '../styles/layered-style.css.js'; +import type { RecipeSelection } from '../styles/recipe.js'; +import { recipe } from '../styles/recipe.js'; import { vars } from '../theme/contract.css.js'; import { FONT_METRIC_SCALE } from '../theme/font-metric-scale.js'; -import type { RecipeSelection } from './recipe.js'; -import { recipe } from './recipe.js'; const base = styleInLayer('recipes', { alignItems: 'center', @@ -24,8 +24,8 @@ const base = styleInLayer('recipes', { }); /** Vanilla-extract recipe for the `Kbd` component's inline keyboard-key appearance. */ -export const kbd = recipe({ +export const kbdRecipe = recipe({ base, }); -export type KbdVariants = RecipeSelection; +export type KbdRecipeVariants = RecipeSelection; diff --git a/packages/@luke-ui/react/src/link/index.tsx b/packages/@luke-ui/react/src/link/index.tsx index 13be38b9..fb67b4ea 100644 --- a/packages/@luke-ui/react/src/link/index.tsx +++ b/packages/@luke-ui/react/src/link/index.tsx @@ -1,14 +1,17 @@ +export { type LinkRecipeVariants, linkRecipe } from './recipe.css.js'; + import type { JSX } from 'react'; import type { LinkProps as RacLinkProps } from 'react-aria-components/Link'; import { Link as RacLink } from 'react-aria-components/Link'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; -import * as styles from '../recipes/link.css.js'; import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { DocumentedLinkProps } from '../types/documented-rac-props.js'; import type { Prettify } from '../types/prettify.js'; import { cx } from '../utils/index.js'; +import type { LinkRecipeVariants } from './recipe.css.js'; +import { linkRecipe } from './recipe.css.js'; -interface LinkVariantProps extends NonNullable {} +interface LinkVariantProps extends NonNullable {} interface LinkStyleProps { /** Hides the underline until hover or press and provides a structural 24px target. */ @@ -21,11 +24,7 @@ type _LinkOmit = DistributiveOmit; interface _LinkProps extends _LinkOmit, LinkStyleProps, DocumentedLinkProps {} -/** - * Props for the primitive link. - * - * @tier atom - */ +/** Props for the `Link` component. */ export type LinkProps = Prettify<_LinkProps>; /** Styled link. */ @@ -36,7 +35,7 @@ export function Link(props: LinkProps): JSX.Element { { - return cx(styles.link({ isStandalone, tone }), className); + return cx(linkRecipe({ isStandalone, tone }), className); })} /> ); diff --git a/packages/@luke-ui/react/src/recipes/link.css.ts b/packages/@luke-ui/react/src/link/recipe.css.ts similarity index 89% rename from packages/@luke-ui/react/src/recipes/link.css.ts rename to packages/@luke-ui/react/src/link/recipe.css.ts index 9cee0fda..9e53005a 100644 --- a/packages/@luke-ui/react/src/recipes/link.css.ts +++ b/packages/@luke-ui/react/src/link/recipe.css.ts @@ -1,7 +1,7 @@ import { styleInLayer } from '../styles/layered-style.css.js'; +import type { RecipeSelection } from '../styles/recipe.js'; +import { recipe } from '../styles/recipe.js'; import { vars } from '../theme/contract.css.js'; -import type { RecipeSelection } from './recipe.js'; -import { recipe } from './recipe.js'; const base = styleInLayer('recipes', { '@media': { @@ -35,8 +35,8 @@ const base = styleInLayer('recipes', { }, }); -/** Vanilla-extract recipe for the `Link` primitive's styles. */ -export const link = recipe({ +/** Vanilla-extract recipe for the `Link` component's styles. */ +export const linkRecipe = recipe({ base, defaultVariants: { isStandalone: false, @@ -91,4 +91,4 @@ export const link = recipe({ }); /** Variant type for the `Link` recipe. */ -export type LinkVariants = RecipeSelection; +export type LinkRecipeVariants = RecipeSelection; diff --git a/packages/@luke-ui/react/src/loading-skeleton/index.tsx b/packages/@luke-ui/react/src/loading-skeleton/index.tsx index 6b971c6c..3cd1209f 100644 --- a/packages/@luke-ui/react/src/loading-skeleton/index.tsx +++ b/packages/@luke-ui/react/src/loading-skeleton/index.tsx @@ -1,10 +1,14 @@ import type { ComponentProps, ElementType, JSX, ReactNode } from 'react'; import { createContext, isValidElement, useContext } from 'react'; -import * as styles from '../recipes/loading-skeleton.css.js'; import { vars } from '../theme/contract.css.js'; import type { Prettify } from '../types/prettify.js'; import { useSynchronizeAnimations } from '../use-synchronize-animations/use-synchronize-animations.js'; import { cx } from '../utils/index.js'; +import { + loadingSkeletonClassName, + skeletonAnimationName, + skeletonRadiusVar, +} from './styles.css.js'; const LoadingSkeletonContext = createContext(null); @@ -41,11 +45,7 @@ interface _LoadingSkeletonProps extends ComponentProps<'span'> { radius?: keyof typeof vars.radius; } -/** - * Props for `LoadingSkeleton`. - * - * @tier atom - */ +/** Props for `LoadingSkeleton`. */ export type LoadingSkeletonProps = Prettify<_LoadingSkeletonProps>; /** @@ -66,7 +66,7 @@ export function LoadingSkeleton(props: LoadingSkeletonProps): ReactNode { const isLoadingContext = useContext(LoadingSkeletonContext); const isLoading = isLoadingContext ?? isLoadingProp ?? true; - useSynchronizeAnimations(isLoading ? styles.skeletonAnimationName : null); + useSynchronizeAnimations(isLoading ? skeletonAnimationName : null); if (!isLoading) return children; @@ -77,10 +77,10 @@ export function LoadingSkeleton(props: LoadingSkeletonProps): ReactNode { {children} diff --git a/packages/@luke-ui/react/src/recipes/loading-skeleton.css.ts b/packages/@luke-ui/react/src/loading-skeleton/styles.css.ts similarity index 100% rename from packages/@luke-ui/react/src/recipes/loading-skeleton.css.ts rename to packages/@luke-ui/react/src/loading-skeleton/styles.css.ts diff --git a/packages/@luke-ui/react/src/loading-spinner/index.tsx b/packages/@luke-ui/react/src/loading-spinner/index.tsx index fc101f20..a15761a1 100644 --- a/packages/@luke-ui/react/src/loading-spinner/index.tsx +++ b/packages/@luke-ui/react/src/loading-spinner/index.tsx @@ -1,7 +1,8 @@ +export { type LoadingSpinnerRecipeVariants, loadingSpinnerRecipe } from './recipe.css.js'; + import type { ComponentProps, ReactNode } from 'react'; import { useId } from 'react'; -import { useIconSizeContext } from '../icon-size-context/index.js'; -import * as styles from '../recipes/loading-spinner.css.js'; +import { useIconSizeContext } from '../icon/icon-size-context.js'; import { ICON_VIEWBOX, ICON_VIEWBOX_SIZE, @@ -12,8 +13,10 @@ import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { Prettify } from '../types/prettify.js'; import { useSynchronizeAnimations } from '../use-synchronize-animations/use-synchronize-animations.js'; import { VisuallyHidden } from '../visually-hidden/index.js'; +import type { LoadingSpinnerRecipeVariants } from './recipe.css.js'; +import { loadingSpinnerRecipe, rubberBandAnimationName, spinAnimationName } from './recipe.css.js'; -interface LoadingSpinnerVariantProps extends NonNullable {} +interface LoadingSpinnerVariantProps extends NonNullable {} interface LoadingSpinnerStyleProps { /** Sets a semantic content color. Omit to inherit the surrounding content color. */ @@ -34,11 +37,7 @@ interface _LoadingSpinnerProps extends _LoadingSpinnerOmit, LoadingSpinnerStyleP isLoading?: boolean; } -/** - * Props for `LoadingSpinner`. - * - * @tier atom - */ +/** Props for `LoadingSpinner`. */ export type LoadingSpinnerProps = Prettify<_LoadingSpinnerProps>; /** Animated spinner shown while work is in progress. Wrap content in it to show the spinner in place of that content until loading finishes. */ @@ -72,7 +71,7 @@ export function LoadingSpinner(props: LoadingSpinnerProps): ReactNode { if (!children) return spinnerElement; - const slots = styles.loadingSpinner(); + const slots = loadingSpinnerRecipe(); return ( @@ -94,11 +93,11 @@ function SpinnerElement({ style, ...spanProps }: SpinnerElementProps) { - useSynchronizeAnimations(styles.spinAnimationName); - useSynchronizeAnimations(styles.rubberBandAnimationName); + useSynchronizeAnimations(spinAnimationName); + useSynchronizeAnimations(rubberBandAnimationName); const labelId = useId(); - const slots = styles.loadingSpinner({ color, size }); + const slots = loadingSpinnerRecipe({ color, size }); const viewBoxCenter = ICON_VIEWBOX_SIZE / 2; return ( diff --git a/packages/@luke-ui/react/src/recipes/loading-spinner.css.ts b/packages/@luke-ui/react/src/loading-spinner/recipe.css.ts similarity index 88% rename from packages/@luke-ui/react/src/recipes/loading-spinner.css.ts rename to packages/@luke-ui/react/src/loading-spinner/recipe.css.ts index c3e452b9..47c33bd0 100644 --- a/packages/@luke-ui/react/src/recipes/loading-spinner.css.ts +++ b/packages/@luke-ui/react/src/loading-spinner/recipe.css.ts @@ -1,9 +1,9 @@ import { keyframes } from '@vanilla-extract/css'; +import { iconSizeVariants } from '../icon/recipe.css.js'; +import type { RecipeSelection, SlottedConfigInput } from '../styles/recipe.js'; +import { recipe } from '../styles/recipe.js'; +import { spinnerOverlayBase } from '../styles/spinner-overlay.js'; import { vars } from '../theme/contract.css.js'; -import { iconSizeVariants } from './icon.css.js'; -import type { RecipeSelection, SlottedConfigInput } from './recipe.js'; -import { recipe } from './recipe.js'; -import { spinnerOverlayBase } from './spinner-overlay.js'; const rotationDuration = '1.2s'; const rubberBandDuration = '2s'; @@ -113,7 +113,7 @@ const loadingSpinnerConfig = { * itself, and `.childrenWrapper() / .hiddenChildren() / .spinnerOverlay()` for the * in-place children overlay. */ -export const loadingSpinner = recipe(loadingSpinnerConfig); +export const loadingSpinnerRecipe = recipe(loadingSpinnerConfig); /** Outer variant selection for the `LoadingSpinner` recipe. */ -export type LoadingSpinnerVariants = RecipeSelection; +export type LoadingSpinnerRecipeVariants = RecipeSelection; diff --git a/packages/@luke-ui/react/src/numeral/index.tsx b/packages/@luke-ui/react/src/numeral/index.tsx index 23256cf4..5505be0b 100644 --- a/packages/@luke-ui/react/src/numeral/index.tsx +++ b/packages/@luke-ui/react/src/numeral/index.tsx @@ -1,5 +1,5 @@ import { useLocale } from 'react-aria-components/I18nProvider'; -import { useIsWithinHeading } from '../heading-context/index.js'; +import { useIsWithinHeading } from '../heading/heading-context.js'; import type { TextProps } from '../text/index.js'; import { Text } from '../text/index.js'; import type { DistributiveOmit } from '../types/distributive-omit.js'; @@ -42,11 +42,7 @@ interface _NumeralProps extends _NumeralOmit { value: number; } -/** - * Props for `Numeral`. - * - * @tier atom - */ +/** Props for `Numeral`. */ export type NumeralProps = Prettify<_NumeralProps>; /** Formats a number and renders it with the same typography props as `Text`. */ diff --git a/packages/@luke-ui/react/src/recipes/mobile-overlay.css.ts b/packages/@luke-ui/react/src/overlays/mobile-overlay.css.ts similarity index 100% rename from packages/@luke-ui/react/src/recipes/mobile-overlay.css.ts rename to packages/@luke-ui/react/src/overlays/mobile-overlay.css.ts diff --git a/packages/@luke-ui/react/src/overlays/mobile-overlay.tsx b/packages/@luke-ui/react/src/overlays/mobile-overlay.tsx index efea9ce0..4da968ec 100644 --- a/packages/@luke-ui/react/src/overlays/mobile-overlay.tsx +++ b/packages/@luke-ui/react/src/overlays/mobile-overlay.tsx @@ -2,9 +2,9 @@ import type { JSX, ReactNode, Ref } from 'react'; import type { DialogProps } from 'react-aria-components/Dialog'; import { Dialog, OverlayTriggerStateContext } from 'react-aria-components/Dialog'; import { Modal, ModalOverlay } from 'react-aria-components/Modal'; -import * as styles from '../recipes/mobile-overlay.css.js'; import { rootClassName } from '../theme/index.js'; import { cx } from '../utils/index.js'; +import { mobileDialog, mobileModal, mobileOverlay } from './mobile-overlay.css.js'; interface MobileOverlayProps { 'aria-describedby'?: DialogProps['aria-describedby']; @@ -35,7 +35,7 @@ export function MobileOverlay({ // labelling behaviour to that state instead of this tray's own `Modal` state. ({ top: typeof window === 'undefined' ? 0 : window.scrollY })} > - + {children} diff --git a/packages/@luke-ui/react/src/button/primitive/index.tsx b/packages/@luke-ui/react/src/primitives/button/index.tsx similarity index 76% rename from packages/@luke-ui/react/src/button/primitive/index.tsx rename to packages/@luke-ui/react/src/primitives/button/index.tsx index 6c377344..5106bb92 100644 --- a/packages/@luke-ui/react/src/button/primitive/index.tsx +++ b/packages/@luke-ui/react/src/primitives/button/index.tsx @@ -1,14 +1,17 @@ +export { type ButtonRecipeVariants, buttonRecipe } from './recipe.css.js'; + import type { JSX } from 'react'; import type { ButtonProps as RacButtonProps } from 'react-aria-components/Button'; import { Button as RacButton } from 'react-aria-components/Button'; import { composeRenderProps } from 'react-aria-components/composeRenderProps'; -import { IconSizeProvider } from '../../icon-size-context/index.js'; -import * as styles from '../../recipes/button.css.js'; +import { IconSizeProvider } from '../../icon/icon-size-context.js'; import { BUTTON_ICON_SIZE } from '../../sizing/button-sizing.js'; import type { Prettify } from '../../types/prettify.js'; import { cx } from '../../utils/index.js'; +import type { ButtonRecipeVariants } from './recipe.css.js'; +import { buttonRecipe } from './recipe.css.js'; -interface ButtonRecipeProps extends NonNullable {} +interface ButtonRecipeProps extends NonNullable {} interface ButtonStyleProps { /** @@ -35,12 +38,7 @@ interface ButtonStyleProps { interface _ButtonProps extends RacButtonProps, ButtonStyleProps {} -/** - * Primitive button — a bare `