diff --git a/.agents/papercuts.md b/.agents/papercuts.md index 779ce110..7cb0e978 100644 --- a/.agents/papercuts.md +++ b/.agents/papercuts.md @@ -89,3 +89,24 @@ not help. Silence in this log is not evidence. consulting that guide before editing documentation. - Problem: The agent-authored section contradicted the required page order. Correcting the order does not establish that documentation authoring applies the page-structure guidance. + +## New layout tests locate named controls with DOM selectors + +- Seen: 2026-10-18 (#847). Self-review found new Button and Link inline-size tests using + `querySelector('button')` and `querySelector('a')` for named controls. +- Where: `packages/@luke-ui/react/src/core/button/button.browser.test.tsx` and + `link/link.browser.test.tsx`. `docs/TESTING.md`, Component tests, prefers role and accessible-name + locators. The write-tests workflow requires reading that guide before authoring tests. +- Problem: The agent-authored tests used DOM selectors despite available semantic locators. + Correcting the tests does not establish that test authoring applies the locator guidance. + +## Theme builds refresh tracked dependency metadata + +- Seen: 2026-10-19 (#847). Building unchanged main `5ca271c` and both implementation worktrees + changed the Paper and Tactile manifests' recorded metrics version from `4.3.0` to `4.4.0`. +- Where: `packages/@luke-ui/theme-paper/package.json` and + `packages/@luke-ui/theme-tactile/package.json`, `inlinedDependencies`. The dependency update in + #886 changed the installed version. AGENTS.md requires keeping unrelated changes out of task + commits. +- Problem: Required builds leave unrelated tracked changes behind, requiring authors to identify and + restore the generated metadata before finishing their work. diff --git a/apps/docs/content/docs/components/actions/button.mdx b/apps/docs/content/docs/components/actions/button.mdx index 80b83235..a6c29576 100644 --- a/apps/docs/content/docs/components/actions/button.mdx +++ b/apps/docs/content/docs/components/actions/button.mdx @@ -50,12 +50,12 @@ action in a group. For ordinary actions, high prominence uses the theme’s prim -## Block layout +## Inline sizing -Set `isBlock` when the button should fill the inline size of its container. A typical case is a -primary action at the end of a narrow form or panel. +Pass `inlineSize="full"` when the button should fill the inline size of its container. A typical +case is a primary action at the end of a narrow form or panel. - + ## Content slots diff --git a/apps/docs/content/docs/components/primitives/button.mdx b/apps/docs/content/docs/components/primitives/button.mdx index 828a6f73..b86b2752 100644 --- a/apps/docs/content/docs/components/primitives/button.mdx +++ b/apps/docs/content/docs/components/primitives/button.mdx @@ -20,8 +20,8 @@ spinner. The primitive does not. ## Appearance and size Use `tone` and `appearance` to set visual emphasis. Use `size` to match nearby controls. Set -`isBlock` when the button should take up its container's inline size. The primitive renders a native -` + ); }; diff --git a/apps/docs/src/lib/component-prop-analysis.test.ts b/apps/docs/src/lib/component-prop-analysis.test.ts index 303fc1d5..bb98d58d 100644 --- a/apps/docs/src/lib/component-prop-analysis.test.ts +++ b/apps/docs/src/lib/component-prop-analysis.test.ts @@ -570,7 +570,7 @@ const PINNED_VISIBLE_PROPS: ReadonlyArray<{ 'children', 'endContent', 'id', - 'isBlock', + 'inlineSize', 'isDisabled', 'isPending', 'onBlur', @@ -609,7 +609,7 @@ const PINNED_VISIBLE_PROPS: ReadonlyArray<{ 'autoFocus', 'children', 'id', - 'isBlock', + 'inlineSize', 'isDisabled', 'isPending', 'onBlur', diff --git a/apps/docs/src/lib/component-props-table-generation.test.ts b/apps/docs/src/lib/component-props-table-generation.test.ts index d04a3cf8..59416ae9 100644 --- a/apps/docs/src/lib/component-props-table-generation.test.ts +++ b/apps/docs/src/lib/component-props-table-generation.test.ts @@ -66,7 +66,7 @@ test('keeps JSDoc descriptions for Button presentation props', async () => { (buttonProps?.entries ?? []).map((entry) => [entry.name, entry] as const), ); - for (const name of ['appearance', 'isBlock', 'prominence', 'size', 'tone'] as const) { + for (const name of ['appearance', 'inlineSize', 'prominence', 'size', 'tone'] as const) { const entry = entriesByName.get(name); expect(entry, `expected ${name} in ButtonProps`).toBeDefined(); expect(entry?.description?.trim().length ?? 0).toBeGreaterThan(0); diff --git a/apps/docs/src/lib/guide-prop-audit-data.ts b/apps/docs/src/lib/guide-prop-audit-data.ts index b0139de9..b842ace9 100644 --- a/apps/docs/src/lib/guide-prop-audit-data.ts +++ b/apps/docs/src/lib/guide-prop-audit-data.ts @@ -13,7 +13,7 @@ export const GUIDE_TAUGHT_PROPS: Readonly>> 'appearance', 'tone', 'prominence', - 'isBlock', + 'inlineSize', 'isDisabled', 'isPending', 'pressAction', @@ -232,7 +232,7 @@ export const GUIDE_TAUGHT_PROPS: Readonly>> 'layout/visually-hidden.mdx::packages/@luke-ui/react/src/core/visually-hidden/visually-hidden.tsx::VisuallyHiddenProps': ['elementType', 'isFocusable', 'ref', 'renderRoot'], 'primitives/button.mdx::packages/@luke-ui/react/src/core/primitives/button/button.tsx::ButtonProps': - ['appearance', 'isBlock', 'isDisabled', 'isPending', 'size', 'tone'], + ['appearance', 'inlineSize', 'isDisabled', 'isPending', 'size', 'tone'], 'primitives/checkbox.mdx::packages/@luke-ui/react/src/core/primitives/checkbox/checkbox.tsx::CheckboxControlProps': [], 'primitives/checkbox.mdx::packages/@luke-ui/react/src/core/primitives/checkbox/checkbox.tsx::CheckboxIndicatorProps': diff --git a/docs/PUBLIC_API.md b/docs/PUBLIC_API.md index 3d4f7b6b..b62d91c2 100644 --- a/docs/PUBLIC_API.md +++ b/docs/PUBLIC_API.md @@ -136,6 +136,12 @@ reference measurements emitted in `rem`. Custom lengths belong in CSS. Box for arbitrary maximum widths. These naming decisions supersede [#761](https://github.com/lukebennett88/luke-ui/issues/761). +### Button inline sizing + +Button and button-shaped Link accept `inlineSize="auto" | "full"`. `auto` is intrinsic and `full` +fills the available inline size. Text appearances do not accept this prop. IconButton and IconLink +do not expose it. Custom widths belong in CSS. + ### State attributes A state attribute is public only when a guide documents it, as the Select primitive guide documents diff --git a/packages/@luke-ui/react/src/core/action-presentation.ts b/packages/@luke-ui/react/src/core/action-presentation.ts index 34598c18..a8cda8cb 100644 --- a/packages/@luke-ui/react/src/core/action-presentation.ts +++ b/packages/@luke-ui/react/src/core/action-presentation.ts @@ -10,10 +10,10 @@ export type ButtonPresentationProps = */ appearance?: 'button'; /** - * Whether the control fills the available inline size. - * @default false + * Use `full` to fill the available inline size. + * @default auto */ - isBlock?: boolean; + inlineSize?: 'auto' | 'full'; /** * Controls the strength of the control's visual treatment, from low to high. * @default standard @@ -36,7 +36,7 @@ export type ButtonPresentationProps = * @default button */ appearance: 'text'; - isBlock?: never; + inlineSize?: never; /** * Controls the strength of the control's visual treatment, from low to high. * @default standard @@ -55,7 +55,7 @@ export type ButtonPresentationProps = * @default button */ appearance: 'text'; - isBlock?: never; + inlineSize?: never; /** * Controls the strength of the control's visual treatment, from low to high. * @default standard @@ -77,7 +77,7 @@ export type LinkPresentationProps = * @default text */ appearance?: 'text'; - isBlock?: never; + inlineSize?: never; /** * Controls the strength of the control's visual treatment, from low to high. * @default standard @@ -92,10 +92,10 @@ export type LinkPresentationProps = */ appearance: 'button'; /** - * Whether the control fills the available inline size. - * @default false + * Use `full` to fill the available inline size. + * @default auto */ - isBlock?: boolean; + inlineSize?: 'auto' | 'full'; /** * Controls the strength of the control's visual treatment, from low to high. * @default standard diff --git a/packages/@luke-ui/react/src/core/button/button.browser.test.tsx b/packages/@luke-ui/react/src/core/button/button.browser.test.tsx index ca999729..17d1d02e 100644 --- a/packages/@luke-ui/react/src/core/button/button.browser.test.tsx +++ b/packages/@luke-ui/react/src/core/button/button.browser.test.tsx @@ -391,3 +391,18 @@ function dispatchPress(element: Element) { ); element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); } + +test('Button preserves intrinsic and full inline sizing', () => { + render( +
+ + +
, + ); + expect( + page.getByRole('button', { name: 'Save auto' }).element().getBoundingClientRect().width, + ).toBeLessThan(320); + expect( + page.getByRole('button', { name: 'Save full' }).element().getBoundingClientRect().width, + ).toBe(320); +}); diff --git a/packages/@luke-ui/react/src/core/button/button.test.ts b/packages/@luke-ui/react/src/core/button/button.test.ts index a5c921e2..1c884fc8 100644 --- a/packages/@luke-ui/react/src/core/button/button.test.ts +++ b/packages/@luke-ui/react/src/core/button/button.test.ts @@ -1,5 +1,5 @@ import { createElement } from 'react'; -import { assertType, test } from 'vite-plus/test'; +import { assertType, expectTypeOf, test } from 'vite-plus/test'; import type { ButtonProps } from './button.js'; test('Button supports ref and rejects unsupported prop combinations', () => { @@ -20,3 +20,11 @@ test('Button supports ref and rejects unsupported prop combinations', () => { tone: 'critical', }); }); + +test('Button accepts only automatic or full inline sizing for button appearances', () => { + expectTypeOf().toEqualTypeOf<'auto' | 'full' | undefined>(); + // @ts-expect-error — custom lengths belong in style + assertType({ appearance: 'button', inlineSize: '20rem' }); + // @ts-expect-error — text appearances do not expose inline sizing + assertType({ appearance: 'text', inlineSize: 'full' }); +}); diff --git a/packages/@luke-ui/react/src/core/button/button.tsx b/packages/@luke-ui/react/src/core/button/button.tsx index a26a0676..13994efc 100644 --- a/packages/@luke-ui/react/src/core/button/button.tsx +++ b/packages/@luke-ui/react/src/core/button/button.tsx @@ -30,7 +30,7 @@ type ButtonContentProps = type _ButtonOmit = DistributiveOmit< PrimitiveButtonProps, | 'appearance' - | 'isBlock' + | 'inlineSize' | 'isPending' | 'prominence' | 'render' @@ -59,7 +59,7 @@ type _ButtonProps = _ButtonOmit & export type ButtonProps = Prettify<_ButtonProps & ButtonPresentationProps & ButtonContentProps>; /** - * Button with appearance, tone, prominence, size, pending, and block options. + * Button with appearance, tone, prominence, size, pending, and inline-size options. * Button appearance truncates its label. Text appearance wraps with surrounding text. Shows a * spinner when pending. */ @@ -68,7 +68,7 @@ export function Button(props: ButtonProps): JSX.Element { appearance = 'button', children, endContent, - isBlock: _isBlock, + inlineSize: _inlineSize, isPending = false, onPress, pressAction, @@ -157,7 +157,7 @@ function getPrimitivePresentationProps(props: ButtonPresentationProps): ButtonPr return { appearance: 'button', - isBlock: props.isBlock, + inlineSize: props.inlineSize, prominence: props.prominence, size: props.size, tone: props.tone, diff --git a/packages/@luke-ui/react/src/core/button/recipe.css.ts b/packages/@luke-ui/react/src/core/button/recipe.css.ts index 814e7c47..2756e574 100644 --- a/packages/@luke-ui/react/src/core/button/recipe.css.ts +++ b/packages/@luke-ui/react/src/core/button/recipe.css.ts @@ -9,7 +9,7 @@ export const buttonRecipe = withDefaultVariants< ButtonPresentationProps & RecipeComposition >(buttonRecipeInternal, { appearance: 'button', - isBlock: false, + inlineSize: 'auto', prominence: 'standard', size: 'medium', tone: 'neutral', diff --git a/packages/@luke-ui/react/src/core/icon-button/icon-button.tsx b/packages/@luke-ui/react/src/core/icon-button/icon-button.tsx index a1fb234d..f044eeaa 100644 --- a/packages/@luke-ui/react/src/core/icon-button/icon-button.tsx +++ b/packages/@luke-ui/react/src/core/icon-button/icon-button.tsx @@ -41,7 +41,7 @@ type _IconButtonOmit = DistributiveOmit< | 'aria-label' | 'aria-labelledby' | 'children' - | 'isBlock' + | 'inlineSize' | 'isPending' | 'prominence' | 'render' diff --git a/packages/@luke-ui/react/src/core/link/link.browser.test.tsx b/packages/@luke-ui/react/src/core/link/link.browser.test.tsx index 41da2b11..c17dbb30 100644 --- a/packages/@luke-ui/react/src/core/link/link.browser.test.tsx +++ b/packages/@luke-ui/react/src/core/link/link.browser.test.tsx @@ -118,3 +118,22 @@ test('forced-colors resting', { tags: ['visual'] }, async () => { await emulateForcedColors('none'); } }); + +test('Link preserves intrinsic and full inline sizing', () => { + render( +
+ + Continue auto + + + Continue full + +
, + ); + expect( + page.getByRole('link', { name: 'Continue auto' }).element().getBoundingClientRect().width, + ).toBeLessThan(320); + expect( + page.getByRole('link', { name: 'Continue full' }).element().getBoundingClientRect().width, + ).toBe(320); +}); diff --git a/packages/@luke-ui/react/src/core/link/link.test.ts b/packages/@luke-ui/react/src/core/link/link.test.ts index 61689a46..20bc4fc9 100644 --- a/packages/@luke-ui/react/src/core/link/link.test.ts +++ b/packages/@luke-ui/react/src/core/link/link.test.ts @@ -1,4 +1,4 @@ -import { assertType, test } from 'vite-plus/test'; +import { assertType, expectTypeOf, test } from 'vite-plus/test'; import type { LinkProps } from './link.js'; test('Link supports ref and rejects unsupported props', () => { @@ -12,3 +12,11 @@ test('Link supports ref and rejects unsupported props', () => { // @ts-expect-error — text Links do not use control sizing assertType({ href: '/settings', size: 'small' }); }); + +test('Link accepts only automatic or full inline sizing for button appearances', () => { + expectTypeOf().toEqualTypeOf<'auto' | 'full' | undefined>(); + // @ts-expect-error — custom lengths belong in style + assertType({ appearance: 'button', inlineSize: '20rem', href: '#' }); + // @ts-expect-error — text appearances do not expose inline sizing + assertType({ appearance: 'text', inlineSize: 'full', href: '#' }); +}); diff --git a/packages/@luke-ui/react/src/core/link/link.tsx b/packages/@luke-ui/react/src/core/link/link.tsx index d5741e17..b36415c6 100644 --- a/packages/@luke-ui/react/src/core/link/link.tsx +++ b/packages/@luke-ui/react/src/core/link/link.tsx @@ -49,7 +49,7 @@ export function Link(props: LinkProps): JSX.Element { prominence = 'standard', children, endContent, - isBlock, + inlineSize, size: sizeProp, startContent, ...restProps @@ -66,7 +66,7 @@ export function Link(props: LinkProps): JSX.Element { return linkRecipe({ appearance: 'button', className: cx(linkCursor, className), - isBlock, + inlineSize, prominence, size, }); diff --git a/packages/@luke-ui/react/src/core/link/recipe.css.ts b/packages/@luke-ui/react/src/core/link/recipe.css.ts index b3e66db6..03643594 100644 --- a/packages/@luke-ui/react/src/core/link/recipe.css.ts +++ b/packages/@luke-ui/react/src/core/link/recipe.css.ts @@ -9,7 +9,7 @@ export const linkRecipe = withDefaultVariants< LinkPresentationProps & RecipeComposition >(buttonRecipeInternal, { appearance: 'text', - isBlock: false, + inlineSize: 'auto', size: 'medium', tone: 'neutral', prominence: 'standard', diff --git a/packages/@luke-ui/react/src/core/packed-consumer/react-fixture.ts b/packages/@luke-ui/react/src/core/packed-consumer/react-fixture.ts index bb989da3..bc21d0fc 100644 --- a/packages/@luke-ui/react/src/core/packed-consumer/react-fixture.ts +++ b/packages/@luke-ui/react/src/core/packed-consumer/react-fixture.ts @@ -413,6 +413,10 @@ export const withoutDisplay: ThemeInput = { export const presetContainer = Content; export const customWidthBox = Content; +export const fullButton = ; +// @ts-expect-error — text actions do not accept inline sizing +export const textButtonWidth = ; + // Box values follow the public token contract. export const retainedBoxValues: Array = [ { backgroundColor: 'surface.field', borderColor: 'controlHover', boxShadow: 'overlay' }, diff --git a/packages/@luke-ui/react/src/core/primitives/button/button.test.ts b/packages/@luke-ui/react/src/core/primitives/button/button.test.ts index 85961715..f6d76435 100644 --- a/packages/@luke-ui/react/src/core/primitives/button/button.test.ts +++ b/packages/@luke-ui/react/src/core/primitives/button/button.test.ts @@ -15,7 +15,7 @@ test('primitive Button supports ref and rejects unsupported prop combinations', // @ts-expect-error — text Buttons do not use control sizing assertType({ appearance: 'text', size: 'small' }); // @ts-expect-error — text Buttons do not use block layout - assertType({ appearance: 'text', isBlock: true }); + assertType({ appearance: 'text', inlineSize: 'full' }); // @ts-expect-error — critical text Buttons do not have high prominence assertType({ appearance: 'text', diff --git a/packages/@luke-ui/react/src/core/primitives/button/button.tsx b/packages/@luke-ui/react/src/core/primitives/button/button.tsx index dfe66a5a..c13f886a 100644 --- a/packages/@luke-ui/react/src/core/primitives/button/button.tsx +++ b/packages/@luke-ui/react/src/core/primitives/button/button.tsx @@ -29,7 +29,7 @@ export function Button(props: ButtonProps): JSX.Element { tone = 'neutral', prominence = 'standard', children, - isBlock = false, + inlineSize = 'auto', isDisabled = false, isPending = false, size: sizeProp, @@ -45,7 +45,7 @@ export function Button(props: ButtonProps): JSX.Element { return buttonRecipeInternal({ appearance, className, - isBlock, + inlineSize, prominence, size, tone, diff --git a/packages/@luke-ui/react/src/core/primitives/button/recipe.css.ts b/packages/@luke-ui/react/src/core/primitives/button/recipe.css.ts index 203f8878..0a73bb08 100644 --- a/packages/@luke-ui/react/src/core/primitives/button/recipe.css.ts +++ b/packages/@luke-ui/react/src/core/primitives/button/recipe.css.ts @@ -23,7 +23,7 @@ export const buttonRecipeInternal = recipe({ }, defaultVariants: { appearance: 'button', - isBlock: false, + inlineSize: 'auto', prominence: 'standard', size: 'medium', tone: 'neutral', @@ -122,9 +122,9 @@ export const buttonRecipeInternal = recipe({ }, ], }, - isBlock: { - false: {}, - true: { inlineSize: '100%' }, + inlineSize: { + auto: {}, + full: { inlineSize: '100%' }, }, prominence: { low: {},