From b4418fc15cb65ae158d5316e06a7af1dea411e57 Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Wed, 15 Jul 2026 16:15:11 +1000 Subject: [PATCH] Rebuild styling entrypoints and add Box (#119) --- README.md | 3 +- .../components/feedback/loading-spinner.mdx | 2 +- .../content/docs/components/layout/box.mdx | 37 + .../content/docs/components/layout/meta.json | 4 + apps/docs/content/docs/components/meta.json | 2 +- .../src/examples/box/responsive-layout.tsx | 17 + docs/STYLING.md | 73 +- knip.config.ts | 1 - .../@luke-ui/rainbow-sprinkles/src/types.ts | 13 +- packages/@luke-ui/react/AGENTS.md | 28 + packages/@luke-ui/react/README.md | 8 +- .../react/icons/{critical.svg => danger.svg} | 0 .../react/icons/{informative.svg => info.svg} | 0 .../react/icons/{caution.svg => warning.svg} | 0 packages/@luke-ui/react/package.json | 6 +- .../react/scripts/generate-color-tokens.ts | 173 ---- .../react/scripts/visual-regression-lib.ts | 7 +- .../react/src/box/box.browser.test.tsx | 79 ++ .../@luke-ui/react/src/box/box.stories.tsx | 62 ++ packages/@luke-ui/react/src/box/box.test.ts | 28 + packages/@luke-ui/react/src/box/index.tsx | 46 + .../react/src/button/button.stories.tsx | 4 +- .../react/src/button/button.visual.test.tsx | 8 +- packages/@luke-ui/react/src/button/index.tsx | 15 +- .../react/src/button/primitive/index.tsx | 6 +- .../react/src/combobox-field/index.tsx | 25 +- .../combobox-field/primitive/clear-button.tsx | 15 +- .../src/combobox-field/primitive/control.tsx | 12 +- .../combobox-field/primitive/empty-state.tsx | 11 +- .../src/combobox-field/primitive/input.tsx | 15 +- .../src/combobox-field/primitive/item.tsx | 30 +- .../src/combobox-field/primitive/listbox.tsx | 19 +- .../src/combobox-field/primitive/popover.tsx | 13 +- .../src/combobox-field/primitive/root.tsx | 19 +- .../src/combobox-field/primitive/section.tsx | 19 +- .../src/combobox-field/primitive/trigger.tsx | 12 +- packages/@luke-ui/react/src/emoji/index.tsx | 16 +- .../@luke-ui/react/src/field/compose-field.ts | 11 +- .../react/src/field/primitive/description.tsx | 9 +- .../react/src/field/primitive/error.tsx | 8 +- .../react/src/field/primitive/index.tsx | 11 +- .../react/src/field/primitive/label.tsx | 6 +- packages/@luke-ui/react/src/heading/index.tsx | 11 +- .../@luke-ui/react/src/icon-button/index.tsx | 18 +- .../react/src/icon-size-context/index.tsx | 8 +- .../@luke-ui/react/src/icon/icon.stories.tsx | 2 +- .../react/src/icon/icon.visual.test.tsx | 2 +- packages/@luke-ui/react/src/icon/index.tsx | 26 +- packages/@luke-ui/react/src/link/index.tsx | 12 +- .../react/src/loading-skeleton/index.tsx | 15 +- .../react/src/loading-spinner/index.tsx | 40 +- packages/@luke-ui/react/src/numeral/index.tsx | 31 +- .../react/src/recipes/button-composed.css.ts | 7 +- packages/@luke-ui/react/src/recipes/index.ts | 2 +- .../react/src/sizing/button-sizing.ts | 4 +- .../react/src/sizing/combobox-sizing.ts | 4 +- .../@luke-ui/react/src/styles/index.css.ts | 2 +- .../react/src/styles/package-exports.test.ts | 12 + .../@luke-ui/react/src/styles/reset.css.ts | 4 +- .../react/src/styles/stylesheet-size.test.ts | 16 + .../react/src/styles/theme-root.css.ts | 10 + .../src/styles/utilities.browser.test.ts | 96 ++ .../react/src/styles/utilities.css.ts | 217 ++-- .../react/src/styles/utilities.stories.tsx | 75 +- .../react/src/styles/utilities.test.ts | 81 ++ .../@luke-ui/react/src/styles/vars.css.ts | 131 --- .../@luke-ui/react/src/text-field/index.tsx | 22 +- .../react/src/text-field/primitive/index.tsx | 18 +- packages/@luke-ui/react/src/text/index.tsx | 8 +- .../@luke-ui/react/src/text/text.stories.tsx | 18 +- packages/@luke-ui/react/src/theme.css.ts | 3 - .../react/src/theme/build-theme.test.ts | 52 +- .../@luke-ui/react/src/theme/build-theme.ts | 38 +- packages/@luke-ui/react/src/theme/index.tsx | 12 +- .../react/src/theme/tokens.stories.tsx | 970 ++++++++++++++++++ .../@luke-ui/react/src/tokens/converters.ts | 110 -- packages/@luke-ui/react/src/tokens/groups.ts | 26 - packages/@luke-ui/react/src/tokens/index.ts | 131 --- .../react/src/tokens/media-queries.ts | 14 - .../react/src/tokens/tokens-vars.stories.tsx | 953 ----------------- packages/@luke-ui/react/src/tokens/values.ts | 117 --- .../@luke-ui/react/src/types/icon-size.ts | 2 + packages/@luke-ui/react/src/types/prettify.ts | 6 + .../react/src/types/public-props.test.ts | 47 + packages/@luke-ui/react/vitest.config.ts | 7 +- pnpm-lock.yaml | 11 - pnpm-workspace.yaml | 1 - 87 files changed, 2160 insertions(+), 2105 deletions(-) create mode 100644 apps/docs/content/docs/components/layout/box.mdx create mode 100644 apps/docs/content/docs/components/layout/meta.json create mode 100644 apps/docs/src/examples/box/responsive-layout.tsx rename packages/@luke-ui/react/icons/{critical.svg => danger.svg} (100%) rename packages/@luke-ui/react/icons/{informative.svg => info.svg} (100%) rename packages/@luke-ui/react/icons/{caution.svg => warning.svg} (100%) delete mode 100644 packages/@luke-ui/react/scripts/generate-color-tokens.ts create mode 100644 packages/@luke-ui/react/src/box/box.browser.test.tsx create mode 100644 packages/@luke-ui/react/src/box/box.stories.tsx create mode 100644 packages/@luke-ui/react/src/box/box.test.ts create mode 100644 packages/@luke-ui/react/src/box/index.tsx create mode 100644 packages/@luke-ui/react/src/styles/package-exports.test.ts create mode 100644 packages/@luke-ui/react/src/styles/stylesheet-size.test.ts create mode 100644 packages/@luke-ui/react/src/styles/theme-root.css.ts create mode 100644 packages/@luke-ui/react/src/styles/utilities.browser.test.ts create mode 100644 packages/@luke-ui/react/src/styles/utilities.test.ts delete mode 100644 packages/@luke-ui/react/src/styles/vars.css.ts delete mode 100644 packages/@luke-ui/react/src/theme.css.ts create mode 100644 packages/@luke-ui/react/src/theme/tokens.stories.tsx delete mode 100644 packages/@luke-ui/react/src/tokens/converters.ts delete mode 100644 packages/@luke-ui/react/src/tokens/groups.ts delete mode 100644 packages/@luke-ui/react/src/tokens/index.ts delete mode 100644 packages/@luke-ui/react/src/tokens/media-queries.ts delete mode 100644 packages/@luke-ui/react/src/tokens/tokens-vars.stories.tsx delete mode 100644 packages/@luke-ui/react/src/tokens/values.ts create mode 100644 packages/@luke-ui/react/src/types/icon-size.ts create mode 100644 packages/@luke-ui/react/src/types/prettify.ts create mode 100644 packages/@luke-ui/react/src/types/public-props.test.ts diff --git a/README.md b/README.md index 264e3e35..975225df 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,7 @@ Useful repo commands: `@luke-ui/react` contains the public React package. -- Tokens: `packages/@luke-ui/react/src/tokens.ts`. -- Theme: `packages/@luke-ui/react/src/theme/`. +- Theme contract and compiler: `packages/@luke-ui/react/src/theme/`. - Styles: `packages/@luke-ui/react/src/styles/`. - Build output: `packages/@luke-ui/react/dist/stylesheet.css`. diff --git a/apps/docs/content/docs/components/feedback/loading-spinner.mdx b/apps/docs/content/docs/components/feedback/loading-spinner.mdx index 60a80bbc..d67101f9 100644 --- a/apps/docs/content/docs/components/feedback/loading-spinner.mdx +++ b/apps/docs/content/docs/components/feedback/loading-spinner.mdx @@ -38,7 +38,7 @@ Omit `value` for indeterminate progress. Pass `value` for determinate progress. ## Accessibility diff --git a/apps/docs/content/docs/components/layout/box.mdx b/apps/docs/content/docs/components/layout/box.mdx new file mode 100644 index 00000000..1a782945 --- /dev/null +++ b/apps/docs/content/docs/components/layout/box.mdx @@ -0,0 +1,37 @@ +--- +title: Box +description: Responsive layout container backed by Luke UI Sprinkles. +--- + +`Box` is a `div` by default. Use its responsive layout, spacing, sizing, positioning, overflow, +flex, and grid-child props to build layout without adding styling props to other components. + + + +Spacing props use `0` or the semantic space steps `100`, `200`, `300`, `400`, `600`, `800`, `1000`, +`1200`, and `1600`. Responsive objects use the `xsmall`, `small`, `medium`, `large`, `xlarge`, and +`xxlarge` breakpoints. + +## Custom div component + +Use `render` with a compatible custom `div` component, such as a motion or presentational wrapper. +Spread the provided props onto the actual `div` so the generated class, inline variables, ref, +accessibility attributes, and event handlers are preserved. The callback does not change Box's DOM +element contract to another element type. + +```tsx + }> + Account summary + +``` + +Semantic colour and typography are deliberately not Box props. Use component APIs or public semantic +variables from `@luke-ui/react/theme` for sanctioned custom styling. + +## Props + + diff --git a/apps/docs/content/docs/components/layout/meta.json b/apps/docs/content/docs/components/layout/meta.json new file mode 100644 index 00000000..9fae5c85 --- /dev/null +++ b/apps/docs/content/docs/components/layout/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Layout", + "pages": ["box"] +} diff --git a/apps/docs/content/docs/components/meta.json b/apps/docs/content/docs/components/meta.json index e0920e66..1849f159 100644 --- a/apps/docs/content/docs/components/meta.json +++ b/apps/docs/content/docs/components/meta.json @@ -1,4 +1,4 @@ { "title": "Components", - "pages": ["actions", "feedback", "forms", "typography", "visuals", "primitives"] + "pages": ["actions", "feedback", "forms", "layout", "typography", "visuals", "primitives"] } diff --git a/apps/docs/src/examples/box/responsive-layout.tsx b/apps/docs/src/examples/box/responsive-layout.tsx new file mode 100644 index 00000000..d596f93d --- /dev/null +++ b/apps/docs/src/examples/box/responsive-layout.tsx @@ -0,0 +1,17 @@ +import { Box } from '@luke-ui/react/box'; +import { vars } from '@luke-ui/react/theme'; + +export default function ResponsiveLayout() { + return ( + + First item + Second item + + ); +} diff --git a/docs/STYLING.md b/docs/STYLING.md index 003c544b..08171fb0 100644 --- a/docs/STYLING.md +++ b/docs/STYLING.md @@ -8,9 +8,8 @@ stylesheet and apply its identity class to the same element. Neither step inject ## Structure -- `tokens.ts`: design token source. -- `styles/vars.css.ts`: theme variables. - `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`. - `styles/`: public layout utilities exported from `@luke-ui/react/styles`. - `theme/contract.ts`: the semantic token tree and its `--luke-*` variable naming. @@ -114,8 +113,9 @@ bundle smaller as the token scale grows. The tradeoff is that some values are applied through inline `style`, which raises specificity. That is acceptable because styling utilities are already the highest-priority escape hatch. -Do not add a polymorphic `Box` component. React Aria Components' `render` prop covers the same need -without adding another component API. +`Box` from `@luke-ui/react/box` applies the same utilities to a `div`. Its `render` prop can use a +compatible custom `div` component while preserving the generated class, style, ref, and DOM props. +It does not provide `as` or `asChild` polymorphism. Do not add style props to every component. Component props should stay focused on component-specific variants and behaviour. @@ -129,8 +129,8 @@ import { createSprinkles } from '@luke-ui/react/styles'; const layout = createSprinkles({ display: 'flex', - gap: 'medium', - padding: 'large', + gap: '400', + padding: '600', }); return ( @@ -140,9 +140,10 @@ return ( ); ``` -Token-backed properties use the design-token scale, for example `padding: 'large'`. Enum-like -properties use CSS-native values, for example `display: 'flex'`. Numeric flex properties use string -values, for example `flexGrow: '1'`. +Spacing and gap properties use `0` or the semantic space steps `100`, `200`, `300`, `400`, `600`, +`800`, `1000`, `1200`, and `1600`. Margin also accepts `auto`. Enum-like properties use CSS-native +values, for example `display: 'flex'`. Sizing, inset, flex-basis, order, and grid-placement values +accept their CSS property values. ## Responsive values @@ -153,24 +154,12 @@ only overrides need to be specified. const responsive = createSprinkles({ display: 'flex', flexDirection: { xsmall: 'column', medium: 'row' }, - gap: { xsmall: 'small', medium: 'large' }, + gap: { xsmall: '300', medium: '600' }, }); ``` -## Pseudo-state conditions - -Use condition objects for `hover` and `focus-visible` states. - -```tsx -const interactive = createSprinkles({ - padding: 'medium', - backgroundColor: { - default: 'neutral', - hover: 'neutralHover', - focusVisible: 'input', - }, -}); -``` +The retained breakpoints are `xsmall` (base), `small` (640px), `medium` (768px), `large` (1024px), +`xlarge` (1280px), and `xxlarge` (1536px). ## React Aria `render` prop @@ -181,7 +170,7 @@ Aria Components' `render` prop. Use `mergeProps` from `@luke-ui/react/utils` so ```tsx import { mergeProps } from '@luke-ui/react/utils'; -const buttonBox = createSprinkles({ padding: 'medium' }); +const buttonBox = createSprinkles({ padding: '400' }); `, - ) + .map(([status, count]) => { + return ``; + }) .join(''); const namespaces = [...new Set(results.map((result) => getNamespace(result.id)))].sort(); const namespaceOptions = namespaces diff --git a/packages/@luke-ui/react/src/box/box.browser.test.tsx b/packages/@luke-ui/react/src/box/box.browser.test.tsx new file mode 100644 index 00000000..944c00a8 --- /dev/null +++ b/packages/@luke-ui/react/src/box/box.browser.test.tsx @@ -0,0 +1,79 @@ +import '../../dist/themes/machined-edge.css'; +import '../stylesheet.css.js'; +import { act, createRef } from 'react'; +import type { Root } from 'react-dom/client'; +import { createRoot } from 'react-dom/client'; +import { afterEach, expect, test } from 'vite-plus/test'; +import { page } from 'vite-plus/test/context'; +import { themeRootClassName } from '../theme/index.js'; +import { machinedEdgeThemeClassName } from '../themes/index.js'; +import { Box } from './index.js'; + +const mounted: Array<{ container: HTMLElement; root: Root }> = []; + +afterEach(async () => { + for (const { container, root } of mounted) { + act(() => root.unmount()); + container.remove(); + } + mounted.length = 0; + await page.viewport(1024, 800); +}); + +test('renders a responsive layout at the retained breakpoints', async () => { + const container = document.body.appendChild(document.createElement('div')); + container.className = `${themeRootClassName} ${machinedEdgeThemeClassName}`; + const root = createRoot(container); + mounted.push({ container, root }); + + act(() => { + root.render( + + First item + Second item + , + ); + }); + + const box = container.firstElementChild; + if (!(box instanceof HTMLElement)) throw new Error('Expected Box element.'); + + await page.viewport(640, 800); + expect(getComputedStyle(box).flexDirection).toBe('column'); + expect(getComputedStyle(box).gap).toBe('8px'); + + await page.viewport(768, 800); + expect(getComputedStyle(box).flexDirection).toBe('row'); + expect(getComputedStyle(box).gap).toBe('24px'); +}); + +test('forwards the ref through a custom rendered div', () => { + const container = document.body.appendChild(document.createElement('div')); + const root = createRoot(container); + const ref = createRef(); + mounted.push({ container, root }); + + act(() => { + root.render( +
} + > + Custom div + , + ); + }); + + const div = container.firstElementChild; + if (!(div instanceof HTMLDivElement)) throw new Error('Expected custom rendered div.'); + + expect(ref.current).toBe(div); + expect(div).toHaveAttribute('data-motion', 'enabled'); + expect(div).toHaveAttribute('id', 'custom-div'); + expect(div).toHaveTextContent('Custom div'); +}); diff --git a/packages/@luke-ui/react/src/box/box.stories.tsx b/packages/@luke-ui/react/src/box/box.stories.tsx new file mode 100644 index 00000000..791ef987 --- /dev/null +++ b/packages/@luke-ui/react/src/box/box.stories.tsx @@ -0,0 +1,62 @@ +import type { BoxProps } from '@luke-ui/react/box'; +import { Box } from '@luke-ui/react/box'; +import { vars } from '@luke-ui/react/theme'; +import type { ComponentPropsWithRef } from 'react'; +import { expect } from 'storybook/test'; +import preview from '../../.storybook/preview.js'; + +const meta = preview.meta({ + component: Box, + tags: ['layout'], + title: 'Layout/Box', +}); + +/** Use Box for responsive layout without attaching layout props to semantic components. */ +export const Default = meta.story({ + args: { + children: ( + <> + First item + Second item + + ), + display: 'flex', + flexDirection: { medium: 'row', xsmall: 'column' }, + gap: { medium: '600', xsmall: '200' }, + padding: { medium: '600', xsmall: '300' }, + style: { backgroundColor: vars.color.surface.recessed }, + } satisfies Partial, + play: async ({ canvas }) => { + const box = canvas.getByText('First item').parentElement; + if (!box) throw new Error('Expected Box parent.'); + + await expect(getComputedStyle(box).display).toBe('flex'); + await expect(getComputedStyle(box).flexDirection).toBe('row'); + await expect(getComputedStyle(box).gap).toBe('24px'); + }, +}); + +/** Use `render` with a compatible custom div while preserving generated and consumer props. */ +export const CustomDiv = meta.story({ + args: { + 'aria-label': 'Account summary', + children: 'Account summary content', + className: 'consumer-class', + id: 'account-summary', + padding: '400', + render: (domProps) => , + style: { backgroundColor: vars.color.surface.resting }, + } satisfies Partial, + play: async ({ canvas }) => { + const div = canvas.getByText('Account summary content'); + await expect(div).toHaveClass('consumer-class'); + await expect(div).toHaveAttribute('data-motion', 'enabled'); + await expect(div).toHaveAttribute('id', 'account-summary'); + await expect(getComputedStyle(div).padding).toBe('16px'); + await expect(getComputedStyle(div).backgroundColor).not.toBe('rgba(0, 0, 0, 0)'); + }, +}); + +function MotionDiv(props: ComponentPropsWithRef<'div'>) { + return
; +} diff --git a/packages/@luke-ui/react/src/box/box.test.ts b/packages/@luke-ui/react/src/box/box.test.ts new file mode 100644 index 00000000..b7c46476 --- /dev/null +++ b/packages/@luke-ui/react/src/box/box.test.ts @@ -0,0 +1,28 @@ +import { createElement, createRef } from 'react'; +import { expectTypeOf, test } from 'vite-plus/test'; +import type { BoxProps } from './index.js'; + +const boxProps = { + 'aria-label': 'Account summary', + display: { medium: 'flex', xsmall: 'block' }, + id: 'account-summary', + onClick: () => undefined, + padding: '400', + ref: createRef(), + render: (domProps, renderProps) => { + expectTypeOf(renderProps).toEqualTypeOf(); + + return createElement('div', domProps); + }, +} satisfies BoxProps; + +// Type assertions are compile-time only. +// oxlint-disable-next-line vitest/expect-expect +test('preserves native DOM, render, and responsive layout props', () => { + expectTypeOf(boxProps).toExtend(); + expectTypeOf(boxProps.render).toExtend(); + expectTypeOf(boxProps.display).toEqualTypeOf<{ + medium: 'flex'; + xsmall: 'block'; + }>(); +}); diff --git a/packages/@luke-ui/react/src/box/index.tsx b/packages/@luke-ui/react/src/box/index.tsx new file mode 100644 index 00000000..aaed1668 --- /dev/null +++ b/packages/@luke-ui/react/src/box/index.tsx @@ -0,0 +1,46 @@ +import type { ComponentPropsWithRef, JSX, ReactElement } from 'react'; +import type { SprinklesProps } from '../styles/index.js'; +import { createSprinkles } from '../styles/index.js'; +import type { DistributiveOmit } from '../types/distributive-omit.js'; +import type { Prettify } from '../types/prettify.js'; +import { mergeProps } from '../utils/index.js'; + +type BoxRender = (props: ComponentPropsWithRef<'div'>, renderProps: undefined) => ReactElement; + +interface _BoxProps extends ComponentPropsWithRef<'div'>, SprinklesProps { + /** Renders a compatible custom `div` while carrying Box's DOM props and generated styles. */ + render?: BoxRender; +} + +/** + * Props for `Box`. Layout props accept responsive values keyed by Luke UI breakpoints. + * + * @tier atom + */ +export type BoxProps = Prettify<_BoxProps>; + +/** A layout container backed by Luke UI Sprinkles. */ +export function Box(props: BoxProps): JSX.Element { + const { className, render, style, ...restProps } = props; + const [sprinklesProps, elementProps] = splitProps(restProps); + const domProps = mergeProps(elementProps, createSprinkles(sprinklesProps)); + const mergedDomProps = mergeProps(domProps, { className, style }); + + return render ? render(mergedDomProps, undefined) :
; +} + +function splitProps( + props: DistributiveOmit, +): [SprinklesProps, ComponentPropsWithRef<'div'>] { + const sprinklesProps: Record = {}; + const elementProps: Record = {}; + + for (const [property, value] of Object.entries(props)) { + const target = createSprinkles.properties.has(property as keyof SprinklesProps) + ? sprinklesProps + : elementProps; + target[property] = value; + } + + return [sprinklesProps as SprinklesProps, elementProps as ComponentPropsWithRef<'div'>]; +} diff --git a/packages/@luke-ui/react/src/button/button.stories.tsx b/packages/@luke-ui/react/src/button/button.stories.tsx index 4f13f912..a498ab9b 100644 --- a/packages/@luke-ui/react/src/button/button.stories.tsx +++ b/packages/@luke-ui/react/src/button/button.stories.tsx @@ -4,7 +4,7 @@ import { Icon } from '@luke-ui/react/icon'; import type { CSSProperties } from 'react'; import { expect, fn, userEvent } from 'storybook/test'; import preview from '../../.storybook/preview.js'; -import { vars } from '../theme.css.js'; +import { vars } from '../theme/index.js'; const meta = preview.meta({ component: Button, @@ -43,7 +43,7 @@ const blockContainerStyle = { } as const satisfies CSSProperties; const truncationContainerStyle = { - borderColor: vars.border.default, + borderColor: vars.color.border.decorative, borderStyle: 'dashed', borderWidth: 1, inlineSize: '100%', diff --git a/packages/@luke-ui/react/src/button/button.visual.test.tsx b/packages/@luke-ui/react/src/button/button.visual.test.tsx index 10be97bb..3a70c8be 100644 --- a/packages/@luke-ui/react/src/button/button.visual.test.tsx +++ b/packages/@luke-ui/react/src/button/button.visual.test.tsx @@ -20,8 +20,8 @@ test('tones and appearances across sizes', async () => { const locator = renderVisual( {sizes.flatMap((size) => { - return tones.flatMap((tone) => - appearances.map((appearance) => ( + return tones.flatMap((tone) => { + return appearances.map((appearance) => ( - )), - ); + )); + }); })} , ); diff --git a/packages/@luke-ui/react/src/button/index.tsx b/packages/@luke-ui/react/src/button/index.tsx index 85714e15..01ea160d 100644 --- a/packages/@luke-ui/react/src/button/index.tsx +++ b/packages/@luke-ui/react/src/button/index.tsx @@ -3,7 +3,9 @@ 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 { 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'; @@ -47,16 +49,19 @@ interface ButtonStyleProps { tone?: PrimitiveButtonRecipeProps['tone']; } +type _ButtonOmit = DistributiveOmit< + PrimitiveButtonProps, + 'appearance' | 'isBlock' | 'isPending' | 'size' | 'tone' | keyof DocumentedPressProps +>; + +interface _ButtonProps extends _ButtonOmit, ButtonStyleProps, DocumentedPressProps {} + /** * Composed button with size, tone, appearance, pending, and block options. * * @tier composed */ -export interface ButtonProps - extends - Omit, - ButtonStyleProps, - DocumentedPressProps {} +export type ButtonProps = Prettify<_ButtonProps>; /** Composed button. Wraps children in a `Text` for ellipsis truncation. Shows a spinner when `isPending`. */ export function Button(props: ButtonProps): JSX.Element { diff --git a/packages/@luke-ui/react/src/button/primitive/index.tsx b/packages/@luke-ui/react/src/button/primitive/index.tsx index 0c08dd48..35e6aff2 100644 --- a/packages/@luke-ui/react/src/button/primitive/index.tsx +++ b/packages/@luke-ui/react/src/button/primitive/index.tsx @@ -5,6 +5,7 @@ import { composeRenderProps } from 'react-aria-components/composeRenderProps'; import { IconSizeProvider } from '../../icon-size-context/index.js'; import * as styles from '../../recipes/button.css.js'; import { BUTTON_ICON_SIZE } from '../../sizing/button-sizing.js'; +import type { Prettify } from '../../types/prettify.js'; import { cx } from '../../utils/index.js'; interface ButtonRecipeProps extends NonNullable {} @@ -32,14 +33,15 @@ interface ButtonStyleProps { tone?: ButtonRecipeProps['tone']; } +interface _ButtonProps extends RacButtonProps, ButtonStyleProps {} + /** * Primitive button — a bare `