Commits
* Expand styling documentation
* Tighten the styling page prose
* Clarify static CSS usage in styling documentation
* Add shadow documentation
* Remove stray responsive claim from the depth example description
* Tighten the shadow page prose
* Visualise theme depth
* Simplify depth example
* Clarify depth documentation example
Adds unbarrelify as a dev tool (catalog + @luke-ui/react devDependency)
and rewires the five internal consumers that imported through
src/styles/index.ts and src/combobox-field/primitive/index.tsx to
import directly from utilities.css.js and item.js instead.
Neither barrel file is deleted: both back public subpath exports
(./styles and ./combobox-field/primitive). Because those exports point
at built dist/ paths rather than the src/ files themselves,
unbarrelify's built-in package-export exception does not recognise
them and would delete both as internal barrels, which breaks the
public API (confirmed via a failing tsc run and vp pack silently
dropping both exports entries). check:barrels therefore skips these
two known package-export barrels; every other barrel in the source
tree is still caught, verified with a throwaway negative test.
Wires check:barrels ("unbarrelify --check") into turbo.json's
aggregate check task the same way check:format/check:lint/check:types
are wired, as its own global task that no-ops for packages without the
script, so `pnpm run check` exercises it repo-wide.
pnpm-lock.yaml picks up unrelated transitive churn (mostly optional
oxc-transform/oxc-minify platform binaries no longer needed after a
srvx bump) that surfaced when the lockfile was re-resolved for the new
dependency; build and tests pass against the regenerated lockfile.
* Replace DS icons with Heroicons
* Simplify copied icon rendering
* Consolidate polymorphic APIs onto elementType and render
Drop the hand-rolled `as` prop (used only by LoadingSkeleton) in favour
of `elementType`, the react-aria-components convention already used by
Text, Heading, and Numeral. `as` and `elementType` did the same job
(pick the DOM tag); this removes the duplicate, non-idiomatic path.
Also extract Box's local `BoxRender` type into a shared, documented
`RenderProp` type. react-aria-components does not publicly export its
render-function type, so this is the canonical Luke UI definition,
stopping the next component from inventing another render-prop variant.
Net API surface: `elementType` = which tag, `render` = take over
rendering. Two orthogonal, RAC-aligned APIs, no bespoke `as`.
- LoadingSkeleton: `as` -> `elementType` (component, stories, docs, example)
- heading-context: fix stale JSDoc example that referenced `as`
- Box: use shared RenderProp<'div'> instead of local BoxRender
- add packages/@luke-ui/react/src/types/render-prop.ts
* Reflow loading-skeleton docs prose to satisfy formatter
* Ship theming docs and global controls
* Tighten theming documentation
* Clarify consumer-owned themes
* Migrate loading feedback primitives (#102)
* Address PR review feedback
* Fix AllIcons story
* Make Storybook canvas fill previews
* Tune loading animations
* Set Storybook canvas foreground colour
* Improve LoadingSkeleton dark contrast
* Migrate typography to composite type roles
* Address typography review feedback
* Migrate Link to semantic navigation states
* Migrate typography to composite type roles
* Address typography review feedback
* Migrate Combobox to the new theme
* Address Combobox review feedback
* Polish combobox theme previews
* Fix Combobox option hover contrast
* Migrate Field and TextInput to tactile wells
Moves Field, TextInput, and TextField onto the new semantic theme contract established by #112. The input control now renders as a recessed well with an accent-coloured hover/focus edge and a complete mode-aware focus ring; invalid fields keep a danger border under focus; disabled fields preserve resting material at reduced opacity; read-only fields flatten into the canvas surface instead of relying on colour alone.
Closes #99
* Give input wells visible recessed depth
* Establish tactile Button and IconButton controls
* Center IconButton content
* Apply the default theme in docs
* Author semantic depth values directly
* Author semantic depth values directly
* Remove authored action control borders
* Remove inset control depth
* Restore exterior control depth
* Add action control face lighting
* Address action control review feedback
* Rename Button appearance API
* Clarify pending action cues
* Restore ELMO and docs theme controls
* Update tokens
* Improve visual regression workflow
* Allow trailing commas in captureVisual ID validation
On iOS Safari window.innerHeight tracks the visual viewport, so it
shrinks with the on-screen keyboard and the keyboard inset collapsed
to ~0, leaving the tray cut off behind the keyboard. Measure the
fixed-position containing block directly with an inset: 0 probe
element instead, which is correct regardless of how a browser reports
toolbar and keyboard geometry.
Refs #91
* refactor: apply code style rules across the codebase
Hoists DOC_GROUPS into package-docs-catalog.ts so the docs group set
lives in one place instead of being duplicated between turbo-generators
and the meta.json. Adds tierBucket() so render-index and render-llms-full
share one classification function instead of maintaining independent
tier-to-section mappings. Adds @tier validation in resolveEntry that
throws when a component export has no @tier tag — missing tags were
silently defaulting to 'composed'.
Changes:
- package-docs-catalog.ts: export DOC_GROUPS, tierBucket(), @tier check
- render-index.ts: use tierBucket (switch over 4 buckets)
- render-llms-full.ts: use tierBucket, drop bucketFor
- turbo-generators/config.ts: import DOC_GROUPS, remove local copy
- turbo-generators/package.json: add @luke-ui/docs-tools dependency
Button, IconButton, Link, TextField each had a local RedeclaredRACProps
interface with hand-copied JSDoc for isDisabled, onPress, type, etc.
Five copies, already drifting.
Creates src/types/documented-rac-props.ts with three mixin interfaces
that carry the canonical JSDoc:
- DocumentedPressProps (button, icon-button)
- DocumentedLinkProps (link)
- DocumentedInputProps (text-field)
Each component deletes its local interface and extends the shared
mixin instead. ComboboxField keeps a local interface because its
value type (Key | null) does not match DocumentedInputProps (string).
4 components touched, ~50 lines removed, one JSDoc source per prop.
Four prop interfaces (FieldProps, TextFieldProps, ComboboxFieldProps,
and the inline quartet in compose-field.ts own FieldSlotProps) each
hand-typed the same label/description/errorMessage/necessityIndicator
props with duplicate JSDoc.
This makes all three consumer interfaces extend FieldSlotProps from
compose-field.ts, so the slot interface and its runtime split travel
together. The necessityIndicator default JSDoc moves to FieldSlotProps.
* Replace per-file size prop types with canonical ComboboxSize inline
Five combobox primitives each declared ComboboxVariantProps and
ComboboxStyleProps types to thread the size prop. This deletes the
local types and imports size?: ComboboxSize from root.js directly.
item.tsx had inlined size?: 'small' | 'medium', which had already drifted
from the canonical type. That is fixed.
* Fix formatting in combobox primitives
Swap Argos for Vitest's native toMatchScreenshot in a new visual project, add a shared test toolkit, cover 12 components, and document a doc-rot convention.
Consolidate the two conflicting Testing sections in CONVENTIONS.md into
a single pointer, and move the docs-tools tests out of __tests__ to sit
beside their sources per the colocation rule.
* Extract a shared input-state module behind the control recipes
text-input.css.ts and combobox.css.ts each maintained their own copy of
the disabled / hover / focus-within / invalid / read-only selector
constants and state treatments, and the copies had drifted: different
focus-ring tokens, forced-colors support on only one of them. The new
recipes/input-states.css.ts owns the state definitions and the full
control chrome; both recipes consume it, and the combobox extends state
detection explicitly for its input+trigger anatomy and keeps its visible
disabled border as a deliberate override.
Deliberate behaviour changes:
- The combobox focus ring now uses the focusRingColor token (was
paletteThemePrimary200, one shade lighter).
- The combobox control gains the forced-colors treatments the text input
already had.
- Read-only detection is scoped to :has(input:read-only). Bare
:read-only matches any non-editable element, so every combobox
control (via its trigger button) and every adorned text input group
rendered with the read-only background unconditionally.
- Read-only styling no longer applies to disabled controls (a disabled
input also matches :read-only; the two backgrounds happen to share a
value today, so this pins semantics rather than changing pixels).
- The combobox focus-within state drops [data-focused="true"]: RAC
Group never emits data-focused, so the selector was dead.
The browser vitest project now loads vanillaExtractPlugin (matching the
unit and storybook projects) so the new regression test can assert
computed styles against the compiled recipes in real Chromium.
* Align the combobox item focus ring with the control ring
comboboxItem's focus-visible outline was the last focus ring still
hardcoded to paletteThemePrimary200; the global reset and the shared
input chrome both use the focusRingColor token, so a focused listbox
item rendered one shade lighter than the focused control above it.
* Add combobox selection indicators and rework the option focus treatment
The keyboard-active listbox option is now indicated by a background one
step stronger than hover (neutralPressed) instead of a focus ring — DOM
focus stays on the input under aria-activedescendant, so the control
shows the only ring. Forced-colors mode keeps a 2px inset ring on the
active option, since background highlights are stripped there.
Selection surfaces, following the Park UI shape:
- The selected option shows a trailing check icon in the listbox.
ComboboxItem re-derives textValue for string children because the
checkmark wrapper is a render function, which disables RAC's own
inference.
- New ComboboxClearButton primitive (exported from the kit) clears the
selection and input text via the modern ComboBoxState value API
(state.value / setValue — selectedKey/setSelectedKey are deprecated).
It renders nothing while there is no value, and opts out of the RAC
button slot so it doesn't toggle the popover. The composed
ComboboxField shows it before the trigger unless disabled/read-only.
- The trigger's non-first-child background is transparent instead of
subtle, so the chevron box no longer reads darker than the input.
Icon sizing moved behind the seam that owns it: COMBOBOX_ICON_SIZE in
sizing/ (deliberately distinct from BUTTON_ICON_SIZE), provided by the
trigger, clear button, and item via IconSizeProvider — callers no
longer pass icon sizes, and consumer icons inside those parts inherit
the right size.
The 'removes stale generated package docs' test runs the real
generate-docs script in a subprocess, which takes ~6s even on a warm
local machine — over Vitest's default 5s timeout. Give the test an
explicit 60s timeout.
* Update GitHub Actions to Node 24 runtimes
Resolves the 'Node.js 20 is deprecated' warnings by bumping
jdx/mise-action to v4, actions/configure-pages to v6,
actions/upload-pages-artifact to v5, and actions/deploy-pages to v5.
All four are runtime-only major bumps with no input changes.
* Run knip via turbo in CI so generate runs first
Bare `pnpm run check:unused` fails on PRs that don't affect any
package: --affected runs nothing, routeTree.gen.ts is never generated,
and knip reports an unresolved import. turbo's //#check:unused task
already declares the generate dependencies.
* Run all CI checks through turbo uniformly
Replace the pull_request/push if-else with one shape: a single turbo
invocation for the scoped checks (--affected only on PRs, where the SCM
env vars are set) plus an unscoped turbo invocation for knip, which is
repo-wide and would be skipped by --affected on package-only changes.
Also adds check:format-root to the PR path, which previously never
checked root-file formatting.
* Add check:docs to the CI check job
A broken docs generator previously passed PR CI and only surfaced in
deploy-pages on main, since only docs#build depends on generate:docs.
TextField and ComboboxField both duplicated the same label/description/
errorMessage/necessityIndicator destructure and <Field> wrap. Concentrate
that scaffold in one internal helper so a third Composed field can reuse
it without copying the pattern again.
Closes #40
Adds unbarrelify as a dev tool (catalog + @luke-ui/react devDependency)
and rewires the five internal consumers that imported through
src/styles/index.ts and src/combobox-field/primitive/index.tsx to
import directly from utilities.css.js and item.js instead.
Neither barrel file is deleted: both back public subpath exports
(./styles and ./combobox-field/primitive). Because those exports point
at built dist/ paths rather than the src/ files themselves,
unbarrelify's built-in package-export exception does not recognise
them and would delete both as internal barrels, which breaks the
public API (confirmed via a failing tsc run and vp pack silently
dropping both exports entries). check:barrels therefore skips these
two known package-export barrels; every other barrel in the source
tree is still caught, verified with a throwaway negative test.
Wires check:barrels ("unbarrelify --check") into turbo.json's
aggregate check task the same way check:format/check:lint/check:types
are wired, as its own global task that no-ops for packages without the
script, so `pnpm run check` exercises it repo-wide.
pnpm-lock.yaml picks up unrelated transitive churn (mostly optional
oxc-transform/oxc-minify platform binaries no longer needed after a
srvx bump) that surfaced when the lockfile was re-resolved for the new
dependency; build and tests pass against the regenerated lockfile.
* Consolidate polymorphic APIs onto elementType and render
Drop the hand-rolled `as` prop (used only by LoadingSkeleton) in favour
of `elementType`, the react-aria-components convention already used by
Text, Heading, and Numeral. `as` and `elementType` did the same job
(pick the DOM tag); this removes the duplicate, non-idiomatic path.
Also extract Box's local `BoxRender` type into a shared, documented
`RenderProp` type. react-aria-components does not publicly export its
render-function type, so this is the canonical Luke UI definition,
stopping the next component from inventing another render-prop variant.
Net API surface: `elementType` = which tag, `render` = take over
rendering. Two orthogonal, RAC-aligned APIs, no bespoke `as`.
- LoadingSkeleton: `as` -> `elementType` (component, stories, docs, example)
- heading-context: fix stale JSDoc example that referenced `as`
- Box: use shared RenderProp<'div'> instead of local BoxRender
- add packages/@luke-ui/react/src/types/render-prop.ts
* Reflow loading-skeleton docs prose to satisfy formatter
* Migrate Field and TextInput to tactile wells
Moves Field, TextInput, and TextField onto the new semantic theme contract established by #112. The input control now renders as a recessed well with an accent-coloured hover/focus edge and a complete mode-aware focus ring; invalid fields keep a danger border under focus; disabled fields preserve resting material at reduced opacity; read-only fields flatten into the canvas surface instead of relying on colour alone.
Closes #99
* Give input wells visible recessed depth
* Establish tactile Button and IconButton controls
* Center IconButton content
* Apply the default theme in docs
* Author semantic depth values directly
* Author semantic depth values directly
* Remove authored action control borders
* Remove inset control depth
* Restore exterior control depth
* Add action control face lighting
* Address action control review feedback
* Rename Button appearance API
* Clarify pending action cues
* Restore ELMO and docs theme controls
* Update tokens
On iOS Safari window.innerHeight tracks the visual viewport, so it
shrinks with the on-screen keyboard and the keyboard inset collapsed
to ~0, leaving the tray cut off behind the keyboard. Measure the
fixed-position containing block directly with an inset: 0 probe
element instead, which is correct regardless of how a browser reports
toolbar and keyboard geometry.
Refs #91
Hoists DOC_GROUPS into package-docs-catalog.ts so the docs group set
lives in one place instead of being duplicated between turbo-generators
and the meta.json. Adds tierBucket() so render-index and render-llms-full
share one classification function instead of maintaining independent
tier-to-section mappings. Adds @tier validation in resolveEntry that
throws when a component export has no @tier tag — missing tags were
silently defaulting to 'composed'.
Changes:
- package-docs-catalog.ts: export DOC_GROUPS, tierBucket(), @tier check
- render-index.ts: use tierBucket (switch over 4 buckets)
- render-llms-full.ts: use tierBucket, drop bucketFor
- turbo-generators/config.ts: import DOC_GROUPS, remove local copy
- turbo-generators/package.json: add @luke-ui/docs-tools dependency
Button, IconButton, Link, TextField each had a local RedeclaredRACProps
interface with hand-copied JSDoc for isDisabled, onPress, type, etc.
Five copies, already drifting.
Creates src/types/documented-rac-props.ts with three mixin interfaces
that carry the canonical JSDoc:
- DocumentedPressProps (button, icon-button)
- DocumentedLinkProps (link)
- DocumentedInputProps (text-field)
Each component deletes its local interface and extends the shared
mixin instead. ComboboxField keeps a local interface because its
value type (Key | null) does not match DocumentedInputProps (string).
4 components touched, ~50 lines removed, one JSDoc source per prop.
Four prop interfaces (FieldProps, TextFieldProps, ComboboxFieldProps,
and the inline quartet in compose-field.ts own FieldSlotProps) each
hand-typed the same label/description/errorMessage/necessityIndicator
props with duplicate JSDoc.
This makes all three consumer interfaces extend FieldSlotProps from
compose-field.ts, so the slot interface and its runtime split travel
together. The necessityIndicator default JSDoc moves to FieldSlotProps.
* Replace per-file size prop types with canonical ComboboxSize inline
Five combobox primitives each declared ComboboxVariantProps and
ComboboxStyleProps types to thread the size prop. This deletes the
local types and imports size?: ComboboxSize from root.js directly.
item.tsx had inlined size?: 'small' | 'medium', which had already drifted
from the canonical type. That is fixed.
* Fix formatting in combobox primitives
* Extract a shared input-state module behind the control recipes
text-input.css.ts and combobox.css.ts each maintained their own copy of
the disabled / hover / focus-within / invalid / read-only selector
constants and state treatments, and the copies had drifted: different
focus-ring tokens, forced-colors support on only one of them. The new
recipes/input-states.css.ts owns the state definitions and the full
control chrome; both recipes consume it, and the combobox extends state
detection explicitly for its input+trigger anatomy and keeps its visible
disabled border as a deliberate override.
Deliberate behaviour changes:
- The combobox focus ring now uses the focusRingColor token (was
paletteThemePrimary200, one shade lighter).
- The combobox control gains the forced-colors treatments the text input
already had.
- Read-only detection is scoped to :has(input:read-only). Bare
:read-only matches any non-editable element, so every combobox
control (via its trigger button) and every adorned text input group
rendered with the read-only background unconditionally.
- Read-only styling no longer applies to disabled controls (a disabled
input also matches :read-only; the two backgrounds happen to share a
value today, so this pins semantics rather than changing pixels).
- The combobox focus-within state drops [data-focused="true"]: RAC
Group never emits data-focused, so the selector was dead.
The browser vitest project now loads vanillaExtractPlugin (matching the
unit and storybook projects) so the new regression test can assert
computed styles against the compiled recipes in real Chromium.
* Align the combobox item focus ring with the control ring
comboboxItem's focus-visible outline was the last focus ring still
hardcoded to paletteThemePrimary200; the global reset and the shared
input chrome both use the focusRingColor token, so a focused listbox
item rendered one shade lighter than the focused control above it.
* Add combobox selection indicators and rework the option focus treatment
The keyboard-active listbox option is now indicated by a background one
step stronger than hover (neutralPressed) instead of a focus ring — DOM
focus stays on the input under aria-activedescendant, so the control
shows the only ring. Forced-colors mode keeps a 2px inset ring on the
active option, since background highlights are stripped there.
Selection surfaces, following the Park UI shape:
- The selected option shows a trailing check icon in the listbox.
ComboboxItem re-derives textValue for string children because the
checkmark wrapper is a render function, which disables RAC's own
inference.
- New ComboboxClearButton primitive (exported from the kit) clears the
selection and input text via the modern ComboBoxState value API
(state.value / setValue — selectedKey/setSelectedKey are deprecated).
It renders nothing while there is no value, and opts out of the RAC
button slot so it doesn't toggle the popover. The composed
ComboboxField shows it before the trigger unless disabled/read-only.
- The trigger's non-first-child background is transparent instead of
subtle, so the chevron box no longer reads darker than the input.
Icon sizing moved behind the seam that owns it: COMBOBOX_ICON_SIZE in
sizing/ (deliberately distinct from BUTTON_ICON_SIZE), provided by the
trigger, clear button, and item via IconSizeProvider — callers no
longer pass icon sizes, and consumer icons inside those parts inherit
the right size.
* Update GitHub Actions to Node 24 runtimes
Resolves the 'Node.js 20 is deprecated' warnings by bumping
jdx/mise-action to v4, actions/configure-pages to v6,
actions/upload-pages-artifact to v5, and actions/deploy-pages to v5.
All four are runtime-only major bumps with no input changes.
* Run knip via turbo in CI so generate runs first
Bare `pnpm run check:unused` fails on PRs that don't affect any
package: --affected runs nothing, routeTree.gen.ts is never generated,
and knip reports an unresolved import. turbo's //#check:unused task
already declares the generate dependencies.
* Run all CI checks through turbo uniformly
Replace the pull_request/push if-else with one shape: a single turbo
invocation for the scoped checks (--affected only on PRs, where the SCM
env vars are set) plus an unscoped turbo invocation for knip, which is
repo-wide and would be skipped by --affected on package-only changes.
Also adds check:format-root to the PR path, which previously never
checked root-file formatting.
* Add check:docs to the CI check job
A broken docs generator previously passed PR CI and only surfaced in
deploy-pages on main, since only docs#build depends on generate:docs.