···33- Do not hand-edit `.generated/entries.ts` or `package.json` exports; entries are generated, tsdown updates exports at build.
44- When adding a component, use `pnpm generate:component` from repo root (not manual file creation) so the group barrel, styles index, and docs are updated correctly.
55- Stories (`*.stories.tsx`) are the tests; there are no separate `*.test.tsx` files.
66+- React Compiler is enabled — do not use `useCallback` or `useMemo`; the compiler auto-memoizes.
···11import type { ComponentProps, JSX } from 'react';
22-import type { DistributiveOmit } from '../types/index.js';
22+import type { DistributiveOmit } from '../types/distributive-omit.js';
33import { cx } from '../utils/index.js';
44import * as styles from '../recipes/{{kebabCase name}}.css.js';
55
+1-1
packages/@luke-ui/react/src/button/index.tsx
···11import type { JSX } from 'react';
22import type { ButtonProps as PrimitiveButtonProps } from '../button/primitive.js';
33import { Button as PrimitiveButton } from '../button/primitive.js';
44-import { BUTTON_FONT_SIZE, BUTTON_ICON_SIZE } from '../lib/button.js';
54import { LoadingSpinner } from '../loading-spinner/index.js';
65import * as styles from '../recipes/button-composed.css.js';
76import type * as primitiveStyles from '../recipes/button.css.js';
77+import { BUTTON_FONT_SIZE, BUTTON_ICON_SIZE } from '../sizing/button-sizing.js';
88import { Text } from '../text/index.js';
991010interface ComposedButtonVariantProps extends NonNullable<styles.ButtonLabelVariants> {}
+1-1
packages/@luke-ui/react/src/button/primitive.tsx
···33import { Button as RacButton } from 'react-aria-components/Button';
44import { composeRenderProps } from 'react-aria-components/composeRenderProps';
55import { IconSizeProvider } from '../icon-size-context/index.js';
66-import { BUTTON_ICON_SIZE } from '../lib/button.js';
76import * as styles from '../recipes/button.css.js';
77+import { BUTTON_ICON_SIZE } from '../sizing/button-sizing.js';
88import { cx } from '../utils/index.js';
991010interface ButtonVariantProps extends NonNullable<styles.ButtonVariants> {}
···11+import type { JSX, ReactNode } from 'react';
22+import { Collection } from 'react-aria-components/Collection';
33+import type { ListBoxProps as RacListBoxProps } from 'react-aria-components/ComboBox';
44+import { ListBox as RacListBox } from 'react-aria-components/ComboBox';
55+import { ListBoxContext } from 'react-aria-components/ListBox';
66+import { composeRenderProps } from 'react-aria-components/composeRenderProps';
77+import { useSlottedContext } from 'react-aria-components/slots';
88+import * as styles from '../recipes/combobox.css.js';
99+import type { DistributiveOmit } from '../types/distributive-omit.js';
1010+import { cx } from '../utils/index.js';
1111+1212+/** Props for the styled listbox. */
1313+export interface ComboboxListBoxProps<T extends object> extends DistributiveOmit<
1414+ RacListBoxProps<T>,
1515+ 'dependencies' | 'items'
1616+> {
1717+ /** Item content for the listbox (render prop or static children). */
1818+ children?: RacListBoxProps<T>['children'];
1919+ /** Values that should invalidate the dynamic item cache. */
2020+ dependencies?: ReadonlyArray<unknown>;
2121+ /** Dynamic items rendered by the `children` render prop. */
2222+ items?: Iterable<T>;
2323+ /** Optional content appended after the main collection, e.g. a load-more sentinel. */
2424+ loadMoreItem?: ReactNode;
2525+}
2626+2727+/** Styled listbox for combobox options. */
2828+export function ComboboxListBox<T extends object>(props: ComboboxListBoxProps<T>): JSX.Element {
2929+ const { children, dependencies, items, loadMoreItem, ...listBoxProps } = props;
3030+ const listBoxContext = useSlottedContext(ListBoxContext);
3131+ const collectionItems = items ?? listBoxContext?.items;
3232+ const listBoxChildren =
3333+ typeof children === 'function' ? (
3434+ <Collection<T> dependencies={dependencies} items={collectionItems}>
3535+ {children}
3636+ </Collection>
3737+ ) : (
3838+ children
3939+ );
4040+4141+ return (
4242+ <RacListBox
4343+ {...listBoxProps}
4444+ className={composeRenderProps(listBoxProps.className, (className) => {
4545+ return cx(styles.comboboxListBox(), className);
4646+ })}
4747+ >
4848+ {listBoxChildren}
4949+ {loadMoreItem}
5050+ </RacListBox>
5151+ );
5252+}
+22
packages/@luke-ui/react/src/combobox/popover.tsx
···11+import type { JSX } from 'react';
22+import type { PopoverProps as RacPopoverProps } from 'react-aria-components/ComboBox';
33+import { Popover as RacPopover } from 'react-aria-components/ComboBox';
44+import { composeRenderProps } from 'react-aria-components/composeRenderProps';
55+import * as styles from '../recipes/combobox.css.js';
66+import { themeRootClassName } from '../theme/index.js';
77+import { cx } from '../utils/index.js';
88+99+/** Props for the styled combobox popover. */
1010+export interface ComboboxPopoverProps extends Omit<RacPopoverProps, 'UNSTABLE_portalContainer'> {}
1111+1212+/** Popover surface used for listbox content. */
1313+export function ComboboxPopover(props: ComboboxPopoverProps): JSX.Element {
1414+ return (
1515+ <RacPopover
1616+ {...props}
1717+ className={composeRenderProps(props.className, (className) => {
1818+ return cx(themeRootClassName, styles.comboboxPopover(), className);
1919+ })}
2020+ />
2121+ );
2222+}
+55
packages/@luke-ui/react/src/combobox/root.tsx
···11+import type { JSX } from 'react';
22+import type { ComboBoxProps as RacComboBoxProps, Key } from 'react-aria-components/ComboBox';
33+import { ComboBox as RacComboBox } from 'react-aria-components/ComboBox';
44+import { composeRenderProps } from 'react-aria-components/composeRenderProps';
55+import * as styles from '../recipes/combobox.css.js';
66+import type { DistributiveOmit } from '../types/distributive-omit.js';
77+import { cx } from '../utils/index.js';
88+99+interface ComboboxVariantProps extends NonNullable<styles.ComboboxVariants> {}
1010+1111+export type ComboboxSize = NonNullable<ComboboxVariantProps['size']>;
1212+1313+export interface ComboboxInputProps<T extends object> extends DistributiveOmit<
1414+ RacComboBoxProps<T, 'single'>,
1515+ | 'defaultSelectedKey'
1616+ | 'defaultValue'
1717+ | 'onChange'
1818+ | 'onOpenChange'
1919+ | 'onSelectionChange'
2020+ | 'selectedKey'
2121+ | 'selectionMode'
2222+ | 'value'
2323+> {
2424+ /** The currently selected key (controlled). Pass `null` for no selection. */
2525+ value?: Key | null;
2626+2727+ /** The initially selected key (uncontrolled). */
2828+ defaultValue?: Key | null;
2929+3030+ /** Called when the selected value changes. */
3131+ onChange?: (value: Key | null) => void;
3232+3333+ /** Called when the open state changes. */
3434+ onOpenChange?: (isOpen: boolean) => void;
3535+3636+ /**
3737+ * The interaction required to display the ComboBox menu.
3838+ * @default 'focus'
3939+ */
4040+ menuTrigger?: 'focus' | 'input' | 'manual';
4141+}
4242+4343+export function ComboboxInput<T extends object>(props: ComboboxInputProps<T>): JSX.Element {
4444+ const { className, menuTrigger = 'focus', ...comboboxProps } = props;
4545+4646+ return (
4747+ <RacComboBox
4848+ {...comboboxProps}
4949+ menuTrigger={menuTrigger}
5050+ className={composeRenderProps(className, (renderedClassName) => {
5151+ return cx(styles.comboboxRoot, renderedClassName);
5252+ })}
5353+ />
5454+ );
5555+}
+38
packages/@luke-ui/react/src/combobox/section.tsx
···11+import type { JSX, ReactNode } from 'react';
22+import type { ListBoxSectionProps as RacListBoxSectionProps } from 'react-aria-components/ComboBox';
33+import { ListBoxSection as RacListBoxSection } from 'react-aria-components/ComboBox';
44+import { Header as RacHeader } from 'react-aria-components/Header';
55+import * as styles from '../recipes/combobox.css.js';
66+import type { DistributiveOmit } from '../types/distributive-omit.js';
77+import { cx } from '../utils/index.js';
88+99+export interface ComboboxSectionProps<T extends object> extends DistributiveOmit<
1010+ RacListBoxSectionProps<T>,
1111+ 'className'
1212+> {
1313+ className?: RacListBoxSectionProps<T>['className'];
1414+ title?: ReactNode;
1515+}
1616+1717+export function ComboboxSection<T extends object>(props: ComboboxSectionProps<T>): JSX.Element {
1818+ const { children, className, title, ...sectionProps } = props;
1919+2020+ const sectionClassName = cx(styles.comboboxSection(), className);
2121+2222+ if (typeof children === 'function') {
2323+ return (
2424+ <RacListBoxSection {...sectionProps} className={sectionClassName}>
2525+ {children}
2626+ </RacListBoxSection>
2727+ );
2828+ }
2929+3030+ return (
3131+ <RacListBoxSection {...sectionProps} className={sectionClassName}>
3232+ {title != null ? (
3333+ <RacHeader className={styles.comboboxSectionHeading}>{title}</RacHeader>
3434+ ) : null}
3535+ {children}
3636+ </RacListBoxSection>
3737+ );
3838+}
+33
packages/@luke-ui/react/src/combobox/trigger.tsx
···11+import type { JSX } from 'react';
22+import type { ButtonProps as RacButtonProps } from 'react-aria-components/ComboBox';
33+import { Button as RacButton } from 'react-aria-components/ComboBox';
44+import { composeRenderProps } from 'react-aria-components/composeRenderProps';
55+import * as styles from '../recipes/combobox.css.js';
66+import type { DistributiveOmit } from '../types/distributive-omit.js';
77+import { cx } from '../utils/index.js';
88+99+interface ComboboxVariantProps extends NonNullable<styles.ComboboxVariants> {}
1010+1111+interface ComboboxStyleProps {
1212+ size?: ComboboxVariantProps['size'];
1313+}
1414+1515+/** Props for the combobox trigger button. */
1616+export interface ComboboxTriggerProps
1717+ extends DistributiveOmit<RacButtonProps, 'className'>, ComboboxStyleProps {
1818+ className?: RacButtonProps['className'];
1919+}
2020+2121+/** Trigger button used by combobox pattern. */
2222+export function ComboboxTrigger(props: ComboboxTriggerProps): JSX.Element {
2323+ const { size = 'medium', ...buttonProps } = props;
2424+2525+ return (
2626+ <RacButton
2727+ {...buttonProps}
2828+ className={composeRenderProps(buttonProps.className, (className) => {
2929+ return cx(styles.comboboxTrigger({ size }), className);
3030+ })}
3131+ />
3232+ );
3333+}
+1-1
packages/@luke-ui/react/src/emoji/index.tsx
···11import type { TextProps } from '../text/index.js';
22import { Text } from '../text/index.js';
33-import type { DistributiveOmit } from '../types/index.js';
33+import type { DistributiveOmit } from '../types/distributive-omit.js';
4455/** Props for `Emoji`. */
66export interface EmojiProps extends DistributiveOmit<TextProps, 'children' | 'elementType'> {
···11-import type { JSX } from 'react';
22-import type { TextProps as RacTextProps } from 'react-aria-components/Text';
33-import { Text as RacText } from 'react-aria-components/Text';
44-import * as styles from '../recipes/field.css.js';
55-import { cx } from '../utils/index.js';
66-77-interface FieldMessageVariantProps extends NonNullable<styles.FieldMessageVariants> {}
88-99-/** Props for `FieldDescription`. */
1010-export interface FieldDescriptionProps
1111- extends Omit<RacTextProps, 'slot'>, Omit<FieldMessageVariantProps, 'tone'> {}
1212-1313-/** Styled helper text shown under a field. */
1414-export function FieldDescription(props: FieldDescriptionProps): JSX.Element {
1515- const { className, ...restProps } = props;
1616-1717- return (
1818- <RacText
1919- {...restProps}
2020- className={cx(styles.fieldMessage({ tone: 'description' }), className)}
2121- slot="description"
2222- />
2323- );
2424-}
-24
packages/@luke-ui/react/src/field-error/index.tsx
···11-import type { JSX } from 'react';
22-import type { FieldErrorProps as RacFieldErrorProps } from 'react-aria-components/FieldError';
33-import { FieldError as RacFieldError } from 'react-aria-components/FieldError';
44-import { composeRenderProps } from 'react-aria-components/composeRenderProps';
55-import * as styles from '../recipes/field.css.js';
66-import { cx } from '../utils/index.js';
77-88-interface FieldMessageVariantProps extends NonNullable<styles.FieldMessageVariants> {}
99-1010-/** Props for `FieldError`. */
1111-export interface FieldErrorProps
1212- extends RacFieldErrorProps, Omit<FieldMessageVariantProps, 'tone'> {}
1313-1414-/** Styled validation message for a field. */
1515-export function FieldError(props: FieldErrorProps): JSX.Element {
1616- return (
1717- <RacFieldError
1818- {...props}
1919- className={composeRenderProps(props.className, (className) => {
2020- return cx(styles.fieldMessage({ tone: 'error' }), className);
2121- })}
2222- />
2323- );
2424-}
-28
packages/@luke-ui/react/src/field-label/index.tsx
···11-import type { JSX } from 'react';
22-import type { LabelProps as RacLabelProps } from 'react-aria-components/Label';
33-import { Label as RacLabel } from 'react-aria-components/Label';
44-import * as styles from '../recipes/field.css.js';
55-import { cx } from '../utils/index.js';
66-77-interface FieldLabelVariantProps extends NonNullable<styles.FieldLabelVariants> {}
88-99-/** Allowed `necessityIndicator` values for `FieldLabel`. */
1010-export type FieldNecessityIndicator = NonNullable<FieldLabelVariantProps['necessityIndicator']>;
1111-1212-interface FieldLabelStyleProps {
1313- /** Shows how required fields are marked. */
1414- necessityIndicator?: FieldNecessityIndicator;
1515-}
1616-1717-/** Props for `FieldLabel`. */
1818-export interface FieldLabelProps
1919- extends Omit<RacLabelProps, keyof FieldLabelStyleProps>, FieldLabelStyleProps {}
2020-2121-/** Styled label for form fields. */
2222-export function FieldLabel(props: FieldLabelProps): JSX.Element {
2323- const { className, necessityIndicator = 'icon', ...restProps } = props;
2424-2525- return (
2626- <RacLabel {...restProps} className={cx(styles.fieldLabel({ necessityIndicator }), className)} />
2727- );
2828-}
+24
packages/@luke-ui/react/src/field/description.tsx
···11+import type { JSX } from 'react';
22+import type { TextProps as RacTextProps } from 'react-aria-components/Text';
33+import { Text as RacText } from 'react-aria-components/Text';
44+import * as styles from '../recipes/field.css.js';
55+import { cx } from '../utils/index.js';
66+77+interface FieldMessageVariantProps extends NonNullable<styles.FieldMessageVariants> {}
88+99+/** Props for `FieldDescription`. */
1010+export interface FieldDescriptionProps
1111+ extends Omit<RacTextProps, 'slot'>, Omit<FieldMessageVariantProps, 'tone'> {}
1212+1313+/** Styled helper text shown under a field. */
1414+export function FieldDescription(props: FieldDescriptionProps): JSX.Element {
1515+ const { className, ...restProps } = props;
1616+1717+ return (
1818+ <RacText
1919+ {...restProps}
2020+ className={cx(styles.fieldMessage({ tone: 'description' }), className)}
2121+ slot="description"
2222+ />
2323+ );
2424+}
+24
packages/@luke-ui/react/src/field/error.tsx
···11+import type { JSX } from 'react';
22+import type { FieldErrorProps as RacFieldErrorProps } from 'react-aria-components/FieldError';
33+import { FieldError as RacFieldError } from 'react-aria-components/FieldError';
44+import { composeRenderProps } from 'react-aria-components/composeRenderProps';
55+import * as styles from '../recipes/field.css.js';
66+import { cx } from '../utils/index.js';
77+88+interface FieldMessageVariantProps extends NonNullable<styles.FieldMessageVariants> {}
99+1010+/** Props for `FieldError`. */
1111+export interface FieldErrorProps
1212+ extends RacFieldErrorProps, Omit<FieldMessageVariantProps, 'tone'> {}
1313+1414+/** Styled validation message for a field. */
1515+export function FieldError(props: FieldErrorProps): JSX.Element {
1616+ return (
1717+ <RacFieldError
1818+ {...props}
1919+ className={composeRenderProps(props.className, (className) => {
2020+ return cx(styles.fieldMessage({ tone: 'error' }), className);
2121+ })}
2222+ />
2323+ );
2424+}
+10-6
packages/@luke-ui/react/src/field/index.tsx
···11import type { ComponentProps, JSX, ReactNode } from 'react';
22-import { FieldDescription } from '../field-description/index.js';
33-import type { FieldErrorProps } from '../field-error/index.js';
44-import { FieldError } from '../field-error/index.js';
55-import type { FieldNecessityIndicator } from '../field-label/index.js';
66-import { FieldLabel } from '../field-label/index.js';
77-import { Field as PrimitiveField } from '../field/primitive.js';
22+import { FieldDescription } from './description.js';
33+import type { FieldDescriptionProps } from './description.js';
44+import type { FieldErrorProps } from './error.js';
55+import { FieldError } from './error.js';
66+import { FieldLabel } from './label.js';
77+import type { FieldLabelProps, FieldNecessityIndicator } from './label.js';
88+import { Field as PrimitiveField } from './primitive.js';
99+1010+export { FieldDescription, FieldError, FieldLabel };
1111+export type { FieldDescriptionProps, FieldErrorProps, FieldLabelProps, FieldNecessityIndicator };
812913type PrimitiveFieldProps = ComponentProps<typeof PrimitiveField>;
1014
+28
packages/@luke-ui/react/src/field/label.tsx
···11+import type { JSX } from 'react';
22+import type { LabelProps as RacLabelProps } from 'react-aria-components/Label';
33+import { Label as RacLabel } from 'react-aria-components/Label';
44+import * as styles from '../recipes/field.css.js';
55+import { cx } from '../utils/index.js';
66+77+interface FieldLabelVariantProps extends NonNullable<styles.FieldLabelVariants> {}
88+99+/** Allowed `necessityIndicator` values for `FieldLabel`. */
1010+export type FieldNecessityIndicator = NonNullable<FieldLabelVariantProps['necessityIndicator']>;
1111+1212+interface FieldLabelStyleProps {
1313+ /** Shows how required fields are marked. */
1414+ necessityIndicator?: FieldNecessityIndicator;
1515+}
1616+1717+/** Props for `FieldLabel`. */
1818+export interface FieldLabelProps
1919+ extends Omit<RacLabelProps, keyof FieldLabelStyleProps>, FieldLabelStyleProps {}
2020+2121+/** Styled label for form fields. */
2222+export function FieldLabel(props: FieldLabelProps): JSX.Element {
2323+ const { className, necessityIndicator = 'icon', ...restProps } = props;
2424+2525+ return (
2626+ <RacLabel {...restProps} className={cx(styles.fieldLabel({ necessityIndicator }), className)} />
2727+ );
2828+}
+1-1
packages/@luke-ui/react/src/heading/index.tsx
···22import { HeadingLevels, HeadingPresenceProvider } from '../heading-context/index.js';
33import type { TextProps } from '../text/index.js';
44import { Text } from '../text/index.js';
55-import type { DistributiveOmit } from '../types/index.js';
55+import type { DistributiveOmit } from '../types/distributive-omit.js';
6677export type { HeadingLevel } from '../heading-context/index.js';
88/** Valid heading tag name for Luke UI headings. */
+2-2
packages/@luke-ui/react/src/icon/index.tsx
···22import { createContext, useContext } from 'react';
33import { iconNames, iconViewBoxes } from '../../.generated/icon-data.js';
44import { useIconSizeContext } from '../icon-size-context/index.js';
55-import { ICON_VIEWBOX } from '../lib/icon.js';
65import * as styles from '../recipes/icon.css.js';
77-import type { DistributiveOmit } from '../types/index.js';
66+import { ICON_VIEWBOX } from '../sizing/icon-sizing.js';
77+import type { DistributiveOmit } from '../types/distributive-omit.js';
88import { cx } from '../utils/index.js';
991010export type { IconName } from '../../.generated/icon-data.js';
-13
packages/@luke-ui/react/src/lib/button.ts
···11-import type { FontSizeToken, IconSizeToken } from '../tokens/index.js';
22-33-/** Button size → icon/spinner size so icons align with control size. */
44-export const BUTTON_ICON_SIZE = {
55- medium: 'medium',
66- small: 'xsmall',
77-} as const satisfies Record<'medium' | 'small', IconSizeToken>;
88-99-/** Button size → Text fontSize for composed button label. */
1010-export const BUTTON_FONT_SIZE = {
1111- medium: 'standard',
1212- small: 'small',
1313-} as const satisfies Record<'medium' | 'small', FontSizeToken>;
-16
packages/@luke-ui/react/src/lib/icon.ts
···11-/** Shared viewBox size for icon SVGs (icons and LoadingSpinner). */
22-export const ICON_VIEWBOX_SIZE = 24;
33-44-/** Inset from viewBox edge to content; icons use 19×19 content in 24×24 box. */
55-const ICON_CONTENT_INSET = 2.5;
66-77-/** Content area size (viewBox size minus insets). */
88-const ICON_CONTENT_SIZE = ICON_VIEWBOX_SIZE - 2 * ICON_CONTENT_INSET;
99-1010-/** Default viewBox string for icon-aligned SVGs. */
1111-export const ICON_VIEWBOX = `0 0 ${ICON_VIEWBOX_SIZE} ${ICON_VIEWBOX_SIZE}`;
1212-1313-export const SPINNER_STROKE_WIDTH = 2;
1414-1515-/** Radius inset by half stroke so the stroke's outer edge aligns with icon content (19px). */
1616-export const SPINNER_CIRCLE_RADIUS = (ICON_CONTENT_SIZE - SPINNER_STROKE_WIDTH) / 2;
···11import { clamp } from '@react-aria/utils';
22import type { ComponentProps } from 'react';
33+import * as styles from '../recipes/loading-spinner.css.js';
34import {
45 ICON_VIEWBOX,
56 ICON_VIEWBOX_SIZE,
67 SPINNER_CIRCLE_RADIUS,
78 SPINNER_STROKE_WIDTH,
88-} from '../lib/icon.js';
99-import * as styles from '../recipes/loading-spinner.css.js';
1010-import type { DistributiveOmit } from '../types/index.js';
99+} from '../sizing/icon-sizing.js';
1010+import type { DistributiveOmit } from '../types/distributive-omit.js';
1111import { cx } from '../utils/index.js';
12121313interface LoadingSpinnerVariantProps extends NonNullable<styles.LoadingSpinnerVariants> {}
···11import type { GlobalStyleRule, StyleRule } from '@vanilla-extract/css';
22import { globalStyle as vanillaGlobalStyle, style as vanillaStyle } from '@vanilla-extract/css';
33import { recipe as vanillaRecipe } from '@vanilla-extract/recipes';
44-import type { DistributiveOmit } from '../types/index.js';
44+import type { DistributiveOmit } from '../types/distributive-omit.js';
55import type { LayerName } from './layers.css.js';
66import { layers } from './layers.css.js';
77
···11import { createTheme, createThemeContract } from '@vanilla-extract/css';
22-import type { ColorTokenValue } from '../tokens/index.js';
32import {
43 colorToCssString,
54 cubicBezierToString,
65 dimensionToRemString,
76 durationToString,
88- tokenKeys,
99- tokens,
1010-} from '../tokens/index.js';
77+} from '../tokens/converters.js';
88+import { tokenKeys } from '../tokens/groups.js';
99+import type { ColorTokenValue } from '../tokens/index.js';
1010+import { tokens } from '../tokens/index.js';
1111import { classSelector, lukeUiClassNames } from './class-names.js';
1212import { globalStyleInLayer } from './layered-style.css.js';
1313import { layers } from './layers.css.js';
+2-2
packages/@luke-ui/react/src/text-field/index.tsx
···44 TextFieldProps as RacTextFieldProps,
55} from 'react-aria-components/TextField';
66import { TextField as RacTextField } from 'react-aria-components/TextField';
77-import type { FieldErrorProps } from '../field-error/index.js';
88-import type { FieldNecessityIndicator } from '../field-label/index.js';
77+import type { FieldErrorProps } from '../field/error.js';
98import { Field } from '../field/index.js';
99+import type { FieldNecessityIndicator } from '../field/label.js';
1010import type { TextInputSize } from '../text-input/index.js';
1111import { TextInput } from '../text-input/index.js';
1212
+50
packages/@luke-ui/react/src/tokens/converters.ts
···11+import { pxToRem } from '../utils/index.js';
22+import type {
33+ ColorTokenValue,
44+ CubicBezierTokenValue,
55+ DimensionTokenValue,
66+ DurationTokenValue,
77+} from './index.js';
88+99+export function dimensionToRemString(value: DimensionTokenValue, base: number = 16): string {
1010+ return value.unit === 'rem' ? `${value.value}rem` : pxToRem(value.value, base);
1111+}
1212+1313+export function dimensionToPxNumber(value: DimensionTokenValue, base: number = 16): number {
1414+ return value.unit === 'px' ? value.value : value.value * base;
1515+}
1616+1717+export function durationToString(value: DurationTokenValue): string {
1818+ return `${value.value}${value.unit}`;
1919+}
2020+2121+export function cubicBezierToString(value: CubicBezierTokenValue): string {
2222+ return `cubic-bezier(${value[0]}, ${value[1]}, ${value[2]}, ${value[3]})`;
2323+}
2424+2525+const functionLikeColorSpaces = new Set(['hsl', 'hwb', 'lab', 'lch', 'oklab', 'oklch']);
2626+2727+export function colorToCssString(value: ColorTokenValue): string {
2828+ if (value.components.length === 0) {
2929+ throw new Error(`Color token "${value.colorSpace}" has no components`);
3030+ }
3131+3232+ const components = value.components.map((component) => formatNumber(component));
3333+ const alphaSuffix =
3434+ value.alpha === undefined || value.alpha === 1 ? '' : ` / ${formatNumber(value.alpha)}`;
3535+3636+ if (functionLikeColorSpaces.has(value.colorSpace)) {
3737+ return `${value.colorSpace}(${components.join(' ')}${alphaSuffix})`;
3838+ }
3939+4040+ return `color(${value.colorSpace} ${components.join(' ')}${alphaSuffix})`;
4141+}
4242+4343+function formatNumber(value: number): string {
4444+ if (!Number.isFinite(value)) {
4545+ throw new Error(`Invalid numeric color component: ${value}`);
4646+ }
4747+4848+ const normalized = Object.is(value, -0) ? 0 : value;
4949+ return `${normalized}`;
5050+}
+24
packages/@luke-ui/react/src/tokens/groups.ts
···11+import type { DesignTokenGroup, TokenName } from './index.js';
22+33+export function toTokenGroup<TType extends string, TValues extends Record<string, unknown>>(
44+ type: TType,
55+ values: TValues,
66+): DesignTokenGroup<TType, TValues> {
77+ const group = { $type: type } as DesignTokenGroup<TType, TValues>;
88+99+ for (const key in values) {
1010+ (group as Record<string, unknown>)[key] = {
1111+ $value: values[key],
1212+ };
1313+ }
1414+1515+ return group;
1616+}
1717+1818+export function tokenKeys<TGroup extends { $type: string }>(
1919+ group: TGroup,
2020+): Array<TokenName<TGroup>> {
2121+ return (Object.keys(group) as Array<keyof TGroup>).filter(
2222+ (key): key is TokenName<TGroup> => key !== '$type',
2323+ );
2424+}
···11+type KeysOfUnion<T> = T extends T ? keyof T : never;
22+33+export type DistributiveOmit<T, K extends KeysOfUnion<T>> = T extends any
44+ ? Omit<T, Extract<K, keyof T>>
55+ : never;
-5
packages/@luke-ui/react/src/types/index.ts
···11-type KeysOfUnion<T> = T extends T ? keyof T : never;
22-33-export type DistributiveOmit<T, K extends KeysOfUnion<T>> = T extends any
44- ? Omit<T, Extract<K, keyof T>>
55- : never;