@maestro-js/components 1.0.0-alpha.23 → 1.0.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- [{"name":"tw","files":[{"name":"utils/tw.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add tw --directory app\n\nimport { twMerge } from 'tailwind-merge'\nimport { composeRenderProps } from 'react-aria-components'\n\nexport function tw(...inputs: unknown[]) {\n return twMerge(inputs.flat(Infinity).filter(Boolean).join(' '))\n}\n\nexport function composeTailwindRenderProps<T>(\n className: string | ((renderProps: T) => string) | undefined,\n tailwind: string\n): string | ((renderProps: T) => string) {\n return composeRenderProps(className, (prev) => tw(tailwind, prev))\n}\n"}]},{"name":"compose-refs","files":[{"name":"utils/compose-refs.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add compose-refs --directory app\n\nimport React from 'react'\n\ntype PossibleRef<T> = React.Ref<T> | undefined\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n if (typeof ref === 'function') {\n ref(value)\n } else if (ref !== null && ref !== undefined) {\n ;(ref as React.MutableRefObject<T>).current = value\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nexport function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n return (node: T) => refs.forEach((ref) => setRef(ref, node))\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nexport function useComposedRefs<T>(...refs: PossibleRef<T>[]) {\n // eslint-disable-next-line react-hooks/exhaustive-deps\n return React.useCallback(composeRefs(...refs), refs)\n}\n"}]},{"name":"use-render-props","files":[{"name":"utils/use-render-props.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-render-props --directory app\n\nimport React from 'react'\n\ntype Prettify<T> = { [K in keyof T]: T[K] } & {}\n\ntype DefaultRenderPropKeys<Props> = Extract<'children' | 'className' | 'style', keyof Props>\n\ntype MaybeRenderProp<Value, RenderProps> = Value | ((renderProps: RenderProps) => Value)\n\nexport type WithRenderProps<\n Props extends Record<string, any>,\n RenderProps,\n Keys extends keyof Props = DefaultRenderPropKeys<Props>\n> = Prettify<{\n [K in keyof Props]: K extends Keys ? MaybeRenderProp<Props[K], RenderProps> : Props[K]\n}>\n\nexport function useRenderProps<\n Props extends Record<string, any>,\n RenderProps,\n Keys extends keyof Props = DefaultRenderPropKeys<Props>\n>(props: Props, renderProps: RenderProps, keys: Keys[]) {\n const withRenderProps = React.useMemo(\n () =>\n Object.fromEntries(\n keys\n .map((key) => {\n if (key in props && typeof props[key] === 'function') {\n return [key, props[key](renderProps)]\n } else {\n return []\n }\n })\n .filter((k) => k.length)\n ),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [...keys, renderProps, props, ...keys.map((key) => props[key])]\n )\n return { ...props, ...withRenderProps }\n}\n"}]},{"name":"colors","files":[{"name":"utils/colors.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add colors --directory app\n\nexport const COLORS = [\n '#f43f5e',\n '#3b82f6',\n '#22c55e',\n '#EAB308',\n '#8b5cf6',\n '#191970',\n '#f97316',\n '#6b7280',\n '#ec4899',\n '#06b6d4',\n '#84cc16',\n '#64748b',\n '#ffff00',\n '#6366f1',\n '#ff6347',\n '#737373',\n '#14b8a6',\n '#f59e0b',\n '#0ea5e9',\n '#ef4444',\n '#78716c',\n '#00ffff',\n '#d946ef',\n '#10b981',\n '#b22222',\n '#40e0d0',\n '#71717a',\n '#a855f7',\n '#7fff00'\n] as const\n\nexport function stringToNumber(str: string) {\n let h = 0x811c9dc5 // FNV offset basis\n for (const b of new TextEncoder().encode(str)) {\n h ^= b\n h = Math.imul(h, 0x01000193) // 16777619\n }\n return h >>> 0 // unsigned 32-bit\n}\n\nexport function getColor(seed: number) {\n return COLORS[seed % COLORS.length]\n}\n"}]},{"name":"file-input-utils","files":[{"name":"utils/file-input.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add file-input-utils --directory app\n\n/**\n * Shared utilities for file-input components.\n */\n\nexport function formatFileSize(bytes: number) {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n\nexport function checkFileType(file: File, accept: string): string | null {\n const acceptedTypes = accept.split(',').map((type) => type.trim())\n const fileType = file.type\n const fileExtension = `.${file.name.split('.').pop()?.toLowerCase() || ''}`\n\n const isValid = acceptedTypes.some((acceptType) => {\n if (acceptType.includes('/*')) {\n const prefix = acceptType.split('/')[0]\n return fileType.startsWith(`${prefix}/`)\n }\n if (acceptType.startsWith('.')) {\n return acceptType.toLowerCase() === fileExtension\n }\n return fileType === acceptType\n })\n\n return isValid ? null : `Unsupported file type. Supported types: ${acceptedTypes.join(', ')}`\n}\n\nexport function checkFileMaxSize(file: File, maxSize: number): string | null {\n if (file.size > maxSize) {\n return `File exceeded max size of ${formatFileSize(maxSize)}`\n }\n return null\n}\n\nexport function validateFileMaxSize(value: string | File | null, maxSize?: number): string | null {\n if (value instanceof File && maxSize) {\n return checkFileMaxSize(value, maxSize)\n }\n return null\n}\n\nexport function validateFileType(value: string | File | null, accept?: string): string | null {\n if (value instanceof File && accept) {\n return checkFileType(value, accept)\n }\n return null\n}\n"}]},{"name":"use-spin-delay","files":[{"name":"utils/use-spin-delay.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-spin-delay --directory app\n\nimport React from 'react'\n\nexport function useSpinDelay(loading: boolean, { delay = 500, minDuration = 200 } = {}) {\n const [show, setShow] = React.useState(false)\n const delayTimer = React.useRef<ReturnType<typeof setTimeout>>(undefined)\n const minTimer = React.useRef<ReturnType<typeof setTimeout>>(undefined)\n\n React.useEffect(() => {\n if (loading) {\n delayTimer.current = setTimeout(() => setShow(true), delay)\n } else {\n clearTimeout(delayTimer.current)\n if (show) {\n minTimer.current = setTimeout(() => setShow(false), minDuration)\n }\n }\n return () => {\n clearTimeout(delayTimer.current)\n clearTimeout(minTimer.current)\n }\n }, [loading, delay, minDuration, show])\n\n return show\n}\n"}]},{"name":"use-number-input","files":[{"name":"utils/use-number-input.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-number-input --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\n\nexport type UseNumberInputOptions = {\n value?: number | null\n defaultValue?: number | null\n onChange?: ((value: number | null) => void) | null\n minValue?: number\n maxValue?: number\n step?: number\n formatOptions?: Intl.NumberFormatOptions\n locale?: string\n isDisabled?: boolean\n isReadOnly?: boolean\n}\n\nexport type UseNumberInputResult = {\n inputValue: string\n setInputValue: (value: string) => void\n numberValue: number | null\n setNumberValue: (value: number | null) => void\n minValue?: number\n maxValue?: number\n commit: (rawValue?: string) => void\n}\n\nfunction formatNumber(value: number | null | undefined, locale: string, formatOptions?: Intl.NumberFormatOptions): string {\n if (value == null) return ''\n try {\n return new Intl.NumberFormat(locale, formatOptions).format(value)\n } catch {\n return String(value)\n }\n}\n\nfunction parseNumber(text: string, locale: string, formatOptions?: Intl.NumberFormatOptions): number | null {\n if (text === '' || text == null) return null\n\n // Get the decimal and group separators for the locale\n const parts = new Intl.NumberFormat(locale, formatOptions).formatToParts(1234.5)\n const groupSeparator = parts.find((p) => p.type === 'group')?.value ?? ','\n const decimalSeparator = parts.find((p) => p.type === 'decimal')?.value ?? '.'\n\n // Strip everything except digits, decimal separator, and minus sign\n let cleaned = text\n // Remove currency symbols, percent signs, and other literals\n for (const part of parts) {\n if (part.type === 'currency' || part.type === 'percentSign' || part.type === 'literal') {\n cleaned = cleaned.replaceAll(part.value, '')\n }\n }\n // Remove group separators\n cleaned = cleaned.replaceAll(groupSeparator, '')\n // Normalize decimal separator to '.'\n if (decimalSeparator !== '.') {\n cleaned = cleaned.replaceAll(decimalSeparator, '.')\n }\n\n cleaned = cleaned.trim()\n if (cleaned === '' || cleaned === '-') return null\n\n const num = Number(cleaned)\n\n // For percent format, Intl.NumberFormat displays 0.5 as \"50%\",\n // so we need to divide by 100 when parsing\n if (formatOptions?.style === 'percent') {\n return isNaN(num) ? null : num / 100\n }\n\n return isNaN(num) ? null : num\n}\n\nfunction clamp(value: number, min?: number, max?: number): number {\n if (min != null && value < min) return min\n if (max != null && value > max) return max\n return value\n}\n\nexport function useNumberInput(options: UseNumberInputOptions): UseNumberInputResult {\n const { minValue, maxValue, step, formatOptions, locale = 'en-US', isDisabled = false, isReadOnly = false } = options\n\n const [numberValue, setNumberValue] = HeadlessForm.useControlledState<number | null>(\n options.value !== undefined ? (options.value ?? null) : undefined!,\n options.defaultValue !== undefined ? (options.defaultValue ?? null) : null,\n options.onChange ?? undefined\n )\n\n const [inputValue, setInputValueRaw] = React.useState(() => formatNumber(numberValue, locale, formatOptions))\n\n // Keep inputValue in sync when numberValue changes externally (controlled mode)\n const prevNumberValueRef = React.useRef(numberValue)\n React.useEffect(() => {\n if (prevNumberValueRef.current !== numberValue) {\n prevNumberValueRef.current = numberValue\n setInputValueRaw(formatNumber(numberValue, locale, formatOptions))\n }\n }, [numberValue, locale, formatOptions])\n\n const setInputValue = React.useCallback(\n (text: string) => {\n if (isDisabled || isReadOnly) return\n setInputValueRaw(text)\n },\n [isDisabled, isReadOnly]\n )\n\n const commit = React.useCallback(\n (rawValue?: string) => {\n if (isDisabled || isReadOnly) return\n\n const parsed = parseNumber(rawValue ?? inputValue, locale, formatOptions)\n if (parsed == null) {\n setNumberValue(null)\n setInputValueRaw('')\n return\n }\n\n let clamped = clamp(parsed, minValue, maxValue)\n\n // Snap to step if provided\n if (step != null && minValue != null) {\n const remainder = (clamped - minValue) % step\n if (remainder !== 0) {\n clamped = clamped - remainder\n }\n }\n\n setNumberValue(clamped)\n setInputValueRaw(formatNumber(clamped, locale, formatOptions))\n },\n [inputValue, locale, formatOptions, minValue, maxValue, step, isDisabled, isReadOnly, setNumberValue]\n )\n\n return {\n inputValue,\n setInputValue,\n numberValue,\n setNumberValue,\n minValue,\n maxValue,\n commit\n }\n}\n"}]},{"name":"use-prevent-default","dependencies":["use-stable-accessor"],"files":[{"name":"utils/use-prevent-default.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-prevent-default --directory app\n\nimport React from 'react'\nimport { useStableAccessor } from './use-stable-accessor'\n\nexport function usePreventDefault<E extends React.SyntheticEvent<any>>(\n eventHandler: React.EventHandler<E> | undefined,\n preventDefault: boolean\n): React.EventHandler<E> {\n const getShouldPreventDefault = useStableAccessor(preventDefault)\n const getEventHandler = useStableAccessor(eventHandler)\n\n const myEventHandler = React.useCallback(\n (event: E) => {\n if (getShouldPreventDefault()) {\n event.preventDefault()\n event.stopPropagation()\n return\n } else {\n const eventHandler = getEventHandler()\n if (eventHandler) {\n return eventHandler(event)\n }\n }\n },\n [getEventHandler, getShouldPreventDefault]\n )\n return myEventHandler\n}\n"}]},{"name":"use-stable-accessor","files":[{"name":"utils/use-stable-accessor.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-stable-accessor --directory app\n\nimport React from 'react'\n\nexport function useStableAccessor<T>(value: T) {\n const initialPersisterKey = React.useRef<{ value: T }>({ value })\n const getValue = React.useCallback(() => {\n return initialPersisterKey.current.value\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [initialPersisterKey.current])\n initialPersisterKey.current.value = value\n return getValue\n}\n"}]},{"name":"use-tab-indicator","files":[{"name":"utils/use-tab-indicator.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-tab-indicator --directory app\n\nimport * as React from 'react'\n\n// --- Shared Tab Styles ---\n\nexport const TAB_BASE_CLASSES = 'relative px-4 py-2 text-sm cursor-default transition-colors duration-200'\nexport const TAB_INACTIVE_CLASSES = 'text-neutral-600 font-medium'\nexport const TAB_DISABLED_CLASSES = 'opacity-50 cursor-not-allowed'\n\n// --- Tab Variant ---\n\nexport type TabVariant = 'underline' | 'oval'\n\n// --- Tab Color Map ---\n\nexport const tabColorMap = {\n primary: {\n text: 'text-primary-600',\n bg: 'bg-primary-600',\n outline: 'outline-primary-600'\n },\n neutral: {\n text: 'text-neutral-800',\n bg: 'bg-neutral-800',\n outline: 'outline-neutral-800'\n }\n} as const\n\nexport type TabColor = keyof typeof tabColorMap\n\n// --- Variant-Specific Styles ---\n\nexport const tabListContainerClasses: Record<TabVariant, string> = {\n underline: 'relative',\n oval: 'relative inline-flex bg-neutral-100 rounded-lg p-1'\n}\n\nexport const tabListInnerClasses: Record<TabVariant, string> = {\n underline: 'flex space-x-1 border-b border-neutral-200',\n oval: 'flex space-x-1'\n}\n\nexport function getIndicatorClasses(variant: TabVariant, color: TabColor): string {\n if (variant === 'underline') {\n return `absolute bottom-0 h-0.5 rounded-full ${tabColorMap[color].bg}`\n }\n return 'absolute inset-y-1 rounded-md bg-white shadow-sm'\n}\n\nexport const TAB_OVAL_BASE_CLASSES =\n 'relative z-10 px-4 py-1.5 text-sm cursor-default transition-colors duration-200 rounded-md'\nexport const TAB_OVAL_SELECTED_CLASSES = 'text-neutral-900 font-semibold'\n\n// --- Animated Indicator Hook ---\n\nexport function useAnimatedIndicator(\n containerRef: React.RefObject<HTMLDivElement | null>,\n activeSelector: string,\n observedAttribute: string\n) {\n const indicatorRef = React.useRef<HTMLDivElement>(null)\n const hasAnimated = React.useRef(false)\n\n const updateIndicator = React.useCallback(() => {\n const container = containerRef.current\n const indicator = indicatorRef.current\n if (!container || !indicator) return\n\n const activeTab = container.querySelector(activeSelector) as HTMLElement | null\n if (activeTab) {\n const containerRect = container.getBoundingClientRect()\n const tabRect = activeTab.getBoundingClientRect()\n\n if (!hasAnimated.current) {\n indicator.style.transition = 'none'\n hasAnimated.current = true\n } else {\n indicator.style.transition = 'left 200ms ease, width 200ms ease'\n }\n\n indicator.style.left = `${tabRect.left - containerRect.left}px`\n indicator.style.width = `${tabRect.width}px`\n indicator.style.opacity = '1'\n } else {\n indicator.style.opacity = '0'\n }\n }, [containerRef, activeSelector])\n\n React.useEffect(() => {\n updateIndicator()\n\n const container = containerRef.current\n if (!container) return\n\n const observer = new MutationObserver(updateIndicator)\n observer.observe(container, { attributes: true, subtree: true, attributeFilter: [observedAttribute] })\n\n const resizeObserver = new ResizeObserver(updateIndicator)\n resizeObserver.observe(container)\n\n return () => {\n observer.disconnect()\n resizeObserver.disconnect()\n }\n }, [containerRef, updateIndicator, observedAttribute])\n\n return indicatorRef\n}\n"}]},{"name":"form-field","dependencies":["tw"],"files":[{"name":"components/helpers/form-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add form-field --directory app\n\nimport * as React from 'react'\nimport {\n Button,\n Collection,\n Group,\n Header,\n Input,\n ListBox,\n ListBoxItem,\n ListBoxSection,\n type InputProps\n} from 'react-aria-components'\nimport { composeTailwindRenderProps, tw } from '../../utils/tw'\nimport { Icon } from '../icon'\n\nconst colorClasses = { neutral: 'text-neutral-500', negative: 'text-negative-600', notice: 'text-notice-600' } as const\n\ntype LabelProps = {\n as?: React.ElementType<React.ComponentPropsWithRef<'label'>>\n} & React.ComponentPropsWithRef<'label'>\n\nconst Label = React.forwardRef<HTMLLabelElement, LabelProps>(({ as = 'label', ...props }, ref) => {\n return React.createElement(as, {\n ...props,\n ref,\n className: tw('block text-sm font-medium text-neutral-700', props.className)\n })\n})\nLabel.displayName = 'Label'\n\nfunction FormFieldHelpText(props: {\n id: string\n isInvalid: boolean\n validationMessage: React.ReactNode\n hasWarning: boolean\n warningMessage?: React.ReactNode\n helpText?: React.ReactNode\n UNSAFE_className?: string\n}) {\n const text = props.isInvalid ? props.validationMessage : props.hasWarning ? props.warningMessage : props.helpText\n const color = props.isInvalid ? 'negative' : props.hasWarning ? 'notice' : 'neutral'\n return text === null ? null : (\n <div\n id={props.id}\n className={tw('mt-1 text-sm', !text && 'h-5', colorClasses[color], 'text-wrap', props.UNSAFE_className)}\n >\n {text ?? ''}\n </div>\n )\n}\n\nfunction LabelRow(\n props: { label?: React.ReactNode; cornerHint?: React.ReactNode } & ({ labelId: string } | { htmlFor: string })\n): React.JSX.Element | null {\n if (!props.label) return null\n return (\n <div className=\"flex justify-between items-center mb-1\">\n {'labelId' in props ? (\n <Label id={props.labelId}>{props.label}</Label>\n ) : (\n <Label htmlFor={props.htmlFor}>{props.label}</Label>\n )}\n {props.cornerHint && <div className=\"text-sm text-neutral-500\">{props.cornerHint}</div>}\n </div>\n )\n}\n\ntype FieldGroupProps = {\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n hasWarning?: boolean\n shape?: 'rectangle' | 'oval'\n className?: string\n children: React.ReactNode\n onClick?: React.MouseEventHandler<HTMLDivElement>\n}\n\nconst FieldGroup = React.forwardRef<HTMLDivElement, FieldGroupProps>(({ children, className, onClick, ...opts }, ref) => {\n const disabled = opts.isDisabled || opts.isReadonly\n const shape = opts.shape === 'oval' ? 'rounded-full' : 'rounded'\n\n return (\n <Group\n ref={ref}\n className={tw(\n 'relative overflow-hidden ring-1 shadow-sm transition-all duration-200 max-w-96',\n 'focus-within:outline-2 focus-within:-outline-offset-1',\n shape,\n disabled && 'ring-neutral-200 bg-neutral-50 text-neutral-400 shadow-none cursor-not-allowed',\n opts.isInvalid && 'ring-negative-300 bg-negative-50 focus-within:outline-negative-500',\n opts.hasWarning && 'ring-notice-300 bg-notice-50 focus-within:outline-notice-500',\n !opts.isInvalid &&\n !opts.hasWarning &&\n !disabled &&\n 'ring-black/10 hover:ring-black/20 focus-within:outline-primary-500',\n className\n )}\n onClick={onClick}\n >\n {children}\n </Group>\n )\n})\n\nconst FieldInput = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {\n return (\n <Input\n {...props}\n ref={ref}\n className={composeTailwindRenderProps(\n props.className,\n tw(\n 'flex-1 bg-transparent px-3 py-2 border-0 focus:ring-0 focus:outline-none text-sm max-sm:text-base text-neutral-900 placeholder:text-neutral-400',\n (props.disabled || props.readOnly) && 'cursor-not-allowed'\n )\n )}\n />\n )\n})\n\nfunction DropdownListBox<T extends object>(props: React.ComponentProps<typeof ListBox<T>>) {\n return (\n <ListBox<T>\n {...props}\n className={composeTailwindRenderProps(\n props.className,\n 'overflow-hidden rounded bg-white border border-neutral-200 shadow-lg text-sm w-full focus:outline-none max-h-60 overflow-y-auto'\n )}\n />\n )\n}\n\nfunction DropdownItem(props: {\n id: string\n textValue: string\n label: React.ReactNode\n secondary?: React.ReactNode\n isDisabled?: boolean\n color?: string | null\n showCheckIcon?: boolean\n customValuePrefix?: string\n className?: string\n}) {\n const isCustom = Boolean(props.customValuePrefix)\n const showCheck = props.showCheckIcon !== false && !isCustom\n\n return (\n <ListBoxItem\n id={props.id}\n textValue={props.textValue}\n isDisabled={props.isDisabled}\n className={({ isFocused, isSelected }) =>\n tw(\n isFocused ? 'bg-neutral-50 text-neutral-900' : 'text-neutral-900 bg-white',\n 'relative cursor-pointer text-sm transition-colors duration-150',\n 'pr-2 pl-3 py-1.5 outline-none',\n props.isDisabled && 'opacity-50 cursor-not-allowed',\n isSelected ? (isFocused ? 'bg-primary-100' : 'bg-primary-50') : '',\n isCustom && 'italic',\n props.className\n )\n }\n >\n {({ isSelected }) => (\n <>\n {props.color && !isCustom ? (\n <div className=\"absolute left-0 top-0 w-[3px] h-full\" style={{ backgroundColor: props.color }} />\n ) : null}\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex-1 min-w-0\">\n <div className={tw(showCheck && isSelected ? 'font-medium' : 'font-normal', 'truncate')}>\n {isCustom ? `${props.customValuePrefix} \"${props.label}\"` : props.label}\n </div>\n {props.secondary && !isCustom && (\n <div className=\"text-xs mt-px truncate text-neutral-500\">{props.secondary}</div>\n )}\n </div>\n {showCheck && isSelected && (\n <div className=\"ml-2 w-fit shrink-0 flex items-center text-primary-500\">\n <Icon name=\"check\" className=\"h-4 w-4\" />\n </div>\n )}\n </div>\n </>\n )}\n </ListBoxItem>\n )\n}\n\nfunction DropdownSection<T extends object>(props: {\n title?: string\n items?: Iterable<T>\n children: React.ReactNode | ((item: T) => React.ReactNode)\n}) {\n return (\n <ListBoxSection>\n {props.title && (\n <Header className=\"sticky top-0 z-10 bg-white px-3 py-1.5 text-xs font-semibold text-neutral-500 border-b border-neutral-100\">\n {props.title}\n </Header>\n )}\n <Collection items={props.items}>{props.children}</Collection>\n </ListBoxSection>\n )\n}\n\nfunction ChevronButton(props?: { className?: string }) {\n return (\n <Button className={tw('flex items-center px-2 py-2', props?.className)}>\n <Icon name=\"chevron-up-down\" className=\"h-4 w-4 text-neutral-400 transition-colors\" />\n </Button>\n )\n}\n\nexport const FormFieldComponents = {\n Label,\n FormFieldHelpText,\n LabelRow,\n FieldGroup,\n FieldInput,\n DropdownListBox,\n DropdownSection,\n DropdownItem,\n ChevronButton\n}\n"}]},{"name":"animated-popover","dependencies":["tw"],"files":[{"name":"components/helpers/animated-popover.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add animated-popover --directory app\n\nimport { Popover, type PopoverProps } from 'react-aria-components'\nimport { tw } from '../../utils/tw'\n\nconst animationClasses = tw(\n 'transition-all duration-150 ease-out',\n // resting state: keep transform properties defined so transitions have endpoints\n 'translate-x-0 translate-y-0',\n // entering: fade + slide from trigger direction\n 'data-[entering]:opacity-0',\n 'data-[entering]:data-[placement=bottom]:-translate-y-1',\n 'data-[entering]:data-[placement=top]:translate-y-1',\n 'data-[entering]:data-[placement=left]:translate-x-1',\n 'data-[entering]:data-[placement=right]:-translate-x-1',\n // exiting: fade + slide toward trigger direction\n 'data-[exiting]:opacity-0',\n 'data-[exiting]:data-[placement=bottom]:-translate-y-1',\n 'data-[exiting]:data-[placement=top]:translate-y-1',\n 'data-[exiting]:data-[placement=left]:translate-x-1',\n 'data-[exiting]:data-[placement=right]:-translate-x-1'\n)\n\nexport function AnimatedPopover({ className, ...props }: PopoverProps) {\n return <Popover {...props} className={tw(animationClasses, className)} />\n}\n"}]},{"name":"headless-button","dependencies":["compose-refs","use-render-props"],"files":[{"name":"components/helpers/headless-button.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add headless-button --directory app\n\nimport { composeRefs } from '../../utils/compose-refs'\nimport { type WithRenderProps, useRenderProps } from '../../utils/use-render-props'\n\nimport React from 'react'\nimport { usePress } from '@react-aria/interactions'\nimport { mergeProps } from '@react-aria/utils'\n\nexport const HeadlessButton = React.forwardRef<\n HTMLButtonElement,\n WithRenderProps<React.ComponentProps<'button'>, { isPressed: boolean }>\n>((rawProps, externalRef) => {\n const myRef = React.useRef<HTMLButtonElement>(null)\n const { isPressed, pressProps } = usePress({ ref: myRef, isDisabled: rawProps.disabled })\n const renderProps = React.useMemo(\n () => ({\n isPressed\n }),\n [isPressed]\n )\n\n const props = useRenderProps(mergeProps(pressProps, rawProps), renderProps, ['children', 'style', 'className'])\n const ref = React.useMemo(() => composeRefs(myRef, externalRef), [myRef, externalRef])\n\n return (\n <button\n type=\"button\"\n {...props}\n {...(props.disabled || props['data-disabled'] ? { ['data-disabled']: true } : {})}\n {...((isPressed && !props.disabled) || props['data-pressed'] ? { ['data-pressed']: true } : {})}\n aria-pressed={props['aria-pressed'] ?? (isPressed && !props.disabled)}\n ref={ref}\n />\n )\n})\n\nHeadlessButton.displayName = 'HeadlessButton'\n"}]},{"name":"get-button-classes","dependencies":["tw"],"files":[{"name":"components/helpers/get-button-classes.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add get-button-classes --directory app\n\nimport { tw } from '../../utils/tw'\n\nexport type ButtonVariant = 'contained' | 'outlined' | 'soft' | 'plain'\nexport type ButtonColor = 'primary' | 'negative' | 'positive' | 'notice' | 'neutral'\nexport type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\nexport type ButtonShape = 'rectangle' | 'circle' | 'oval'\n\nconst base =\n 'relative inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow,transform,opacity] duration-150 cursor-default select-none [&_svg]:pointer-events-none [&_svg:not([class*=size-])]:size-4 [&_svg]:shrink-0 data-[disabled]:cursor-not-allowed'\n\nconst sizeClasses: Record<ButtonSize, string> = {\n xs: 'h-7 min-w-14 px-2 text-xs',\n sm: 'h-8 min-w-16 px-2.5 text-sm',\n md: 'h-9 min-w-20 px-3 text-sm',\n lg: 'h-10 min-w-24 px-4 text-base',\n xl: 'h-12 min-w-28 px-5 text-base'\n}\n\nconst shapeClasses: Record<ButtonShape, string> = {\n rectangle: 'rounded',\n circle: 'rounded-full aspect-square px-0 min-w-0',\n oval: 'rounded-full'\n}\n\nconst variantColorClasses: Record<ButtonVariant, Record<ButtonColor, string>> = {\n contained: {\n primary:\n 'bg-primary-600 text-white ring-1 ring-inset ring-primary-700 data-[hovered]:bg-primary-700 data-[hovered]:ring-primary-800 data-[pressed]:bg-primary-800 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none',\n negative:\n 'bg-negative-600 text-white ring-1 ring-inset ring-negative-700 data-[hovered]:bg-negative-700 data-[hovered]:ring-negative-800 data-[pressed]:bg-negative-800 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none',\n positive:\n 'bg-positive-600 text-white ring-1 ring-inset ring-positive-700 data-[hovered]:bg-positive-700 data-[hovered]:ring-positive-800 data-[pressed]:bg-positive-800 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none',\n notice:\n 'bg-notice-600 text-white ring-1 ring-inset ring-notice-700 data-[hovered]:bg-notice-700 data-[hovered]:ring-notice-800 data-[pressed]:bg-notice-800 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none',\n neutral:\n 'bg-neutral-800 text-white ring-1 ring-inset ring-neutral-900 data-[hovered]:bg-neutral-900 data-[pressed]:bg-neutral-950 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none'\n },\n outlined: {\n primary:\n 'bg-white ring-1 ring-inset ring-primary-300 text-primary-700 data-[hovered]:bg-primary-50 data-[hovered]:ring-primary-400 data-[pressed]:bg-primary-100 data-[pressed]:ring-primary-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent',\n negative:\n 'bg-white ring-1 ring-inset ring-negative-300 text-negative-700 data-[hovered]:bg-negative-50 data-[hovered]:ring-negative-400 data-[pressed]:bg-negative-100 data-[pressed]:ring-negative-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent',\n positive:\n 'bg-white ring-1 ring-inset ring-positive-300 text-positive-700 data-[hovered]:bg-positive-50 data-[hovered]:ring-positive-400 data-[pressed]:bg-positive-100 data-[pressed]:ring-positive-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent',\n notice:\n 'bg-white ring-1 ring-inset ring-notice-300 text-notice-700 data-[hovered]:bg-notice-50 data-[hovered]:ring-notice-400 data-[pressed]:bg-notice-100 data-[pressed]:ring-notice-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent',\n neutral:\n 'bg-white ring-1 ring-inset ring-neutral-300 text-neutral-700 data-[hovered]:bg-neutral-50 data-[hovered]:ring-neutral-400 data-[pressed]:bg-neutral-100 data-[pressed]:ring-neutral-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent'\n },\n soft: {\n primary:\n 'bg-primary-100 text-primary-800 data-[hovered]:bg-primary-200 data-[pressed]:bg-primary-300 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n negative:\n 'bg-negative-100 text-negative-800 data-[hovered]:bg-negative-200 data-[pressed]:bg-negative-300 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n positive:\n 'bg-positive-100 text-positive-800 data-[hovered]:bg-positive-200 data-[pressed]:bg-positive-300 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n notice:\n 'bg-notice-100 text-notice-800 data-[hovered]:bg-notice-200 data-[pressed]:bg-notice-300 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n neutral:\n 'bg-neutral-200 text-neutral-800 data-[hovered]:bg-neutral-300 data-[pressed]:bg-neutral-400 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400'\n },\n plain: {\n primary:\n 'bg-transparent text-primary-700 data-[hovered]:bg-primary-50 data-[pressed]:bg-primary-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n negative:\n 'bg-transparent text-negative-700 data-[hovered]:bg-negative-50 data-[pressed]:bg-negative-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n positive:\n 'bg-transparent text-positive-700 data-[hovered]:bg-positive-50 data-[pressed]:bg-positive-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n notice:\n 'bg-transparent text-notice-700 data-[hovered]:bg-notice-50 data-[pressed]:bg-notice-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n neutral:\n 'bg-transparent text-neutral-700 data-[hovered]:bg-neutral-50 data-[pressed]:bg-neutral-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400'\n }\n}\n\nconst toggledVariantColorClasses: Record<ButtonVariant, Record<ButtonColor, string>> = {\n contained: {\n primary: 'bg-primary-800 ring-primary-900',\n negative: 'bg-negative-800 ring-negative-900',\n positive: 'bg-positive-800 ring-positive-900',\n notice: 'bg-notice-800 ring-notice-900',\n neutral: 'bg-neutral-950'\n },\n outlined: {\n primary: 'bg-primary-100 ring-primary-500',\n negative: 'bg-negative-100 ring-negative-500',\n positive: 'bg-positive-100 ring-positive-500',\n notice: 'bg-notice-100 ring-notice-500',\n neutral: 'bg-neutral-100 ring-neutral-500'\n },\n soft: {\n primary: 'bg-primary-200',\n negative: 'bg-negative-200',\n positive: 'bg-positive-200',\n notice: 'bg-notice-200',\n neutral: 'bg-neutral-300'\n },\n plain: {\n primary: 'bg-primary-100',\n negative: 'bg-negative-100',\n positive: 'bg-positive-100',\n notice: 'bg-notice-100',\n neutral: 'bg-neutral-100'\n }\n}\n\nconst focusClasses =\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 data-[focus-visible]:outline-2 data-[focus-visible]:outline-offset-2 data-[focus-visible]:outline-primary-600'\n\nexport function getButtonClasses({\n variant = 'contained',\n color = 'primary',\n size = 'md',\n shape = 'oval',\n showSpinner,\n isToggled,\n className\n}: {\n variant?: ButtonVariant\n color?: ButtonColor\n size?: ButtonSize\n shape?: ButtonShape\n showSpinner?: boolean\n isToggled?: boolean\n className?: string\n} = {}) {\n return tw(\n base,\n 'data-[pressed]:scale-[.98]',\n showSpinner ? '[&>:not([data-loader])]:opacity-0 !text-transparent' : '',\n sizeClasses[size],\n shapeClasses[shape],\n variantColorClasses[variant][color],\n isToggled && toggledVariantColorClasses[variant][color],\n focusClasses,\n className\n )\n}\n"}]},{"name":"spinner","dependencies":["tw"],"files":[{"name":"components/spinner.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add spinner --directory app\n\nimport { ProgressBar } from 'react-aria-components'\nimport { Icon } from './icon'\nimport { tw } from '../utils/tw'\n\ninterface SpinnerProps {\n className?: string\n 'aria-label'?: string\n}\n\nexport function Spinner({ className, 'aria-label': ariaLabel = 'Loading…' }: SpinnerProps) {\n return (\n <ProgressBar isIndeterminate aria-label={ariaLabel}>\n <Icon name=\"arrow-path\" className={tw('animate-spin', className)} />\n </ProgressBar>\n )\n}\n"}]},{"name":"button-context","dependencies":["get-button-classes"],"files":[{"name":"components/helpers/button-context.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add button-context --directory app\n\nimport React from 'react'\nimport type { ButtonVariant, ButtonColor, ButtonSize, ButtonShape } from './get-button-classes'\n\nexport const ButtonContext = React.createContext<{\n variant?: ButtonVariant\n color?: ButtonColor\n size?: ButtonSize\n shape?: ButtonShape\n fullWidth?: boolean\n}>({})\n"}]},{"name":"calendar-month-year-picker","dependencies":["tw"],"files":[{"name":"components/helpers/calendar-month-year-picker.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add calendar-month-year-picker --directory app\n\nimport * as React from 'react'\nimport {\n Button,\n Calendar,\n CalendarGrid,\n CalendarCell,\n Heading,\n CalendarGridHeader,\n CalendarHeaderCell,\n CalendarGridBody\n} from 'react-aria-components'\nimport { CalendarDate, CalendarDateTime } from '@internationalized/date'\nimport { tw } from '../../utils/tw'\nimport { Icon } from '../icon'\n\ntype MonthYearPickerProps = {\n currentDate: CalendarDate | CalendarDateTime\n onSelect: (year: number, month: number) => void\n onCancel: () => void\n}\n\nexport function MonthYearPicker({ currentDate, onSelect, onCancel }: MonthYearPickerProps) {\n const [selectedYear, setSelectedYear] = React.useState(currentDate.year)\n const [viewingYear, setViewingYear] = React.useState(currentDate.year)\n\n const months = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December'\n ]\n\n // Generate year range (current year ± 50 years)\n const startYear = currentDate.year - 50\n const endYear = currentDate.year + 50\n const years = Array.from({ length: endYear - startYear + 1 }, (_, i) => startYear + i)\n\n // Find years to display (show 12 years at a time, 3x4 grid)\n const yearIndex = years.indexOf(viewingYear)\n const displayYears = years.slice(Math.max(0, yearIndex - 5), Math.min(years.length, yearIndex + 7))\n\n const handlePreviousYears = () => {\n setViewingYear((prev) => prev - 12)\n }\n\n const handleNextYears = () => {\n setViewingYear((prev) => prev + 12)\n }\n\n return (\n <div className=\"bg-white border border-neutral-200 rounded-md shadow-lg p-4 w-80\">\n {/* Year selection header */}\n <div className=\"mb-4\">\n <header className=\"flex items-center justify-between mb-3\">\n <Button className=\"p-1 hover:bg-neutral-100 rounded\" onPress={handlePreviousYears}>\n <Icon name=\"chevron-left\" className=\"h-4 w-4\" />\n </Button>\n <div className=\"text-sm font-semibold text-neutral-900\">\n {displayYears[0]} - {displayYears[displayYears.length - 1]}\n </div>\n <Button className=\"p-1 hover:bg-neutral-100 rounded\" onPress={handleNextYears}>\n <Icon name=\"chevron-right\" className=\"h-4 w-4\" />\n </Button>\n </header>\n\n {/* Year grid */}\n <div className=\"grid grid-cols-3 gap-2\">\n {displayYears.map((year) => (\n <button\n key={year}\n type=\"button\"\n onClick={() => setSelectedYear(year)}\n className={tw(\n 'py-2 px-3 text-sm rounded cursor-pointer transition-colors',\n 'hover:bg-neutral-100',\n selectedYear === year && 'bg-primary-500 text-white hover:bg-primary-500',\n year === currentDate.year && selectedYear !== year && 'font-semibold'\n )}\n >\n {year}\n </button>\n ))}\n </div>\n </div>\n\n {/* Month selection */}\n <div>\n <div className=\"text-sm font-semibold text-neutral-900 mb-2\">{selectedYear}</div>\n <div className=\"grid grid-cols-3 gap-2\">\n {months.map((month, index) => {\n const monthNum = index + 1\n const isCurrentMonth = selectedYear === currentDate.year && monthNum === currentDate.month\n\n return (\n <button\n key={month}\n type=\"button\"\n onClick={() => onSelect(selectedYear, monthNum)}\n className={tw(\n 'py-2 px-2 text-sm rounded cursor-pointer transition-colors',\n 'hover:bg-neutral-100',\n isCurrentMonth && 'font-semibold border border-primary-500'\n )}\n >\n {month.slice(0, 3)}\n </button>\n )\n })}\n </div>\n </div>\n\n {/* Cancel button */}\n <div className=\"mt-4 flex justify-end\">\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"py-1 px-3 text-sm text-neutral-600 hover:text-neutral-900 hover:bg-neutral-100 rounded transition-colors\"\n >\n Cancel\n </button>\n </div>\n </div>\n )\n}\n\ntype CustomCalendarPropsBase = {\n isDateUnavailable?: (date: CalendarDate | CalendarDateTime) => boolean\n}\n\ntype CustomCalendarPropsDate = CustomCalendarPropsBase & {\n value: CalendarDate | null\n onChange: (date: CalendarDate | null) => void\n minValue?: CalendarDate\n maxValue?: CalendarDate\n}\n\ntype CustomCalendarPropsDateTime = CustomCalendarPropsBase & {\n value: CalendarDateTime | null\n onChange: (date: CalendarDateTime | null) => void\n minValue?: CalendarDateTime\n maxValue?: CalendarDateTime\n}\n\ntype CustomCalendarProps = CustomCalendarPropsDate | CustomCalendarPropsDateTime\n\nexport function CustomCalendar(props: CustomCalendarProps) {\n const { value, onChange, minValue, maxValue, isDateUnavailable } = props\n const [showMonthYearPicker, setShowMonthYearPicker] = React.useState(false)\n\n // Create a default focused date\n const createDefaultDate = () => {\n const now = new Date()\n const year = now.getFullYear()\n const month = now.getMonth() + 1\n const day = now.getDate()\n\n // Check if we're working with CalendarDateTime\n if (value && 'hour' in value) {\n return new CalendarDateTime(year, month, day, 0, 0, 0)\n }\n return new CalendarDate(year, month, day)\n }\n\n const [focusedDate, setFocusedDate] = React.useState(value || createDefaultDate())\n\n const handleMonthYearSelect = (year: number, month: number) => {\n if ('hour' in focusedDate) {\n // CalendarDateTime\n const newDate = focusedDate.set({ year, month })\n setFocusedDate(newDate)\n } else {\n // CalendarDate - use safe day value\n const newDate = new CalendarDate(year, month, Math.min(focusedDate.day, 28))\n setFocusedDate(newDate)\n }\n setShowMonthYearPicker(false)\n }\n\n const handleFocusChange = (date: CalendarDate) => {\n if ('hour' in focusedDate) {\n // Convert CalendarDate to CalendarDateTime by preserving time from focusedDate\n const newDateTime = focusedDate.set({\n year: date.year,\n month: date.month,\n day: date.day\n })\n setFocusedDate(newDateTime)\n } else {\n setFocusedDate(date)\n }\n }\n\n if (showMonthYearPicker) {\n return (\n <MonthYearPicker\n currentDate={focusedDate}\n onSelect={handleMonthYearSelect}\n onCancel={() => setShowMonthYearPicker(false)}\n />\n )\n }\n\n const handleCalendarChange = (date: CalendarDate | CalendarDateTime | null) => {\n if (typeof onChange === 'function') {\n // Type assertion needed due to union type constraints\n ;(onChange as (date: CalendarDate | CalendarDateTime | null) => void)(date)\n }\n }\n\n return (\n <Calendar\n value={value ?? undefined}\n onChange={handleCalendarChange}\n focusedValue={focusedDate}\n onFocusChange={handleFocusChange}\n minValue={minValue !== undefined && minValue !== null ? minValue : undefined}\n maxValue={maxValue !== undefined && maxValue !== null ? maxValue : undefined}\n isDateUnavailable={\n isDateUnavailable\n ? (date) => {\n // Convert DateValue to CalendarDate for the callback\n if ('hour' in date) {\n // It's a CalendarDateTime, convert to CalendarDate\n const calendarDate = new CalendarDate(date.year, date.month, date.day)\n return isDateUnavailable(calendarDate)\n }\n return isDateUnavailable(date as CalendarDate)\n }\n : undefined\n }\n className=\"bg-white border border-neutral-200 rounded-md shadow-lg p-3\"\n >\n <header className=\"flex items-center justify-between mb-2\">\n <Button slot=\"previous\" className=\"p-1 hover:bg-neutral-100 rounded\">\n <Icon name=\"chevron-left\" className=\"h-4 w-4\" />\n </Button>\n <button\n type=\"button\"\n onClick={() => setShowMonthYearPicker(true)}\n className=\"text-sm font-semibold text-neutral-900 hover:bg-neutral-100 px-2 py-1 rounded transition-colors\"\n >\n <Heading />\n </button>\n <Button slot=\"next\" className=\"p-1 hover:bg-neutral-100 rounded\">\n <Icon name=\"chevron-right\" className=\"h-4 w-4\" />\n </Button>\n </header>\n <CalendarGrid>\n <CalendarGridHeader>\n {(day) => <CalendarHeaderCell className=\"text-xs text-neutral-500 font-medium w-8 h-8\">{day}</CalendarHeaderCell>}\n </CalendarGridHeader>\n <CalendarGridBody>\n {(date) => (\n <CalendarCell\n date={date}\n className={({ isSelected, isOutsideMonth, isDisabled, isUnavailable, isFocusVisible }) =>\n tw(\n 'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center',\n 'hover:bg-neutral-100',\n isOutsideMonth && 'text-neutral-300',\n (isDisabled || isUnavailable) && 'text-neutral-300 cursor-not-allowed',\n isSelected && 'bg-primary-500 text-white hover:bg-primary-500',\n isFocusVisible && 'ring-2 ring-primary-500 ring-offset-1'\n )\n }\n />\n )}\n </CalendarGridBody>\n </CalendarGrid>\n </Calendar>\n )\n}\n"}]},{"name":"pdf-dist","files":[{"name":"components/helpers/pdf-dist.client.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add pdf-dist --directory app\n\n// @ts-ignore\nimport pdfjs from '@bundled-es-modules/pdfjs-dist'\n\npdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`\n\nexport default pdfjs\n"}]},{"name":"button","dependencies":["tw","spinner","button-context","get-button-classes","headless-button","compose-refs","use-prevent-default","use-render-props","use-spin-delay","use-stable-accessor","icon"],"files":[{"name":"components/button.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add button --directory app\n\nimport * as React from 'react'\nimport { Spinner } from './spinner'\nimport { flushSync } from 'react-dom'\nimport { composeRenderProps } from 'react-aria-components'\nimport {\n getButtonClasses,\n type ButtonColor,\n type ButtonShape,\n type ButtonSize,\n type ButtonVariant\n} from './helpers/get-button-classes'\nimport { HeadlessButton } from './helpers/headless-button'\nimport { useSpinDelay } from '../utils/use-spin-delay'\nimport { usePreventDefault } from '../utils/use-prevent-default'\nimport { composeTailwindRenderProps, tw } from '../utils/tw'\nimport { useStableAccessor } from '../utils/use-stable-accessor'\nimport { mergeProps } from '@react-aria/utils'\nimport { ButtonContext } from './helpers/button-context'\n\n// --- Public types ---\n\nexport type ButtonProps = {\n variant?: ButtonVariant\n color?: ButtonColor\n size?: ButtonSize\n shape?: ButtonShape\n isToggled?: boolean\n fullWidth?: boolean\n onActionSucceeded?: () => void\n onActionCompleted?: () => void\n disabled?: boolean\n onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void\n className?: string\n children?: React.ReactNode\n value?: string | undefined\n isPending?: boolean\n} & Partial<Pick<React.ComponentProps<'button'>, 'type' | 'role' | 'name' | 'form' | 'id' | 'title' | 'aria-label'>>\n\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ isToggled, onActionSucceeded, onActionCompleted, isPending: isPendingProp, ...propsRaw }, ref) => {\n const {\n variant = 'contained',\n color = 'primary',\n size = 'md',\n shape = 'oval',\n fullWidth,\n ...props\n } = mergeProps(propsRaw, React.useContext(ButtonContext))\n\n const { isClicking, onClick: isClickingHandler } = useIsClicking(props.onClick)\n\n const isPending = isPendingProp || isClicking\n const isPendingSmoothed = useSpinDelay(isPending, { delay: 0, minDuration: 50 })\n const showSpinner = useSpinDelay(isPending, { delay: 500, minDuration: 200 })\n\n const getOnActionCompleted = useStableAccessor(onActionCompleted)\n const getOnActionSucceeded = useStableAccessor(onActionSucceeded)\n const wasPending = React.useRef(false)\n React.useEffect(() => {\n if (isPendingSmoothed !== wasPending.current) {\n wasPending.current = isPendingSmoothed\n if (!isPendingSmoothed) {\n getOnActionCompleted()?.()\n getOnActionSucceeded()?.()\n }\n }\n }, [isPendingSmoothed, getOnActionCompleted, getOnActionSucceeded])\n\n const onClick = usePreventDefault(isClickingHandler, showSpinner || isClicking)\n\n return (\n <HeadlessButton\n {...props}\n ref={ref}\n data-pending={isPending}\n onClick={onClick}\n {...(isToggled != null ? { 'aria-pressed': isToggled } : {})}\n className={composeTailwindRenderProps(\n props.className,\n tw(getButtonClasses({ variant, color, size, shape, isToggled }), fullWidth && 'w-full')\n )}\n >\n {composeRenderProps(props.children, (children) => (\n <>\n {(size === 'xs' || size === 'sm' || shape === 'circle') && (\n <span className=\"absolute left-1/2 top-1/2 size-12 -translate-x-1/2 -translate-y-1/2\" aria-hidden=\"true\" />\n )}\n <span className={tw('inline-flex items-center gap-2', showSpinner && 'opacity-0')}>{children}</span>\n <span\n aria-hidden={!showSpinner ? true : undefined}\n className={tw('absolute inset-0 flex items-center justify-center', !showSpinner && 'opacity-0')}\n >\n <Spinner className=\"size-4\" />\n </span>\n </>\n ))}\n </HeadlessButton>\n )\n }\n)\n\nButton.displayName = 'Button'\n\nfunction useIsClicking<C extends ((event: React.MouseEvent<any, MouseEvent>) => unknown) | undefined>(\n onClickRaw: C\n): { onClick: C; isClicking: boolean } {\n type Event = C extends (event: infer E) => unknown ? E : never\n\n const [isClicking, setIsClicking] = React.useState<boolean>(false)\n\n const onClickRawStabilized = useStableAccessor(onClickRaw)\n\n const onClickDebounced = React.useCallback(\n (event: Event) => {\n try {\n const onClickInner = onClickRawStabilized()\n flushSync(() => setIsClicking(true))\n const result = onClickInner ? onClickInner(event) : null\n Promise.resolve(result).finally(() => {\n setIsClicking(false)\n })\n return result\n } catch (e) {\n setIsClicking(false)\n throw e\n }\n },\n [onClickRawStabilized, setIsClicking]\n )\n\n if (!onClickRaw) {\n return { onClick: undefined as C, isClicking }\n } else {\n return { onClick: onClickDebounced as C, isClicking }\n }\n}\n"}]},{"name":"icon","files":[{"name":"components/icon.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add icon --directory app\n\nimport * as React from 'react'\n// @ts-ignore\nimport href from '../icons/sprite.svg'\nimport type { IconName } from '~/icons/types'\n\n// ---------------------------------------------------------------------------\n// Props\n// ---------------------------------------------------------------------------\n\nexport interface IconProps extends React.ComponentProps<'svg'> {\n name: IconName\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport const Icon = React.forwardRef<SVGSVGElement, IconProps>(({ className, name, ...props }, ref) => {\n return (\n <svg ref={ref} viewBox=\"0 0 24 24\" className={className} {...props}>\n <use href={`${href}#${name}`} />\n </svg>\n )\n})\n\nIcon.displayName = 'Icon'\n"}]},{"name":"headless-file-input","dependencies":["compose-refs"],"files":[{"name":"components/headless-file-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add headless-file-input --directory app\n\nimport React from 'react'\nimport { useComposedRefs } from '../utils/compose-refs'\n\nexport type HeadlessFileInputProps = {\n children?: React.ReactNode\n} & (\n | {\n multiple: true\n value?: (string | File)[]\n defaultValue?: (string | File)[]\n onChange?(value: (string | File)[]): unknown\n }\n | {\n multiple?: false\n value?: string | File | null\n defaultValue?: string | File | null\n onChange?(value: string | File | null): unknown\n }\n)\n\ntype FileInputContext =\n | { multiple?: false; value: File | string | null; onChange(value: File | string | null): unknown }\n | { multiple: true; value: (File | string)[]; onChange(value: (File | string)[]): unknown }\n\nconst fileInputContext = React.createContext<FileInputContext>({\n multiple: false,\n value: null,\n onChange: () => null\n})\n\nfunction FileInputContainer({ multiple, value, defaultValue, onChange, children }: HeadlessFileInputProps) {\n const controlled = useControlled({ multiple, value, defaultValue, onChange }) as FileInputContext\n return <fileInputContext.Provider value={controlled}>{children}</fileInputContext.Provider>\n}\n\nFileInputContainer.displayName = 'HeadlessFileInput'\n\nconst FileInputDropZone = React.forwardRef<\n HTMLLabelElement,\n {\n name?: string\n children: ((props: { dragOver: boolean }) => React.ReactElement) | React.ReactElement\n inputId?: string\n accept?: string | undefined\n onDragChange?(value: boolean): unknown\n capture?: React.InputHTMLAttributes<'file'>['capture']\n disabled?: React.InputHTMLAttributes<'file'>['disabled']\n processFile?: (file: File) => Promise<File>\n inputRef?: React.Ref<HTMLInputElement>\n } & Omit<React.ComponentProps<'label'>, 'children'>\n>(({ children, inputId, inputRef, onDragChange, name, accept, capture, disabled, processFile, ...props }, ref) => {\n const { onChange, value, multiple } = React.useContext(fileInputContext)\n const [dragOver, setDragOver] = React.useState(false)\n const startDrag = React.useCallback(\n (e: React.DragEvent<HTMLInputElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setDragOver(true)\n onDragChange && onDragChange(true)\n },\n [setDragOver, onDragChange]\n )\n const stopDrag = React.useCallback(\n (e: React.DragEvent<HTMLInputElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setDragOver(false)\n onDragChange && onDragChange(false)\n },\n [setDragOver, onDragChange]\n )\n\n const childrenResult = typeof children === 'function' ? children({ dragOver }) : children\n const childProps = childrenResult.props as Record<string, unknown>\n const cloned = React.cloneElement(\n childrenResult,\n {\n ...childProps\n },\n [\n ...React.Children.toArray(childProps.children as React.ReactNode),\n <FileInputInput\n key=\"file-input-hidden\"\n id={inputId}\n name={name}\n accept={accept}\n ref={inputRef}\n style={{ width: '0.1px', height: '0.1px', opacity: 0, overflow: 'hidden', position: 'absolute', zIndex: -10 }}\n capture={capture}\n disabled={disabled}\n processFile={processFile}\n />\n ]\n )\n\n return (\n <label {...props} ref={ref}>\n <div\n onDrag={cancel}\n onDragStart={cancel}\n onDragOver={cancel}\n onDragEnter={startDrag}\n onDragLeave={stopDrag}\n onDrop={(e) => {\n e.preventDefault()\n e.stopPropagation()\n setDragOver(false)\n onDragChange && onDragChange(false)\n if (e.dataTransfer.files.length) {\n if (multiple) {\n onChange([...value, ...Array.from(e.dataTransfer.files)])\n } else {\n onChange(e.dataTransfer.files.item(0) || null)\n }\n } else {\n const uri = e.dataTransfer.getData('text/uri-list')\n if (uri) {\n fetchFileFromUri(uri).then((file) => {\n if (multiple) {\n onChange([...value, file])\n } else {\n onChange(file)\n }\n })\n }\n }\n }}\n >\n {cloned}\n </div>\n </label>\n )\n})\nFileInputDropZone.displayName = 'FileInputDropZone'\n\nconst FileInputInput = React.forwardRef<\n HTMLInputElement,\n React.ComponentPropsWithoutRef<'input'> & { processFile?: (file: File) => Promise<File> }\n>(({ name, processFile, form, ...props }, refProp) => {\n const { multiple, value, onChange } = React.useContext(fileInputContext)\n const localRef = React.useRef<HTMLInputElement>(null)\n const ref = useComposedRefs(localRef, refProp)\n\n React.useEffect(() => {\n if (localRef.current) {\n const dt = new DataTransfer()\n if (Array.isArray(value)) {\n value.forEach(async (v) => v instanceof File && dt.items.add(v))\n } else if (value && value instanceof File) {\n dt.items.add(value)\n }\n localRef.current.files = dt.files\n }\n }, [value, processFile])\n\n const urls = React.useMemo(() => {\n if (Array.isArray(value)) {\n return value.map((v, i) => (typeof v === 'string' ? { name: `${name}.${i}`, value: v } : null)).filter(Boolean) as {\n name: string\n value: string\n }[]\n } else if (value && typeof value === 'string') {\n return [{ name: name, value: value }]\n } else {\n return []\n }\n }, [value, name])\n\n const hasFiles =\n typeof value !== 'string' &&\n !!value &&\n !(Array.isArray(value) && !value.filter((v) => v && typeof v !== 'string').length)\n const hasUrls = !!urls.length\n\n return (\n <>\n <input\n {...props}\n type=\"file\"\n form={form}\n ref={hasFiles ? ref : undefined}\n name={hasFiles ? name : undefined}\n onChange={async (e) => {\n const newValue = (\n multiple\n ? e.currentTarget.files\n ? [\n ...value,\n ...(await Promise.all(\n Array.from(e.currentTarget.files).map(async (file) => (processFile ? processFile(file) : file))\n ))\n ]\n : []\n : processFile && e.currentTarget.files?.item(0)\n ? await processFile(e.currentTarget.files?.item(0) as File)\n : e.currentTarget.files?.item(0)\n ) as any\n\n onChange(newValue)\n }}\n multiple={multiple}\n />\n {urls.map((url, i) => (\n <input\n type=\"hidden\"\n ref={i === 0 && !hasFiles ? ref : undefined}\n form={form}\n value={url.value}\n name={url.name}\n key={url.name}\n />\n ))}\n {!hasUrls && !hasFiles ? <input type=\"hidden\" ref={ref} form={form} name={name} value=\"\" /> : null}\n </>\n )\n})\nFileInputInput.displayName = 'FileInputInput'\n\nfunction FilePreview({\n children\n}: {\n children(\n props: {\n file: File | null\n fileUrl: string\n remove(): unknown\n replace(newFile: File | string): unknown\n }[]\n ): React.ReactNode\n}) {\n const { value, onChange } = React.useContext(fileInputContext)\n const urlMap = useFileUrls(value)\n const removeSingle = React.useCallback(() => onChange(null as any), [onChange])\n const replaceSingle = React.useCallback((file: File | string) => onChange(file as any), [onChange])\n\n const removeMultiple = React.useCallback(\n (index: number) => onChange((value as File[]).filter((f, i) => i !== index) as any),\n [value, onChange]\n )\n const replaceMultiple = React.useCallback(\n (index: number, file: File | string) =>\n onChange((value as (File | string)[]).map((f, i) => (i === index ? file : f)) as any),\n [value, onChange]\n )\n\n if (!value) {\n return <>{children([])}</>\n } else if (Array.isArray(value)) {\n return (\n <>\n {children(\n value.map((v, i) => ({\n file: typeof v === 'string' ? null : v,\n fileUrl: urlMap.get(v) ?? (typeof v === 'string' ? v : ''),\n remove: () => removeMultiple(i),\n replace: (f) => replaceMultiple(i, f)\n }))\n )}\n </>\n )\n } else {\n return (\n <>\n {children([\n {\n file: typeof value === 'string' ? null : value,\n fileUrl: urlMap.get(value) ?? (typeof value === 'string' ? value : ''),\n remove: removeSingle,\n replace: replaceSingle\n }\n ])}\n </>\n )\n }\n}\n\nexport const HeadlessFileInput = Object.assign(FileInputContainer, {\n Input: FileInputInput,\n Preview: FilePreview,\n DropZone: FileInputDropZone\n})\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction cancel(e: React.SyntheticEvent) {\n e.stopPropagation()\n e.preventDefault()\n}\n\nfunction useControlled<T extends string | File | null | (string | File)[]>({\n defaultValue,\n value,\n onChange: onChangeRaw,\n multiple\n}: {\n value?: T\n defaultValue?: T\n onChange?(value: null | string | File | (string | File)[]): unknown\n multiple?: boolean\n}) {\n const [state, setState] = React.useState(defaultValue)\n\n const onChange = React.useCallback(\n (v: T) => {\n setState(v)\n onChangeRaw && onChangeRaw(v)\n },\n [onChangeRaw, setState]\n )\n\n const proposed = value === undefined ? state : value\n\n return { value: !proposed && multiple ? [] : proposed || null, onChange, multiple: !!multiple }\n}\n\nfunction useFileUrls(value: (File | string)[] | File | string | null): Map<File | string, string> {\n const urlMapRef = React.useRef(new Map<File | string, string>())\n\n const items = value === null ? [] : Array.isArray(value) ? value : [value]\n\n // Create blob URLs for new items only\n for (const item of items) {\n if (!urlMapRef.current.has(item)) {\n urlMapRef.current.set(item, typeof item === 'string' ? item : URL.createObjectURL(item))\n }\n }\n\n // Prune stale entries and revoke their blob URLs\n const currentKeys = new Set(items)\n React.useEffect(() => {\n for (const [key, url] of urlMapRef.current) {\n if (!currentKeys.has(key)) {\n if (typeof key !== 'string') URL.revokeObjectURL(url)\n urlMapRef.current.delete(key)\n }\n }\n })\n\n // Revoke all blob URLs on unmount\n React.useEffect(() => {\n const map = urlMapRef.current\n return () => {\n for (const [key, url] of map) {\n if (typeof key !== 'string') URL.revokeObjectURL(url)\n }\n map.clear()\n }\n }, [])\n\n return urlMapRef.current\n}\n\nasync function fetchFileFromUri(uri: string): Promise<File> {\n const res = await fetch(uri)\n const blob = await res.blob()\n const contentType = res.headers.get('content-type') || blob.type || ''\n const contentDisposition = res.headers.get('content-disposition')\n let filename = new URL(uri).pathname.split('/').pop() || 'file'\n const match = contentDisposition?.match(/filename\\*?=(?:UTF-8'')?[\"']?([^\"';\\n]+)/)\n if (match) filename = decodeURIComponent(match[1]!)\n return new File([blob], filename, { type: contentType })\n}\n"}]},{"name":"button-link","dependencies":["tw","spinner","button-context","get-button-classes","use-spin-delay","icon"],"files":[{"name":"components/button-link.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add button-link --directory app\n\nimport * as React from 'react'\nimport { Link } from 'react-router'\nimport { usePress } from '@react-aria/interactions'\nimport { mergeProps } from '@react-aria/utils'\nimport { Spinner } from './spinner'\nimport {\n getButtonClasses,\n type ButtonColor,\n type ButtonShape,\n type ButtonSize,\n type ButtonVariant\n} from './helpers/get-button-classes'\nimport { useSpinDelay } from '../utils/use-spin-delay'\nimport { tw } from '../utils/tw'\nimport { ButtonContext } from './helpers/button-context'\n\nexport type ButtonLinkProps = React.ComponentPropsWithoutRef<typeof Link> & {\n size?: ButtonSize\n variant?: ButtonVariant\n color?: ButtonColor\n shape?: ButtonShape\n disabled?: boolean\n fullWidth?: boolean\n isPending?: boolean\n}\n\nexport const ButtonLink = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(\n ({ disabled, fullWidth, isPending: isPendingProp, children, onClick, className, ...propsRaw }, ref) => {\n const {\n size = 'md',\n variant = 'contained',\n color = 'primary',\n shape = 'oval',\n ...props\n } = mergeProps(propsRaw, React.useContext(ButtonContext))\n const { isPressed, pressProps } = usePress({ isDisabled: disabled })\n\n const isPending = isPendingProp ?? false\n const showSpinner = useSpinDelay(isPending, { delay: 500, minDuration: 200 })\n\n const handleClick = disabled || isPending ? (e: React.MouseEvent<HTMLAnchorElement>) => e.preventDefault() : onClick\n\n return (\n <Link\n {...mergeProps(props, pressProps, { onClick: handleClick })}\n ref={ref}\n data-pending={isPending || undefined}\n data-disabled={disabled || undefined}\n data-pressed={(isPressed && !disabled) || undefined}\n aria-disabled={disabled || undefined}\n className={getButtonClasses({\n size,\n variant,\n color,\n shape,\n className: tw(fullWidth && 'w-full', className)\n })}\n >\n {(size === 'xs' || size === 'sm' || shape === 'circle') && (\n <span className=\"absolute left-1/2 top-1/2 size-12 -translate-x-1/2 -translate-y-1/2\" aria-hidden=\"true\" />\n )}\n <span className={tw('inline-flex items-center gap-2', showSpinner && 'opacity-0')}>{children}</span>\n <span\n aria-hidden={!showSpinner ? true : undefined}\n className={tw('absolute inset-0 flex items-center justify-center', !showSpinner && 'opacity-0')}\n >\n <Spinner className=\"size-4\" />\n </span>\n </Link>\n )\n }\n)\n\nButtonLink.displayName = 'ButtonLink'\n"}]},{"name":"button-group","dependencies":["tw"],"files":[{"name":"components/button-group.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add button-group --directory app\n\nimport * as React from 'react'\nimport { mergeProps } from '@react-aria/utils'\nimport { tw } from '../utils/tw'\n\nexport type ButtonGroupOrientation = 'horizontal' | 'vertical'\nexport type ButtonGroupAlignment = 'start' | 'center' | 'end'\nexport type ButtonGroupSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n\nexport type ButtonGroupProps = {\n orientation?: ButtonGroupOrientation\n alignment?: ButtonGroupAlignment\n size?: ButtonGroupSize\n className?: string\n children?: React.ReactNode\n 'aria-label'?: string\n 'aria-labelledby'?: string\n 'aria-describedby'?: string\n fullWidth?: boolean\n}\n\nexport const ButtonGroupContext = React.createContext<{\n orientation?: ButtonGroupOrientation\n alignment?: ButtonGroupAlignment\n size?: ButtonGroupSize\n fullWidth?: boolean\n className?: string\n}>({})\n\nconst sizeGapClasses: Record<ButtonGroupSize, string> = {\n xl: 'gap-4',\n lg: 'gap-3',\n md: 'gap-2',\n sm: 'gap-1',\n xs: 'gap-0.5'\n}\n\nconst alignmentClasses: Record<ButtonGroupAlignment, string> = {\n start: 'justify-start',\n center: 'justify-center',\n end: 'justify-end'\n}\n\nexport function ButtonGroup(inputProps: ButtonGroupProps) {\n const context = React.useContext(ButtonGroupContext)\n const {\n orientation = 'horizontal',\n alignment = 'end',\n size = 'md',\n className,\n children,\n fullWidth,\n ...ariaProps\n } = mergeProps(React.useContext(ButtonGroupContext), inputProps)\n\n return (\n <div\n role=\"group\"\n aria-label={ariaProps['aria-label']}\n aria-labelledby={ariaProps['aria-labelledby']}\n aria-describedby={ariaProps['aria-describedby']}\n className={tw(\n 'inline-flex w-fit',\n sizeGapClasses[size],\n orientation === 'horizontal' ? 'flex-row items-center flex-wrap' : 'flex-col',\n alignmentClasses[alignment],\n fullWidth && 'w-full',\n className\n )}\n >\n {children}\n </div>\n )\n}\n\nButtonGroup.displayName = 'ButtonGroup'\n"}]},{"name":"toggle-button-group","dependencies":["tw","get-button-classes"],"files":[{"name":"components/toggle-button-group.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add toggle-button-group --directory app\n\nimport * as React from 'react'\nimport { RadioGroup, Radio } from 'react-aria-components'\nimport { tw } from '../utils/tw'\nimport type { ButtonSize } from './helpers/get-button-classes'\n\n// --- Types ---\n\nexport type ToggleButtonGroupSize = ButtonSize\n\nexport type ToggleButtonGroupProps = {\n value?: string | null\n defaultValue?: string | null\n onChange?: (value: string) => void\n orientation?: 'horizontal' | 'vertical'\n className?: string\n isDisabled?: boolean\n name?: string\n children: React.ReactNode\n 'aria-label'?: string\n 'aria-labelledby'?: string\n}\n\n// --- ToggleButtonGroup ---\n\nexport function ToggleButtonGroup({ orientation = 'horizontal', className, children, ...props }: ToggleButtonGroupProps) {\n return (\n <RadioGroup\n {...props}\n orientation={orientation}\n className={tw(\n 'inline-flex',\n orientation === 'horizontal'\n ? ['flex-row', '[&>*:first-child]:rounded-l [&>*:last-child]:rounded-r', '[&>*:not(:first-child)]:-ml-px']\n : ['flex-col', '[&>*:first-child]:rounded-t [&>*:last-child]:rounded-b', '[&>*:not(:first-child)]:-mt-px'],\n className\n )}\n >\n {children}\n </RadioGroup>\n )\n}\n\nToggleButtonGroup.displayName = 'ToggleButtonGroup'\n\n// --- ToggleButton ---\n\nexport type ToggleButtonProps = {\n value: string\n children: React.ReactNode\n isDisabled?: boolean\n className?: string\n 'aria-label'?: string\n size?: ToggleButtonGroupSize\n}\n\nexport function ToggleButton({ children, className, size = 'md', ...props }: ToggleButtonProps) {\n return (\n <Radio {...props} className={({ isSelected }) => tw(getToggleButtonClasses({ size, isSelected }), className)}>\n {children}\n </Radio>\n )\n}\n\nToggleButton.displayName = 'ToggleButton'\n\n// --- Styling ---\n\nfunction getToggleButtonClasses({ size = 'md', isSelected }: { size?: ButtonSize; isSelected?: boolean }) {\n return tw(\n 'relative rounded-none cursor-default select-none',\n 'font-medium transition-all',\n 'data-[focus-visible]:outline data-[focus-visible]:outline-2 data-[focus-visible]:outline-offset-2',\n 'data-[disabled]:bg-neutral-50 data-[disabled]:ring-neutral-50 data-[disabled]:cursor-not-allowed data-[disabled]:text-neutral-400',\n\n // Size\n size === 'xs' && 'px-3.5 py-1.5 text-xs gap-x-1 min-w-6',\n size === 'sm' && 'px-4 py-1.5 text-sm gap-x-1.5 min-w-7',\n size === 'md' && 'px-5 py-2 text-sm gap-x-2 min-w-8',\n size === 'lg' && 'px-6 py-2.5 text-base gap-x-2 min-w-9',\n size === 'xl' && 'px-8 py-3 text-base gap-x-2.5 min-w-10',\n\n // Unselected\n !isSelected && 'bg-white ring-1 ring-inset ring-neutral-300',\n !isSelected && 'data-[hovered]:bg-neutral-50 data-[hovered]:ring-neutral-400',\n !isSelected && 'text-neutral-700 data-[focus-visible]:outline-primary-600',\n\n // Selected\n isSelected && 'bg-primary-500 text-white ring-1 ring-inset ring-primary-700',\n isSelected && 'data-[hovered]:bg-primary-600 data-[hovered]:ring-primary-700',\n isSelected && 'data-[focus-visible]:outline-primary-600',\n\n 'inline-flex justify-center items-center gap-x-2'\n )\n}\n"}]},{"name":"date-picker","dependencies":["tw","animated-popover","calendar-month-year-picker","form-field","icon"],"files":[{"name":"components/date-picker.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add date-picker --directory app\n\nimport * as React from 'react'\nimport { DatePicker as AriaDatePicker, DateInput, DateSegment, Button, Dialog, I18nProvider } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport { CalendarDate, parseDate } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { CustomCalendar } from './helpers/calendar-month-year-picker'\nimport { dateFns, type Iso } from 'iso-fns'\n\nexport type DatePickerProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<Iso.Date | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (ISO date string YYYY-MM-DD)\n value?: Iso.Date | null\n defaultValue?: Iso.Date | null\n onChange?: (value: Iso.Date | null) => void\n\n // Date specific (ISO date strings)\n minValue?: Iso.Date\n maxValue?: Iso.Date\n isDateUnavailable?: (date: Iso.Date) => boolean\n\n // Calendar display\n locale?: string\n\n // Input shape\n shape?: 'rectangle' | 'oval'\n}\n\nfunction DatePicker_(props: DatePickerProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Internal state for CalendarDate\n const [value, _setValue] = React.useState(() => {\n try {\n const startingDate = props.value === undefined ? (props.defaultValue ?? null) : props.value\n return startingDate ? parseDate(startingDate) : null\n } catch {\n return null\n }\n })\n\n function setValue(d: CalendarDate | null) {\n _setValue(d)\n if (!d || dateFns.isValid(d.toString())) {\n props.onChange && props.onChange(d ? (d.toString() as Iso.Date) : null)\n }\n commitValidation()\n }\n\n // Sync controlled value\n if (props.value !== undefined) {\n const currentValueAsIso = value && dateFns.isValid(value.toString()) ? value.toString() : null\n const propValue = props.value && dateFns.isValid(props.value) ? props.value : null\n if ((!value || dateFns.isValid(value.toString())) && propValue !== currentValueAsIso) {\n try {\n _setValue(propValue ? parseDate(propValue) : null)\n } catch {\n _setValue(null)\n }\n }\n }\n\n // Convert min/max dates\n const minDate = React.useMemo(() => {\n if (!props.minValue) return undefined\n try {\n return parseDate(props.minValue)\n } catch {\n return undefined\n }\n }, [props.minValue])\n\n const maxDate = React.useMemo(() => {\n if (!props.maxValue) return undefined\n try {\n return parseDate(props.maxValue)\n } catch {\n return undefined\n }\n }, [props.maxValue])\n\n // Form validation\n const {\n validationMessage: validationMessageRaw,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: value && dateFns.isValid(value.toString()) ? (value.toString() as Iso.Date) : null,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by AriaDatePicker\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const lessThanMin =\n props.minValue && value && dateFns.isValid(value.toString()) ? value.toString() < props.minValue : false\n const greaterThanMax =\n props.maxValue && value && dateFns.isValid(value.toString()) ? value.toString() > props.maxValue : false\n\n const validationMessage =\n validationMessageRaw ||\n (lessThanMin ? `Cannot be less than ${dateFns.format(props.minValue!, 'MM/dd/yyyy')}` : '') ||\n (greaterThanMax ? `Cannot be greater than ${dateFns.format(props.maxValue!, 'MM/dd/yyyy')}` : '')\n\n const isInvalid = props.isInvalid || validationInvalid || lessThanMin || greaterThanMax\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value?.toString() ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaDatePicker\n value={value}\n aria-labelledby={labelId}\n onChange={setValue}\n minValue={minDate}\n maxValue={maxDate}\n isDateUnavailable={\n props.isDateUnavailable ? (date) => props.isDateUnavailable!(date.toString() as Iso.Date) : undefined\n }\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <DateInput className=\"flex-1 grow flex px-3 py-2 text-sm\">\n {(segment) => (\n <DateSegment\n segment={segment}\n className={tw('px-0.5 tabular-nums outline-none rounded-sm', 'focus:bg-primary-500 focus:text-white')}\n />\n )}\n </DateInput>\n <Button className=\"flex items-center px-2 py-2\">\n <Icon name=\"calendar\" className=\"h-4 w-4 text-neutral-400\" />\n </Button>\n </FormFieldComponents.FieldGroup>\n\n <AnimatedPopover>\n <Dialog>\n <CustomCalendar\n value={value}\n onChange={setValue}\n minValue={minDate}\n maxValue={maxDate}\n isDateUnavailable={\n props.isDateUnavailable ? (date) => props.isDateUnavailable!(date.toString() as Iso.Date) : undefined\n }\n />\n </Dialog>\n </AnimatedPopover>\n </AriaDatePicker>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n\nconst today = dateFns.now()\nexport const DatePicker = Object.assign(DatePicker_, {\n isDateInPast: (date: Iso.Date) => dateFns.isBefore(date, today)\n})\n"}]},{"name":"text-field","dependencies":["tw","form-field","headless-button","compose-refs","use-render-props"],"files":[{"name":"components/text-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add text-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessButton } from './helpers/headless-button'\n\nexport type TextFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string>\n\n // Value management\n value?: string\n defaultValue?: string\n onChange?: (value: string) => void\n onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>\n\n // Input type\n type?: 'text' | 'email' | 'password' | 'search' | 'tel' | 'url'\n shape?: 'rectangle' | 'oval'\n\n // Additional input props\n autoComplete?: string\n autoFocus?: boolean\n maxLength?: number\n minLength?: number\n pattern?: string\n inputMode?: React.HTMLAttributes<HTMLInputElement>['inputMode']\n\n trailingInlineAddon?: React.ReactNode\n}\n\nexport function TextField(props: TextFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const visibleRef = React.useRef<HTMLInputElement>(null)\n\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? '', props.onChange)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n visibleRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n setValue(e.target.value)\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={inputRef} name={props.name} value={value} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n {/* Input wrapper with state-based styling */}\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <FormFieldComponents.FieldInput\n id={inputId}\n type={props.type ?? 'text'}\n value={value}\n onChange={handleChange}\n onKeyDown={props.onKeyDown}\n onBlur={commitValidation}\n placeholder={props.placeholder}\n disabled={props.isDisabled}\n readOnly={props.isReadonly}\n autoComplete={props.autoComplete ?? 'off'}\n autoFocus={props.autoFocus}\n maxLength={props.maxLength}\n minLength={props.minLength}\n pattern={props.pattern}\n inputMode={props.inputMode}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n ref={visibleRef}\n />\n {props.trailingInlineAddon}\n </FormFieldComponents.FieldGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nexport function TrailingInlineAddonButton({ className, ...props }: React.ComponentProps<typeof HeadlessButton>) {\n return (\n <HeadlessButton\n {...props}\n className={(a) =>\n tw(\n 'inline-flex items-center inset-y-0 right-0 px-3 text-neutral-500 hover:bg-neutral-50 rounded-l sm:text-sm',\n typeof className === 'function' ? className(a) : className\n )\n }\n />\n )\n}\n"}]},{"name":"text-area","dependencies":["tw","form-field"],"files":[{"name":"components/text-area.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add text-area --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\n\nexport type TextAreaProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string>\n\n // Value management\n value?: string\n defaultValue?: string\n onChange?: (value: string) => void\n\n // TextArea specific\n rows?: number\n minRows?: number\n maxRows?: number\n autoResize?: boolean\n maxLength?: number\n resize?: 'none' | 'both' | 'horizontal' | 'vertical'\n shape?: 'rectangle' | 'oval'\n}\n\nexport function TextArea(props: TextAreaProps) {\n const autoResize = props.autoResize ?? true\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const textAreaRef = React.useRef<HTMLTextAreaElement>(null)\n const helpTextId = React.useId()\n const textAreaId = React.useId()\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? '', props.onChange)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n textAreaRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setValue(e.target.value)\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={textAreaId} />\n\n {/* TextArea wrapper with state-based styling */}\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className={autoResize ? 'grid' : undefined}\n >\n {/* Auto-expand helper element */}\n {autoResize && (\n <p\n className={tw(\n 'whitespace-pre-wrap overflow-hidden col-span-full row-span-full px-3 py-2 text-sm text-transparent pointer-events-none',\n 'border-0 m-0'\n )}\n style={{\n WebkitLineClamp: props.maxRows || undefined,\n WebkitBoxOrient: props.maxRows ? 'vertical' : undefined,\n display: props.maxRows ? '-webkit-box' : undefined,\n minHeight: props.minRows ? `${props.minRows * 1.5}rem` : undefined\n }}\n aria-hidden=\"true\"\n >\n {value ? value + ' ' : ' '}\n </p>\n )}\n\n <textarea\n ref={textAreaRef}\n id={textAreaId}\n className={tw(\n 'w-full bg-transparent px-3 py-2 border-0 focus:ring-0 focus:outline-none text-sm max-sm:text-base text-neutral-900 placeholder:text-neutral-400',\n autoResize && 'resize-none overflow-hidden col-span-full row-span-full',\n (props.isDisabled || props.isReadonly) && 'cursor-not-allowed',\n !autoResize && props.resize === 'none' && 'resize-none',\n !autoResize && props.resize === 'both' && 'resize',\n !autoResize && props.resize === 'horizontal' && 'resize-x',\n !autoResize && props.resize === 'vertical' && 'resize-y',\n !autoResize && !props.resize && 'resize-y'\n )}\n value={value}\n onChange={handleChange}\n onBlur={commitValidation}\n placeholder={props.placeholder}\n disabled={props.isDisabled}\n readOnly={props.isReadonly}\n maxLength={props.maxLength}\n rows={!autoResize ? props.rows || props.minRows || 3 : props.minRows || 3}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n />\n </FormFieldComponents.FieldGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n"}]},{"name":"number-field","dependencies":["tw","form-field","use-number-input"],"files":[{"name":"components/number-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add number-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { useNumberInput } from '../utils/use-number-input'\n\nexport type NumberFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<number>\n errorMessage?: React.ReactNode | ((v: import('@maestro-js/form').ValidationResult) => React.ReactNode)\n\n // Value management\n value?: number | null\n defaultValue?: number | null\n onChange?: (value: number | null) => void\n\n // Number specific\n min?: number | null\n max?: number | null\n step?: number | null\n formatOptions?: Intl.NumberFormatOptions\n autoComplete?: string | null\n autoFocus?: boolean\n shape?: 'rectangle' | 'oval'\n}\n\nexport function NumberField(props: NumberFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n const numberInput = useNumberInput({\n value: props.value ?? undefined,\n defaultValue: props.defaultValue ?? undefined,\n onChange: props.onChange,\n minValue: props.min ?? undefined,\n maxValue: props.max ?? undefined,\n step: props.step ?? undefined,\n formatOptions: props.formatOptions,\n locale: 'en-US',\n isDisabled: props.isDisabled,\n isReadOnly: props.isReadonly ?? false\n })\n\n HeadlessForm.useReset(inputRef, numberInput.numberValue, numberInput.setNumberValue)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n value: numberInput.numberValue as number,\n validate: props.validate,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n const handleBlur = () => {\n numberInput.commit(inputRef.current?.value)\n commitValidation()\n }\n\n return (\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n {/* Hidden input for form submission */}\n {props.isDisabled ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n form={props.form}\n name={props.name}\n value={numberInput.numberValue ?? ''}\n type=\"number\"\n min={numberInput.minValue}\n max={numberInput.maxValue}\n />\n )}\n\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <FormFieldComponents.FieldInput\n ref={inputRef}\n id={inputId}\n type=\"text\"\n value={numberInput.inputValue}\n onChange={(e) => numberInput.setInputValue(e.currentTarget.value)}\n onBlur={handleBlur}\n placeholder={props.placeholder}\n disabled={props.isDisabled}\n readOnly={props.isReadonly}\n autoComplete={props.autoComplete ?? undefined}\n autoFocus={props.autoFocus}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n />\n </FormFieldComponents.FieldGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n"}]},{"name":"currency-field","dependencies":["tw","form-field","use-number-input"],"files":[{"name":"components/currency-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add currency-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { useNumberInput } from '../utils/use-number-input'\n\nexport type CurrencyFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<number>\n errorMessage?: React.ReactNode | ((v: import('@maestro-js/form').ValidationResult) => React.ReactNode)\n\n // Value management (in the unit specified by `units`)\n value?: number | null\n defaultValue?: number | null\n onChange?: (value: number | null) => void\n\n // Number specific\n min?: number | null\n max?: number | null\n autoComplete?: string | null\n autoFocus?: boolean\n\n // Currency specific\n units: 'dollars' | 'cents'\n shape?: 'rectangle' | 'oval'\n}\n\nconst currencyFormatOptions: Intl.NumberFormatOptions = {\n style: 'currency',\n currency: 'USD',\n minimumFractionDigits: 2,\n maximumFractionDigits: 2\n}\n\nexport function CurrencyField(props: CurrencyFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n const isCents = props.units === 'cents'\n\n // Stable ref for onChange to avoid unnecessary re-renders in useNumberInput\n const onChangeRef = React.useRef(props.onChange)\n onChangeRef.current = props.onChange\n\n const handleChange = React.useCallback(\n (newValue: number | null) => {\n onChangeRef.current?.(isCents && newValue !== null ? dollarsToCents(newValue) : newValue)\n },\n [isCents]\n )\n\n const displayValue = isCents && props.value != null ? centsToDollars(props.value) : props.value\n const displayDefaultValue = isCents && props.defaultValue != null ? centsToDollars(props.defaultValue) : props.defaultValue\n const displayMin = isCents && props.min != null ? centsToDollars(props.min) : props.min\n const displayMax = isCents && props.max != null ? centsToDollars(props.max) : props.max\n\n const numberInput = useNumberInput({\n value: displayValue ?? undefined,\n defaultValue: displayDefaultValue ?? undefined,\n onChange: handleChange,\n minValue: displayMin ?? undefined,\n maxValue: displayMax ?? undefined,\n step: 0.01,\n formatOptions: currencyFormatOptions,\n locale: 'en-US',\n isDisabled: props.isDisabled,\n isReadOnly: props.isReadonly ?? false\n })\n\n HeadlessForm.useReset(inputRef, numberInput.numberValue, numberInput.setNumberValue)\n\n // Validation value should be in the original unit\n const validationValue = isCents ? dollarsToCents(numberInput.numberValue) : numberInput.numberValue\n\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: validationValue!,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Hidden input value in the original unit\n const hiddenValue = isCents ? dollarsToCents(numberInput.numberValue) : numberInput.numberValue\n\n const handleBlur = () => {\n numberInput.commit(inputRef.current?.value)\n commitValidation()\n }\n\n return (\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n {/* Hidden input for form submission */}\n {props.isDisabled ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n form={props.form}\n name={props.name}\n value={hiddenValue ?? ''}\n type=\"number\"\n min={numberInput.minValue}\n max={numberInput.maxValue}\n />\n )}\n\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <FormFieldComponents.FieldInput\n ref={inputRef}\n id={inputId}\n type=\"text\"\n value={numberInput.inputValue}\n onChange={(e) => numberInput.setInputValue(e.currentTarget.value)}\n onBlur={handleBlur}\n placeholder={props.placeholder}\n disabled={props.isDisabled}\n readOnly={props.isReadonly}\n autoComplete={props.autoComplete ?? undefined}\n autoFocus={props.autoFocus}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n />\n </FormFieldComponents.FieldGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nfunction centsToDollars(cents: number): number {\n return cents / 100\n}\n\nfunction dollarsToCents(dollars: number | null): number | null {\n if (dollars == null) return null\n return Math.round(dollars * 100)\n}\n"}]},{"name":"percentage-field","dependencies":["tw","form-field","use-number-input"],"files":[{"name":"components/percentage-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add percentage-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { useNumberInput } from '../utils/use-number-input'\n\nexport type PercentageFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<number>\n errorMessage?: React.ReactNode | ((v: import('@maestro-js/form').ValidationResult) => React.ReactNode)\n\n // Value management (in the unit specified by `units`)\n value?: number | null\n defaultValue?: number | null\n onChange?: (value: number | null) => void\n\n // Number specific\n min?: number | null\n max?: number | null\n autoComplete?: string | null\n autoFocus?: boolean\n\n // Percentage specific\n units: 'integer' | 'decimal'\n minimumFractionDigits?: number\n maximumFractionDigits?: number\n shape?: 'rectangle' | 'oval'\n}\n\nexport function PercentageField(props: PercentageFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n const isInteger = props.units === 'integer'\n\n // Stable ref for onChange to avoid unnecessary re-renders in useNumberInput\n const onChangeRef = React.useRef(props.onChange)\n onChangeRef.current = props.onChange\n\n const handleChange = React.useCallback(\n (newValue: number | null) => {\n onChangeRef.current?.(isInteger && newValue !== null ? decimalToInteger(newValue) : newValue)\n },\n [isInteger]\n )\n\n const formatOptions = React.useMemo<Intl.NumberFormatOptions>(\n () => ({\n style: 'percent',\n minimumFractionDigits: props.minimumFractionDigits,\n maximumFractionDigits: props.maximumFractionDigits\n }),\n [props.minimumFractionDigits, props.maximumFractionDigits]\n )\n\n const displayValue = isInteger && props.value != null ? integerToDecimal(props.value) : props.value\n const displayDefaultValue =\n isInteger && props.defaultValue != null ? integerToDecimal(props.defaultValue) : props.defaultValue\n const displayMin = isInteger && props.min != null ? integerToDecimal(props.min) : props.min\n const displayMax = isInteger && props.max != null ? integerToDecimal(props.max) : props.max\n\n const numberInput = useNumberInput({\n value: displayValue ?? undefined,\n defaultValue: displayDefaultValue ?? undefined,\n onChange: handleChange,\n minValue: displayMin ?? undefined,\n maxValue: displayMax ?? undefined,\n formatOptions,\n locale: 'en-US',\n isDisabled: props.isDisabled,\n isReadOnly: props.isReadonly ?? false\n })\n\n HeadlessForm.useReset(inputRef, numberInput.numberValue, numberInput.setNumberValue)\n\n // Validation value should be in the original unit\n const validationValue = isInteger ? decimalToInteger(numberInput.numberValue) : numberInput.numberValue\n\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: validationValue as number,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Hidden input value in the original unit\n const hiddenValue = isInteger ? decimalToInteger(numberInput.numberValue) : numberInput.numberValue\n\n const handleBlur = () => {\n numberInput.commit(inputRef.current?.value)\n commitValidation()\n }\n\n return (\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n {/* Hidden input for form submission */}\n {props.isDisabled ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n form={props.form}\n name={props.name}\n value={hiddenValue ?? ''}\n type=\"number\"\n min={numberInput.minValue}\n max={numberInput.maxValue}\n />\n )}\n\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <FormFieldComponents.FieldInput\n ref={inputRef}\n id={inputId}\n type=\"text\"\n value={numberInput.inputValue}\n onChange={(e) => numberInput.setInputValue(e.currentTarget.value)}\n onBlur={handleBlur}\n placeholder={props.placeholder}\n disabled={props.isDisabled}\n readOnly={props.isReadonly}\n autoComplete={props.autoComplete ?? undefined}\n autoFocus={props.autoFocus}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n />\n </FormFieldComponents.FieldGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nfunction integerToDecimal(value: number): number {\n return value / 100\n}\n\nfunction decimalToInteger(value: number | null): number | null {\n if (value == null) return null\n return Math.round(value * 100)\n}\n"}]},{"name":"phone-number-field","dependencies":["tw","form-field"],"files":[{"name":"components/phone-number-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add phone-number-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { FormFieldComponents } from './helpers/form-field'\n\nexport type PhoneNumberFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string>\n\n // Value management\n value?: string\n defaultValue?: string\n onChange?: (value: string) => void\n\n // Phone specific\n country?: 'US' | 'CA' | 'international'\n autoFormat?: boolean\n autoComplete?: string\n autoFocus?: boolean\n shape?: 'rectangle' | 'oval'\n}\n\nexport function PhoneNumberField(props: PhoneNumberFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? '', props.onChange)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Track whether field is focused for formatting toggle\n const [isFocused, setIsFocused] = React.useState(false)\n\n // Display value - formatted when not focused, raw when focused\n const displayValue = React.useMemo(() => {\n if (props.autoFormat === false) return value\n if (isFocused) return value\n return formatPhoneNumber(value, props.country)\n }, [value, props.autoFormat, props.country, isFocused])\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (props.autoFormat !== false) {\n setValue(getDigits(e.target.value))\n } else {\n setValue(e.target.value)\n }\n }\n\n const handleFocus = () => {\n setIsFocused(true)\n }\n\n const handleBlur = () => {\n setIsFocused(false)\n commitValidation()\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={inputRef} name={props.name} value={value} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <FormFieldComponents.FieldInput\n id={inputId}\n type=\"tel\"\n value={displayValue}\n onChange={handleChange}\n onFocus={handleFocus}\n onBlur={handleBlur}\n placeholder={props.placeholder ?? (props.country === 'international' ? '+1234567890' : '(555) 555-5555')}\n disabled={props.isDisabled}\n readOnly={props.isReadonly}\n autoComplete={props.autoComplete ?? 'tel'}\n autoFocus={props.autoFocus}\n inputMode=\"tel\"\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n />\n </FormFieldComponents.FieldGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\n// Format phone number for display\nfunction formatPhoneNumber(value: string, country: 'US' | 'CA' | 'international' = 'US'): string {\n const digits = value.replace(/\\D/g, '')\n\n if (country === 'US' || country === 'CA') {\n if (digits.length === 0) return ''\n if (digits.length <= 3) return `(${digits}`\n if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`\n if (digits.length <= 10) return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`\n // Handle country code\n if (digits.length === 11 && digits[0] === '1') {\n return `+1 (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7)}`\n }\n return value\n }\n\n // International - just add + prefix for readability\n if (digits.length > 0 && !value.startsWith('+')) {\n return '+' + digits\n }\n return value\n}\n\n// Get raw digits from formatted phone\nfunction getDigits(value: string): string {\n return value.replace(/\\D/g, '')\n}\n"}]},{"name":"tag-field","dependencies":["tw","form-field","icon"],"files":[{"name":"components/tag-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add tag-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\n\nexport type TagFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string[]>\n\n // Value management\n value?: string[]\n defaultValue?: string[]\n onChange?: (value: string[]) => void\n\n // Tag specific\n maxTags?: number\n hideTagCount?: boolean\n allowDuplicates?: boolean\n separator?: string | RegExp | (string | RegExp)[]\n validateTag?: (tag: string) => boolean\n transformTag?: (tag: string) => string\n\n // Custom rendering\n renderTag?: (tag: string, index: number, onRemove: () => void, isDisabled?: boolean) => React.ReactNode\n shape?: 'rectangle' | 'oval'\n}\n\nexport function TagField(props: TagFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n const [query, setQuery] = React.useState('')\n\n // Controlled state management\n const [tags, setTags] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? [], props.onChange)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: tags,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n const separators = React.useMemo(() => {\n const sep = props.separator ?? [',', '\\n']\n return Array.isArray(sep) ? sep : [sep]\n }, [props.separator])\n\n const addTag = React.useCallback(\n (tag: string) => {\n const trimmed = tag.trim()\n if (!trimmed) return\n\n const final = props.transformTag ? props.transformTag(trimmed) : trimmed\n\n if (props.validateTag && !props.validateTag(final)) return\n if (!props.allowDuplicates && tags.includes(final)) return\n if (props.maxTags !== undefined && tags.length >= props.maxTags) return\n\n setTags([...tags, final])\n setQuery('')\n commitValidation()\n },\n [tags, setTags, commitValidation, props.transformTag, props.validateTag, props.allowDuplicates, props.maxTags]\n )\n\n const removeTag = React.useCallback(\n (index: number) => {\n setTags(tags.filter((_, i) => i !== index))\n commitValidation()\n },\n [tags, setTags, commitValidation]\n )\n\n const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const value = e.target.value\n\n for (const sep of separators) {\n const matches = typeof sep === 'string' ? value.includes(sep) : sep.test(value)\n if (matches) {\n const parts = typeof sep === 'string' ? value.split(sep) : value.split(sep)\n for (const part of parts) {\n if (part.trim()) addTag(part)\n }\n setQuery('')\n return\n }\n }\n\n setQuery(value)\n }\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n if (query) addTag(query)\n } else if (e.key === 'Backspace' && !query && tags.length > 0) {\n removeTag(tags.length - 1)\n }\n }\n\n const atMax = props.maxTags !== undefined && tags.length >= props.maxTags\n const isDisabled = props.isDisabled || atMax\n\n return (\n <div className=\"relative\">\n {/* Hidden inputs for form submission */}\n {tags.map((tag, i) => (\n <HeadlessForm.HiddenInput\n key={i}\n ref={i === 0 ? hiddenInputRef : undefined}\n name={props.name}\n value={tag}\n form={props.form}\n />\n ))}\n {tags.length === 0 && <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value=\"\" form={props.form} />}\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n onClick={() => inputRef.current?.focus()}\n >\n <div className=\"flex flex-wrap items-center gap-1.5 px-3 py-1.5 min-h-[38px] w-full\">\n {/* Display existing tags */}\n {tags.map((tag, index) =>\n props.renderTag ? (\n <React.Fragment key={index}>\n {props.renderTag(tag, index, () => removeTag(index), props.isDisabled)}\n </React.Fragment>\n ) : (\n <Tag\n key={index}\n onRemove={!props.isDisabled ? () => removeTag(index) : undefined}\n isDisabled={props.isDisabled}\n >\n {tag}\n </Tag>\n )\n )}\n\n {/* Text input for adding tags */}\n <input\n id={inputId}\n ref={inputRef}\n className={tw(\n 'flex-1 min-w-[120px] bg-transparent p-0 border-0 focus:ring-0 focus:outline-none text-sm text-neutral-900 placeholder:text-neutral-400',\n props.isDisabled && 'cursor-not-allowed'\n )}\n value={query}\n onChange={handleInputChange}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n if (query) addTag(query)\n commitValidation()\n }}\n placeholder={\n tags.length === 0 ? (props.placeholder ?? 'Add tags...') : atMax ? `Maximum ${props.maxTags} tags` : ''\n }\n disabled={isDisabled}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n autoComplete=\"off\"\n />\n </div>\n </FormFieldComponents.FieldGroup>\n\n {/* Tag count */}\n {!props.hideTagCount && props.maxTags !== undefined && (\n <div className=\"mt-1 text-xs text-neutral-500\">\n {tags.length} / {props.maxTags} tags\n </div>\n )}\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nfunction Tag({\n children,\n onRemove,\n isDisabled\n}: {\n children: React.ReactNode\n onRemove?: () => void\n isDisabled?: boolean\n}) {\n return (\n <span\n className={tw(\n 'inline-flex items-center rounded-full pl-2 pr-1.5 py-0.5 text-xs font-normal bg-primary-500/10 text-primary-500',\n isDisabled && 'opacity-50'\n )}\n >\n <span className=\"truncate\">{children}</span>\n {onRemove && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n onRemove()\n }}\n className=\"inline-flex items-center justify-center size-4 -m-0.5 p-0.5 rounded-full translate-x-1 hover:bg-neutral-950/10\"\n aria-label=\"Remove\"\n tabIndex={-1}\n >\n <Icon name=\"x-mark\" className=\"size-2\" aria-hidden=\"true\" />\n </button>\n )}\n </span>\n )\n}\n"}]},{"name":"numeric-tag-field","dependencies":["tw","form-field","icon"],"files":[{"name":"components/numeric-tag-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add numeric-tag-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\n\nexport type NumericTagFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<number[]>\n\n // Value management\n value?: number[]\n defaultValue?: number[]\n onChange?: (value: number[]) => void\n\n // Tag specific\n maxTags?: number\n hideTagCount?: boolean\n allowDuplicates?: boolean\n separator?: string | RegExp | (string | RegExp)[]\n validateTag?: (tag: number) => boolean\n transformTag?: (tag: number) => number\n min?: number\n max?: number\n shape?: 'rectangle' | 'oval'\n}\n\nexport function NumericTagField(props: NumericTagFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n const [query, setQuery] = React.useState('')\n\n // Controlled state management\n const [tags, setTags] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? [], props.onChange)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: tags,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n const separators = React.useMemo(() => {\n const sep = props.separator ?? [',', '\\n']\n return Array.isArray(sep) ? sep : [sep]\n }, [props.separator])\n\n const addTag = React.useCallback(\n (raw: string) => {\n const trimmed = raw.trim()\n if (!trimmed || Number.isNaN(Number(trimmed))) return\n\n let num = Number(trimmed)\n\n // Transform tag if needed\n if (props.transformTag) num = props.transformTag(num)\n\n // Check min/max bounds\n if (props.min !== undefined && num < props.min) return\n if (props.max !== undefined && num > props.max) return\n\n // Validate tag\n if (props.validateTag && !props.validateTag(num)) return\n\n // Check for duplicates\n if (!props.allowDuplicates && tags.includes(num)) return\n\n // Check max tags\n if (props.maxTags !== undefined && tags.length >= props.maxTags) return\n\n setTags([...tags, num])\n setQuery('')\n commitValidation()\n },\n [\n tags,\n setTags,\n commitValidation,\n props.transformTag,\n props.validateTag,\n props.allowDuplicates,\n props.maxTags,\n props.min,\n props.max\n ]\n )\n\n const removeTag = React.useCallback(\n (index: number) => {\n setTags(tags.filter((_, i) => i !== index))\n commitValidation()\n },\n [tags, setTags, commitValidation]\n )\n\n const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const value = e.target.value\n\n for (const sep of separators) {\n const matches = typeof sep === 'string' ? value.includes(sep) : sep.test(value)\n if (matches) {\n const parts = typeof sep === 'string' ? value.split(sep) : value.split(sep)\n for (const part of parts) {\n if (part.trim()) addTag(part)\n }\n setQuery('')\n return\n }\n }\n\n setQuery(value)\n }\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n if (query) addTag(query)\n } else if (e.key === 'Backspace' && !query && tags.length > 0) {\n removeTag(tags.length - 1)\n }\n }\n\n const atMax = props.maxTags !== undefined && tags.length >= props.maxTags\n const isDisabled = props.isDisabled || atMax\n\n const defaultPlaceholder =\n props.min !== undefined && props.max !== undefined\n ? `Add numbers (${props.min}–${props.max})...`\n : props.min !== undefined\n ? `Add numbers (min: ${props.min})...`\n : props.max !== undefined\n ? `Add numbers (max: ${props.max})...`\n : 'Add numbers...'\n\n return (\n <div className=\"relative\">\n {/* Hidden inputs for form submission */}\n {tags.map((tag, i) => (\n <HeadlessForm.HiddenInput\n key={i}\n ref={i === 0 ? hiddenInputRef : undefined}\n name={props.name}\n value={tag}\n form={props.form}\n />\n ))}\n {tags.length === 0 && <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value=\"\" form={props.form} />}\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n onClick={() => inputRef.current?.focus()}\n >\n <div className=\"flex flex-wrap items-center gap-1.5 px-3 py-1.5 min-h-[38px] w-full\">\n {/* Display existing tags */}\n {tags.map((tag, index) => (\n <Tag key={index} onRemove={!props.isDisabled ? () => removeTag(index) : undefined} isDisabled={props.isDisabled}>\n {tag}\n </Tag>\n ))}\n\n {/* Text input for adding tags */}\n <input\n id={inputId}\n ref={inputRef}\n className={tw(\n 'flex-1 min-w-[120px] bg-transparent p-0 border-0 focus:ring-0 focus:outline-none text-sm text-neutral-900 placeholder:text-neutral-400',\n props.isDisabled && 'cursor-not-allowed'\n )}\n value={query}\n onChange={handleInputChange}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n if (query) addTag(query)\n commitValidation()\n }}\n placeholder={\n tags.length === 0 ? (props.placeholder ?? defaultPlaceholder) : atMax ? `Maximum ${props.maxTags} tags` : ''\n }\n disabled={isDisabled}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n autoComplete=\"off\"\n inputMode=\"numeric\"\n />\n </div>\n </FormFieldComponents.FieldGroup>\n\n {/* Tag count */}\n {!props.hideTagCount && props.maxTags !== undefined && (\n <div className=\"mt-1 text-xs text-neutral-500\">\n {tags.length} / {props.maxTags} tags\n </div>\n )}\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nfunction Tag({\n children,\n onRemove,\n isDisabled\n}: {\n children: React.ReactNode\n onRemove?: () => void\n isDisabled?: boolean\n}) {\n return (\n <span\n className={tw(\n 'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-normal bg-primary-500/10 text-primary-500',\n isDisabled && 'opacity-50'\n )}\n >\n <span className=\"truncate\">{children}</span>\n {onRemove && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n onRemove()\n }}\n className=\"inline-flex items-center justify-center h-4 w-4 -m-0.5 p-0.5 rounded-full translate-x-1 hover:bg-black/10\"\n aria-label=\"Remove\"\n tabIndex={-1}\n >\n <Icon name=\"x-mark\" className=\"h-2 w-2\" aria-hidden=\"true\" />\n </button>\n )}\n </span>\n )\n}\n"}]},{"name":"date-input","dependencies":["tw","form-field"],"files":[{"name":"components/date-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add date-input --directory app\n\nimport * as React from 'react'\nimport { DateField, DateInput as AriaDateInput, DateSegment, I18nProvider } from 'react-aria-components'\nimport { CalendarDate, parseDate } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { dateFns, type Iso } from 'iso-fns'\n\nexport type DateInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<Iso.Date | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (ISO date string YYYY-MM-DD)\n value?: Iso.Date | null\n defaultValue?: Iso.Date | null\n onChange?: (value: Iso.Date | null) => void\n\n // Date specific (ISO date strings)\n minValue?: Iso.Date\n maxValue?: Iso.Date\n isDateUnavailable?: (date: Iso.Date) => boolean\n\n // Locale\n locale?: string\n shape?: 'rectangle' | 'oval'\n}\n\nexport function DateInput(props: DateInputProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Internal state for CalendarDate\n const [value, _setValue] = React.useState(() => {\n try {\n const startingDate = props.value === undefined ? (props.defaultValue ?? null) : props.value\n return startingDate ? parseDate(startingDate) : null\n } catch {\n return null\n }\n })\n\n function setValue(d: CalendarDate | null) {\n _setValue(d)\n if (!d || dateFns.isValid(d.toString())) {\n props.onChange && props.onChange(d ? (d.toString() as Iso.Date) : null)\n }\n commitValidation()\n }\n\n // Sync controlled value\n if (props.value !== undefined) {\n const currentValueAsIso = value && dateFns.isValid(value.toString()) ? value.toString() : null\n const propValue = props.value && dateFns.isValid(props.value) ? props.value : null\n if ((!value || dateFns.isValid(value.toString())) && propValue !== currentValueAsIso) {\n try {\n _setValue(propValue ? parseDate(propValue) : null)\n } catch {\n _setValue(null)\n }\n }\n }\n\n // Convert min/max dates\n const minDate = React.useMemo(() => {\n if (!props.minValue) return undefined\n try {\n return parseDate(props.minValue)\n } catch {\n return undefined\n }\n }, [props.minValue])\n\n const maxDate = React.useMemo(() => {\n if (!props.maxValue) return undefined\n try {\n return parseDate(props.maxValue)\n } catch {\n return undefined\n }\n }, [props.maxValue])\n\n // Form validation\n const {\n validationMessage: validationMessageRaw,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: value && dateFns.isValid(value.toString()) ? (value.toString() as Iso.Date) : null,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by DateField\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const lessThanMin =\n props.minValue && value && dateFns.isValid(value.toString()) ? value.toString() < props.minValue : false\n const greaterThanMax =\n props.maxValue && value && dateFns.isValid(value.toString()) ? value.toString() > props.maxValue : false\n\n const validationMessage =\n validationMessageRaw ||\n (lessThanMin ? `Cannot be less than ${dateFns.format(props.minValue!, 'MM/dd/yyyy')}` : '') ||\n (greaterThanMax ? `Cannot be greater than ${dateFns.format(props.maxValue!, 'MM/dd/yyyy')}` : '')\n\n const isInvalid = props.isInvalid || validationInvalid || lessThanMin || greaterThanMax\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value?.toString() ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n {/* Date input */}\n <DateField\n aria-labelledby={labelId}\n aria-describedby={helpTextId}\n value={value}\n onChange={setValue}\n minValue={minDate}\n maxValue={maxDate}\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n isDateUnavailable={\n props.isDateUnavailable ? (date) => props.isDateUnavailable!(date.toString() as Iso.Date) : undefined\n }\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n >\n <AriaDateInput className=\"flex px-3 py-2 text-sm\">\n {(segment) => (\n <DateSegment\n segment={segment}\n className={tw(\n 'px-0.5 tabular-nums outline-none rounded-sm',\n 'focus:bg-primary-500 focus:text-white',\n segment.isPlaceholder && 'text-neutral-400'\n )}\n />\n )}\n </AriaDateInput>\n </FormFieldComponents.FieldGroup>\n </DateField>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n"}]},{"name":"date-range-picker","dependencies":["tw","animated-popover","form-field","icon"],"files":[{"name":"components/date-range-picker.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add date-range-picker --directory app\n\nimport * as React from 'react'\nimport {\n DateRangePicker as AriaDateRangePicker,\n DateInput,\n DateSegment,\n Button,\n Dialog,\n RangeCalendar,\n CalendarGrid,\n CalendarCell,\n Heading,\n CalendarGridHeader,\n CalendarHeaderCell,\n CalendarGridBody,\n I18nProvider\n} from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport { CalendarDate, parseDate } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\n\nexport type DateRange = {\n start: string | null\n end: string | null\n}\n\nexport type DateRangePickerProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<DateRange>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: DateRange\n defaultValue?: DateRange\n onChange?: (value: DateRange) => void\n\n // Date specific (ISO date strings)\n minValue?: string\n maxValue?: string\n presetRanges?: Array<{\n label: string\n start: string\n end: string\n }>\n\n // Calendar display\n locale?: string\n shape?: 'rectangle' | 'oval'\n}\n\nexport function DateRangePicker(props: DateRangePickerProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState<DateRange>(\n props.value,\n props.defaultValue ?? { start: null, end: null },\n props.onChange\n )\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by AriaDateRangePicker\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Convert ISO strings to CalendarDate for aria components\n const ariaValue = React.useMemo(() => {\n if (!value.start || !value.end) return null\n try {\n return {\n start: parseDate(value.start),\n end: parseDate(value.end)\n }\n } catch {\n return null\n }\n }, [value])\n\n const minDate = React.useMemo(() => {\n if (!props.minValue) return undefined\n try {\n return parseDate(props.minValue)\n } catch {\n return undefined\n }\n }, [props.minValue])\n\n const maxDate = React.useMemo(() => {\n if (!props.maxValue) return undefined\n try {\n return parseDate(props.maxValue)\n } catch {\n return undefined\n }\n }, [props.maxValue])\n\n const handleRangeChange = (range: { start: CalendarDate; end: CalendarDate } | null) => {\n if (!range) {\n setValue({ start: null, end: null })\n } else {\n setValue({\n start: range.start.toString(),\n end: range.end.toString()\n })\n }\n commitValidation()\n }\n\n const handlePresetClick = (preset: { start: string; end: string }) => {\n setValue({ start: preset.start, end: preset.end })\n commitValidation()\n }\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden inputs for form submission */}\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name ? `${props.name}.start` : undefined}\n value={value.start || ''}\n form={props.form}\n />\n <HeadlessForm.HiddenInput\n name={props.name ? `${props.name}.end` : undefined}\n value={value.end || ''}\n form={props.form}\n />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n {/* Preset ranges */}\n {props.presetRanges && props.presetRanges.length > 0 && (\n <div className=\"flex flex-wrap gap-2 mb-2\">\n {props.presetRanges.map((preset, i) => (\n <button\n key={i}\n type=\"button\"\n onClick={() => handlePresetClick(preset)}\n disabled={props.isDisabled}\n className=\"px-2 py-1 text-xs rounded border border-neutral-200 hover:bg-neutral-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {preset.label}\n </button>\n ))}\n </div>\n )}\n\n {/* Date range picker */}\n <AriaDateRangePicker\n aria-labelledby={labelId}\n value={ariaValue}\n onChange={handleRangeChange}\n minValue={minDate}\n maxValue={maxDate}\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <div className=\"flex items-center justify-start flex-1 px-3 py-2 text-sm\">\n <DateInput slot=\"start\" className=\"flex\">\n {(segment) => (\n <DateSegment\n segment={segment}\n className={tw('px-0.5 tabular-nums outline-none rounded-sm', 'focus:bg-primary-500 focus:text-white')}\n />\n )}\n </DateInput>\n <span className=\"mx-2 text-neutral-400\">&ndash;</span>\n <DateInput slot=\"end\" className=\"flex\">\n {(segment) => (\n <DateSegment\n segment={segment}\n className={tw('px-0.5 tabular-nums outline-none rounded-sm', 'focus:bg-primary-500 focus:text-white')}\n />\n )}\n </DateInput>\n </div>\n <Button className=\"flex items-center px-2 py-2\">\n <Icon name=\"calendar\" className=\"h-4 w-4 text-neutral-400\" />\n </Button>\n </FormFieldComponents.FieldGroup>\n\n <AnimatedPopover>\n <Dialog>\n <RangeCalendar className=\"bg-white border border-neutral-200 rounded-md shadow-lg p-3\">\n <header className=\"flex items-center justify-between mb-2\">\n <Button slot=\"previous\" className=\"p-1 hover:bg-neutral-100 rounded\">\n <Icon name=\"chevron-left\" className=\"h-4 w-4\" />\n </Button>\n <Heading className=\"text-sm font-semibold text-neutral-900 flex-1 text-center\" />\n <Button slot=\"next\" className=\"p-1 hover:bg-neutral-100 rounded\">\n <Icon name=\"chevron-right\" className=\"h-4 w-4\" />\n </Button>\n </header>\n <CalendarGrid>\n <CalendarGridHeader>\n {(day) => (\n <CalendarHeaderCell className=\"text-xs text-neutral-500 font-medium w-8 h-8\">{day}</CalendarHeaderCell>\n )}\n </CalendarGridHeader>\n <CalendarGridBody>\n {(date) => (\n <CalendarCell\n date={date}\n className={({\n isSelected,\n isSelectionStart,\n isSelectionEnd,\n isOutsideMonth,\n isDisabled,\n isUnavailable,\n isFocusVisible\n }) =>\n tw(\n 'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center',\n 'hover:bg-neutral-100',\n isOutsideMonth && 'text-neutral-300',\n (isDisabled || isUnavailable) && 'text-neutral-300 cursor-not-allowed',\n isSelected && !isSelectionStart && !isSelectionEnd && 'bg-primary-500/20',\n (isSelectionStart || isSelectionEnd) && 'bg-primary-500 text-white hover:bg-primary-500',\n isFocusVisible && 'ring-2 ring-primary-500 ring-offset-1'\n )\n }\n />\n )}\n </CalendarGridBody>\n </CalendarGrid>\n </RangeCalendar>\n </Dialog>\n </AnimatedPopover>\n </AriaDateRangePicker>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n"}]},{"name":"date-time-picker","dependencies":["tw","animated-popover","calendar-month-year-picker","form-field","icon"],"files":[{"name":"components/date-time-picker.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add date-time-picker --directory app\n\nimport * as React from 'react'\nimport { DatePicker as AriaDatePicker, DateInput, DateSegment, Button, Dialog, I18nProvider } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport { CalendarDateTime, parseDateTime } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { CustomCalendar } from './helpers/calendar-month-year-picker'\nimport { dateFns, dateTimeFns, type Iso } from 'iso-fns'\n\nexport type DateTimePickerProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<Iso.DateTime | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (ISO datetime string e.g. 2024-01-15T09:30:00)\n value?: Iso.DateTime | null\n defaultValue?: Iso.DateTime | null\n onChange?: (value: Iso.DateTime | null) => void\n\n // Date/Time specific\n minValue?: Iso.DateTime\n maxValue?: Iso.DateTime\n use24Hour?: boolean\n minuteStep?: number\n locale?: string\n shape?: 'rectangle' | 'oval'\n}\n\nexport function DateTimePicker(props: DateTimePickerProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Internal state for CalendarDateTime\n const [value, _setValue] = React.useState(() => {\n try {\n const startingValue = props.value === undefined ? (props.defaultValue ?? null) : props.value\n return startingValue ? parseDateTime(startingValue) : null\n } catch {\n return null\n }\n })\n\n function setValue(dt: CalendarDateTime | null) {\n _setValue(dt)\n if (!dt || dateTimeFns.isValid(dt.toString())) {\n props.onChange && props.onChange(dt ? (dt.toString() as Iso.DateTime) : null)\n }\n commitValidation()\n }\n\n // Sync controlled value\n if (props.value !== undefined) {\n const currentValueAsIso = value && dateTimeFns.isValid(value.toString()) ? value.toString() : null\n const propValue = props.value && dateTimeFns.isValid(props.value) ? props.value : null\n if ((!value || dateTimeFns.isValid(value.toString())) && propValue !== currentValueAsIso) {\n try {\n _setValue(propValue ? parseDateTime(propValue) : null)\n } catch {\n _setValue(null)\n }\n }\n }\n\n // Convert min/max values\n const minDateTime = React.useMemo(() => {\n if (!props.minValue) return undefined\n try {\n return parseDateTime(props.minValue)\n } catch {\n return undefined\n }\n }, [props.minValue])\n\n const maxDateTime = React.useMemo(() => {\n if (!props.maxValue) return undefined\n try {\n return parseDateTime(props.maxValue)\n } catch {\n return undefined\n }\n }, [props.maxValue])\n\n // Form validation\n const {\n validationMessage: validationMessageRaw,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: value && dateTimeFns.isValid(value.toString()) ? (value.toString() as Iso.DateTime) : null,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by AriaDatePicker\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const lessThanMin =\n props.minValue && value && dateTimeFns.isValid(value.toString()) ? value.toString() < props.minValue : false\n const greaterThanMax =\n props.maxValue && value && dateTimeFns.isValid(value.toString()) ? value.toString() > props.maxValue : false\n\n const validationMessage =\n validationMessageRaw ||\n (lessThanMin ? `Cannot be before ${dateTimeFns.format(props.minValue!, 'MM/dd/yyyy h:mm a')}` : '') ||\n (greaterThanMax ? `Cannot be after ${dateTimeFns.format(props.maxValue!, 'MM/dd/yyyy h:mm a')}` : '')\n\n const isInvalid = props.isInvalid || validationInvalid || lessThanMin || greaterThanMax\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value?.toString() ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaDatePicker\n value={value}\n aria-labelledby={labelId}\n aria-describedby={helpTextId}\n onChange={setValue}\n minValue={minDateTime}\n maxValue={maxDateTime}\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n granularity=\"minute\"\n hideTimeZone\n hourCycle={props.use24Hour ? 24 : 12}\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <DateInput className=\"flex-1 grow flex px-3 py-2 text-sm\">\n {(segment) => (\n <DateSegment\n segment={segment}\n className={tw('px-0.5 tabular-nums outline-none rounded-sm', 'focus:bg-primary-500 focus:text-white')}\n />\n )}\n </DateInput>\n <Button className=\"flex items-center px-2 py-2\">\n <Icon name=\"calendar\" className=\"h-4 w-4 text-neutral-400\" />\n </Button>\n </FormFieldComponents.FieldGroup>\n\n <AnimatedPopover>\n <Dialog>\n <CustomCalendar value={value} onChange={setValue} minValue={minDateTime} maxValue={maxDateTime} />\n </Dialog>\n </AnimatedPopover>\n </AriaDatePicker>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n"}]},{"name":"time-input","dependencies":["tw","form-field"],"files":[{"name":"components/time-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add time-input --directory app\n\nimport * as React from 'react'\nimport { TimeField, DateInput, DateSegment, I18nProvider } from 'react-aria-components'\nimport { Time, parseTime } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { timeFns, type Iso } from 'iso-fns'\n\nexport type TimeInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<Iso.Time | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (IsoTimeString format HH:mm:ss)\n value?: Iso.Time | null\n defaultValue?: Iso.Time | null\n onChange?: (value: Iso.Time | null) => void\n\n // Time specific\n use24Hour?: boolean\n minuteStep?: number\n minTime?: Iso.Time\n maxTime?: Iso.Time\n\n // Locale\n locale?: string\n shape?: 'rectangle' | 'oval'\n}\n\nexport function TimeInput(props: TimeInputProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Internal state for Time object\n const [value, _setValue] = React.useState<Time | null>(() => {\n try {\n const startingTime = props.value === undefined ? (props.defaultValue ?? null) : props.value\n return startingTime ? parseTime(startingTime) : null\n } catch {\n return null\n }\n })\n\n function setValue(t: Time | null) {\n _setValue(t)\n const isoTime = t ? (t.toString() as Iso.Time) : null\n props.onChange?.(isoTime)\n commitValidation()\n }\n\n // Sync controlled value\n if (props.value !== undefined) {\n const currentValueAsIso = value && isValidTimeString(value.toString()) ? value.toString() : null\n const propValue = props.value && isValidTimeString(props.value) ? props.value : null\n if ((!value || isValidTimeString(value.toString())) && propValue !== currentValueAsIso) {\n try {\n _setValue(propValue ? parseTime(propValue) : null)\n } catch {\n _setValue(null)\n }\n }\n }\n\n // Convert min/max times\n const minTime = React.useMemo(() => {\n if (!props.minTime) return undefined\n try {\n return parseTime(props.minTime)\n } catch {\n return undefined\n }\n }, [props.minTime])\n\n const maxTime = React.useMemo(() => {\n if (!props.maxTime) return undefined\n try {\n return parseTime(props.maxTime)\n } catch {\n return undefined\n }\n }, [props.maxTime])\n\n // Form validation\n const {\n validationMessage: validationMessageRaw,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: value && isValidTimeString(value.toString()) ? (value.toString() as Iso.Time) : null,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by TimeField\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const lessThanMin =\n props.minTime && value && isValidTimeString(value.toString()) ? value.toString() < props.minTime : false\n const greaterThanMax =\n props.maxTime && value && isValidTimeString(value.toString()) ? value.toString() > props.maxTime : false\n\n const validationMessage =\n validationMessageRaw ||\n (lessThanMin ? `Cannot be before ${formatTimeString(props.minTime!, 'h:mm a')}` : '') ||\n (greaterThanMax ? `Cannot be after ${formatTimeString(props.maxTime!, 'h:mm a')}` : '')\n\n const isInvalid = props.isInvalid || validationInvalid || lessThanMin || greaterThanMax\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value?.toString() ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n {/* Time field */}\n <TimeField\n aria-labelledby={labelId}\n aria-describedby={helpTextId}\n value={value}\n onChange={setValue}\n isRequired={props.isRequired}\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n minValue={minTime}\n maxValue={maxTime}\n hourCycle={props.use24Hour ? 24 : 12}\n granularity=\"minute\"\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n >\n <DateInput className=\"flex w-full bg-transparent px-3 py-2 text-sm\">\n {(segment) => (\n <DateSegment\n segment={segment}\n className={tw(\n 'px-0.5 tabular-nums outline-none rounded-sm',\n 'focus:bg-primary-500 focus:text-white',\n segment.isPlaceholder && 'text-neutral-400'\n )}\n />\n )}\n </DateInput>\n </FormFieldComponents.FieldGroup>\n </TimeField>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n\n/** Check if a string is a valid HH:mm:ss time string */\nfunction isValidTimeString(timeStr: string): boolean {\n return timeFns.isValid(timeStr)\n}\n\n/** Format an HH:mm:ss time string using a date-fns format pattern */\nfunction formatTimeString(timeStr: Iso.Time, fmt: string): string {\n return timeFns.format(timeStr, fmt)\n}\n"}]},{"name":"month-day-input","dependencies":["tw","form-field"],"files":[{"name":"components/month-day-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add month-day-input --directory app\n\nimport * as React from 'react'\nimport { DateField, DateInput as AriaDateInput, DateSegment, I18nProvider } from 'react-aria-components'\nimport { CalendarDate } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { monthDayFns, type Iso } from 'iso-fns'\n\nexport type MonthDayInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<Iso.MonthDay | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (ISO month-day string --MM-DD)\n value?: Iso.MonthDay | null\n defaultValue?: Iso.MonthDay | null\n onChange?: (value: Iso.MonthDay | null) => void\n\n // Locale\n locale?: string\n shape?: 'rectangle' | 'oval'\n}\n\nexport function MonthDayInput(props: MonthDayInputProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Internal state for CalendarDate (using placeholder year 1972)\n const [value, _setValue] = React.useState(() => {\n try {\n const startingDate = props.value === undefined ? (props.defaultValue ?? null) : props.value\n return startingDate ? parseMonthDay(startingDate) : null\n } catch {\n return null\n }\n })\n\n function setValue(d: CalendarDate | null) {\n _setValue(d)\n const monthDay = formatMonthDay(d)\n props.onChange?.(monthDay)\n commitValidation()\n }\n\n // Sync controlled value\n if (props.value !== undefined) {\n const currentMonthDay = formatMonthDay(value)\n if (props.value !== currentMonthDay) {\n try {\n _setValue(props.value ? parseMonthDay(props.value) : null)\n } catch {\n _setValue(null)\n }\n }\n }\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: formatMonthDay(value),\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by DateField\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n value={formatMonthDay(value) ?? ''}\n form={props.form}\n />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n {/* Month/Day input using DateField */}\n <DateField\n aria-labelledby={labelId}\n aria-describedby={helpTextId}\n value={value}\n onChange={setValue}\n placeholderValue={new CalendarDate(1972, 1, 1)}\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n granularity=\"day\"\n hideTimeZone\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n >\n <AriaDateInput className=\"flex px-3 py-2 text-sm [&>[data-literal]:not(:has(~[data-literal]))]:hidden\">\n {(segment) => {\n const isHidden = segment.type === 'year'\n\n return (\n <DateSegment\n segment={segment}\n data-literal={segment.type === 'literal' ? true : undefined}\n className={tw(\n 'px-0.5 tabular-nums outline-none rounded-sm',\n 'focus:bg-primary-500 focus:text-white',\n segment.isPlaceholder && 'text-neutral-400',\n 'last-of-type:hidden',\n isHidden && 'hidden'\n )}\n />\n )\n }}\n </AriaDateInput>\n </FormFieldComponents.FieldGroup>\n </DateField>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n\nfunction parseMonthDay(monthDay: Iso.MonthDay): CalendarDate | null {\n if (!monthDayFns.isValid(monthDay)) return null\n // Use a leap-year placeholder so Feb 29 is valid\n const { month, day } = monthDayFns.getFields(monthDay)\n return new CalendarDate(1972, month, day)\n}\n\nfunction formatMonthDay(date: CalendarDate | null): Iso.MonthDay | null {\n if (!date) return null\n const month = date.month.toString().padStart(2, '0')\n const day = date.day.toString().padStart(2, '0')\n return `--${month}-${day}` as Iso.MonthDay\n}\n"}]},{"name":"year-month-input","dependencies":["tw","form-field"],"files":[{"name":"components/year-month-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add year-month-input --directory app\n\nimport * as React from 'react'\nimport { DateField, DateInput as AriaDateInput, DateSegment, I18nProvider } from 'react-aria-components'\nimport { CalendarDate } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { yearMonthFns } from 'iso-fns'\n\n/** ISO year-month string in YYYY-MM format */\nexport type YearMonth = string\n\nexport type YearMonthInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<YearMonth | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (ISO year-month string YYYY-MM)\n value?: YearMonth | null\n defaultValue?: YearMonth | null\n onChange?: (value: YearMonth | null) => void\n\n // Locale\n locale?: string\n shape?: 'rectangle' | 'oval'\n}\n\nexport function YearMonthInput(props: YearMonthInputProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Internal state for CalendarDate\n const [value, _setValue] = React.useState(() => {\n try {\n const startingDate = props.value === undefined ? (props.defaultValue ?? null) : props.value\n return startingDate ? parseYearMonth(startingDate) : null\n } catch {\n return null\n }\n })\n\n function setValue(d: CalendarDate | null) {\n _setValue(d)\n const yearMonth = formatYearMonth(d)\n props.onChange?.(yearMonth)\n commitValidation()\n }\n\n // Sync controlled value\n if (props.value !== undefined) {\n const currentYearMonth = formatYearMonth(value)\n if (props.value !== currentYearMonth) {\n try {\n _setValue(props.value ? parseYearMonth(props.value) : null)\n } catch {\n _setValue(null)\n }\n }\n }\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: formatYearMonth(value),\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by DateField\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n value={formatYearMonth(value) ?? ''}\n form={props.form}\n />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n {/* Year/Month input using DateField */}\n <DateField\n aria-labelledby={labelId}\n aria-describedby={helpTextId}\n value={value}\n onChange={setValue}\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n granularity=\"day\"\n hideTimeZone\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n >\n <AriaDateInput className=\"flex px-3 py-2 text-sm [&>[data-literal]:not(:has(~[data-literal]))]:hidden\">\n {(segment) => {\n // Hide day segment\n const isHidden = segment.type === 'day'\n\n return (\n <DateSegment\n segment={segment}\n data-literal={segment.type === 'literal' ? true : undefined}\n className={tw(\n 'px-0.5 tabular-nums outline-none rounded-sm',\n 'focus:bg-primary-500 focus:text-white',\n segment.isPlaceholder && 'text-neutral-400',\n isHidden && 'hidden'\n )}\n />\n )\n }}\n </AriaDateInput>\n </FormFieldComponents.FieldGroup>\n </DateField>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n\nfunction parseYearMonth(yearMonth: YearMonth): CalendarDate | null {\n const match = yearMonth.match(/^(\\d{4})-(\\d{2})$/)\n if (!match || match.length < 3) return null\n const year = parseInt(match[1]!, 10)\n const month = parseInt(match[2]!, 10)\n // Use day 1 as placeholder for the CalendarDate\n return new CalendarDate(year, month, 1)\n}\n\nfunction formatYearMonth(date: CalendarDate | null): YearMonth | null {\n if (!date) return null\n const year = date.year.toString().padStart(4, '0')\n const month = date.month.toString().padStart(2, '0')\n const yearMonth = `${year}-${month}`\n return isValidYearMonth(yearMonth) ? yearMonth : null\n}\n\n/** Check if a string is a valid YYYY-MM year-month string */\nfunction isValidYearMonth(yearMonth: string): boolean {\n return yearMonthFns.isValid(yearMonth)\n}\n"}]},{"name":"select","dependencies":["tw","animated-popover","form-field","colors","icon"],"files":[{"name":"components/select.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add select --directory app\n\nimport * as React from 'react'\nimport { Select as AriaSelect, Button, ListBoxItem } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\nimport { getColor, stringToNumber } from '../utils/colors'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type SelectOption = {\n value: string | number | null\n label: string\n secondary?: string\n isDisabled?: boolean\n}\n\nexport type SelectProps<T extends SelectOption = SelectOption> = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<T['value'] | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: T['value'] | null\n defaultValue?: T['value'] | null\n onChange?: (value: T['value'] | null) => void\n\n // Options\n options: T[]\n renderOption?: (props: { option: T; colored: boolean; index: number }) => React.ReactNode\n\n colored?: boolean\n shape?: 'rectangle' | 'oval'\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nexport function Select<T extends SelectOption = SelectOption>(props: SelectProps<T>) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const buttonRef = React.useRef<HTMLButtonElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? null, props.onChange)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n buttonRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Find the selected option for display\n const selectedOption = React.useMemo(() => {\n return props.options.find((option) => option.value === value) ?? null\n }, [props.options, value])\n\n const selectedKey = value != null ? toKey(value) : null\n\n return (\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaSelect\n aria-labelledby={props.label ? labelId : undefined}\n aria-label={!props.label ? 'Select' : undefined}\n selectedKey={selectedKey}\n onSelectionChange={(key) => {\n const newValue = fromKey(key, props.options)\n setValue(newValue)\n commitValidation()\n }}\n isDisabled={props.isDisabled || props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n >\n {/* Select button */}\n <Button\n ref={buttonRef}\n className={tw(\n 'relative overflow-hidden ring-1 shadow-sm transition-all duration-200 max-w-96',\n props.shape === 'oval' ? 'rounded-full' : 'rounded',\n 'focus:outline-2 focus:-outline-offset-1',\n (props.isDisabled || props.isReadonly) &&\n 'ring-neutral-200 bg-neutral-50 text-neutral-400 shadow-none cursor-not-allowed',\n isInvalid && 'ring-negative-300 bg-negative-50 focus:outline-negative-500',\n hasWarning && 'ring-notice-300 bg-notice-50 focus:outline-notice-500',\n !isInvalid &&\n !hasWarning &&\n !props.isDisabled &&\n !props.isReadonly &&\n 'ring-black/10 hover:ring-black/20 focus:outline-primary-500',\n 'w-full cursor-default py-2 pl-3 pr-10 text-left text-sm max-sm:text-base placeholder:text-neutral-400'\n )}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n >\n {props.colored && selectedOption ? (\n <div\n className=\"absolute left-0 top-0 w-[3px] h-full\"\n style={{ backgroundColor: getColor(stringToNumber(String(selectedOption.value))) }}\n />\n ) : null}\n {selectedOption ? (\n <span className=\"block truncate text-sm min-h-5\">{selectedOption.label}</span>\n ) : (\n <span className=\"block truncate text-sm min-h-5 text-neutral-400 italic\">\n {props.placeholder ?? 'Select an option'}\n </span>\n )}\n <span className=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2\">\n <Icon name=\"chevron-up-down\" className=\"h-4 w-4 text-neutral-400\" />\n </span>\n </Button>\n\n {/* Options dropdown */}\n <AnimatedPopover offset={4} className=\"w-(--trigger-width)\">\n <FormFieldComponents.DropdownListBox\n items={props.options.map((option, index) => ({ ...option, index, id: toKey(option.value) }))}\n renderEmptyState={() => <div className=\"px-3 py-2 text-neutral-500 text-sm\">No options available</div>}\n >\n {(item) => {\n const option = item as T & { index: number; id: string }\n if (props.renderOption) {\n return (\n <ListBoxItem\n id={option.id}\n textValue={option.label}\n isDisabled={option.isDisabled}\n className=\"outline-none\"\n >\n {props.renderOption({ option, colored: props.colored ?? false, index: option.index })}\n </ListBoxItem>\n )\n }\n\n const color = props.colored ? getColor(stringToNumber(String(option.value))) : null\n return (\n <FormFieldComponents.DropdownItem\n id={option.id}\n textValue={option.label}\n label={option.label}\n secondary={option.secondary}\n isDisabled={option.isDisabled}\n color={color}\n />\n )\n }}\n </FormFieldComponents.DropdownListBox>\n </AnimatedPopover>\n </AriaSelect>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────────────────\n\nconst NULL_KEY = '__select_null__'\n\nfunction toKey(value: string | number | null): string {\n if (value === null) return NULL_KEY\n return String(value)\n}\n\nfunction fromKey<T extends SelectOption>(key: React.Key | null, options: T[]): T['value'] | null {\n if (key === null || key === undefined) return null\n const keyStr = String(key)\n if (keyStr === NULL_KEY) return null\n const option = options.find((o) => toKey(o.value) === keyStr)\n return option?.value ?? null\n}\n"}]},{"name":"boolean-select","dependencies":["tw","animated-popover","form-field","colors","icon","select"],"files":[{"name":"components/boolean-select.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add boolean-select --directory app\n\nimport * as React from 'react'\nimport { Select, type SelectProps, type SelectOption } from './select'\n\nexport type BooleanSelectProps = Omit<SelectProps, 'options' | 'value' | 'defaultValue' | 'onChange'> & {\n // Value management\n value?: boolean | null\n defaultValue?: boolean | null\n onChange?: (value: boolean | null) => void\n\n // Customizable labels\n trueLabel?: string\n falseLabel?: string\n nullLabel?: string\n includeNull?: boolean\n}\n\nexport function BooleanSelect({\n trueLabel = 'Yes',\n falseLabel = 'No',\n nullLabel = 'Not Set',\n includeNull = false,\n value,\n defaultValue,\n onChange,\n ...restProps\n}: BooleanSelectProps) {\n const options: SelectOption[] = [\n ...(includeNull ? [{ value: 'null', label: nullLabel }] : []),\n { value: 'true', label: trueLabel },\n { value: 'false', label: falseLabel }\n ]\n\n // Convert boolean value to string for Select component\n const stringValue = value === null ? 'null' : value === undefined ? undefined : String(value)\n const stringDefaultValue = defaultValue === null ? 'null' : defaultValue === undefined ? undefined : String(defaultValue)\n\n // Convert string value back to boolean for onChange\n const handleChange = (val: string | number | null) => {\n if (!onChange) return\n if (val === 'null' || val === null) onChange(null)\n else if (val === 'true') onChange(true)\n else if (val === 'false') onChange(false)\n }\n\n return (\n <Select {...restProps} options={options} value={stringValue} defaultValue={stringDefaultValue} onChange={handleChange} />\n )\n}\n"}]},{"name":"autocomplete","dependencies":["tw","animated-popover","form-field","colors","icon"],"files":[{"name":"components/autocomplete.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add autocomplete --directory app\n\nimport * as React from 'react'\nimport { ComboBox as AriaComboBox, ListBoxItem } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport type { Key } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { getColor, stringToNumber } from '../utils/colors'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { matchSorter } from 'match-sorter'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type AutocompleteOption = {\n value: string | number\n label: string\n secondary?: string\n}\n\nexport type AutocompleteProps<T extends AutocompleteOption = AutocompleteOption> = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<T['value'] | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: T['value'] | null\n defaultValue?: T['value'] | null\n onChange?: (value: T['value'] | null) => void\n\n // Options\n options: T[]\n renderOption?: (props: { option: T; colored: boolean; index: number }) => React.ReactNode\n\n colored?: boolean\n\n // Filtering\n filterFunction?: ((items: readonly T[], value: string) => T[]) | (keyof T)[]\n\n // Loading\n isLoading?: boolean\n\n // Input\n inputAutocomplete?: React.ComponentProps<'input'>['autoComplete']\n id?: string\n shape?: 'rectangle' | 'oval'\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nexport function Autocomplete<T extends AutocompleteOption = AutocompleteOption>(props: AutocompleteProps<T>) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Controlled state for the selected value\n const [selectedValue, setSelectedValue] = HeadlessForm.useControlledState(\n props.value,\n props.defaultValue ?? null,\n props.onChange\n )\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: selectedValue,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Find the selected option\n const selectedOption = React.useMemo(() => {\n return props.options.find((option) => option.value === selectedValue) ?? null\n }, [props.options, selectedValue])\n\n // Input value state (what's displayed in the input)\n const [inputValue, setInputValue] = React.useState(() => {\n const initial = props.value ?? props.defaultValue ?? null\n if (initial != null) {\n const option = props.options.find((o) => o.value === initial)\n return option?.label ?? ''\n }\n return ''\n })\n\n // Sync inputValue when external props.value changes\n const lastPropsValue = React.useRef(props.value)\n React.useEffect(() => {\n if (props.value !== lastPropsValue.current) {\n lastPropsValue.current = props.value\n if (props.value != null) {\n const option = props.options.find((o) => o.value === props.value)\n setInputValue(option?.label ?? '')\n } else {\n setInputValue('')\n }\n }\n }, [props.value, props.options])\n\n // Filter options based on input value\n const filteredOptions = React.useMemo(() => {\n if (props.isLoading) return []\n\n // When input matches the selected option's label, show all options (not filtered)\n const isShowingSelectedLabel = selectedOption && inputValue === selectedOption.label\n const searchTerm = isShowingSelectedLabel ? '' : inputValue\n\n const filterFn = props.filterFunction\n let filtered: T[]\n\n if (typeof filterFn === 'function') {\n filtered = filterFn(props.options, searchTerm).slice(0, 50)\n } else if (Array.isArray(filterFn)) {\n filtered = matchSorter(props.options, searchTerm, {\n keys: filterFn as string[]\n }).slice(0, 50)\n } else {\n filtered = defaultFilterFunction(props.options, searchTerm).slice(0, 50)\n }\n\n // Ensure selected option is always included\n if (selectedOption && !filtered.some((o) => o.value === selectedOption.value)) {\n filtered = [selectedOption, ...filtered]\n }\n\n return filtered\n }, [props.options, inputValue, props.filterFunction, props.isLoading, selectedOption])\n\n // Items for React Aria (with id for key management)\n const items = React.useMemo(\n () => filteredOptions.map((option, index) => ({ ...option, index, id: String(option.value) })),\n [filteredOptions]\n )\n\n // Selected key for React Aria\n const selectedKey = selectedValue != null ? String(selectedValue) : null\n\n const handleSelectionChange = (key: Key | null) => {\n if (key === null) {\n setSelectedValue(null)\n setInputValue('')\n } else {\n const option = props.options.find((o) => String(o.value) === String(key))\n setSelectedValue(option?.value ?? null)\n setInputValue(option?.label ?? '')\n }\n commitValidation()\n }\n\n const handleInputChange = (value: string) => {\n setInputValue(value)\n // Clear selection when input doesn't match any option label\n const matchingOption = props.options.find((o) => o.label === value)\n if (!matchingOption) {\n setSelectedValue(null)\n }\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={selectedValue ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaComboBox\n aria-labelledby={props.label ? labelId : undefined}\n aria-label={!props.label ? 'Autocomplete' : undefined}\n selectedKey={selectedKey}\n onSelectionChange={handleSelectionChange}\n inputValue={inputValue}\n onInputChange={handleInputChange}\n items={items}\n isDisabled={props.isDisabled || props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n allowsEmptyCollection\n menuTrigger=\"focus\"\n onBlur={() => commitValidation()}\n >\n {/* Input and button wrapper */}\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex w-full items-center cursor-default\"\n >\n {props.colored && selectedOption ? (\n <div\n className=\"absolute left-0 top-0 w-[3px] h-full\"\n style={{ backgroundColor: getColor(stringToNumber(String(selectedOption.value))) }}\n />\n ) : null}\n <FormFieldComponents.FieldInput\n ref={inputRef}\n autoComplete={props.inputAutocomplete ?? 'off'}\n className=\"pr-1\"\n placeholder={props.placeholder ?? undefined}\n aria-describedby={helpTextId}\n id={props.id}\n />\n <FormFieldComponents.ChevronButton />\n </FormFieldComponents.FieldGroup>\n\n {/* Dropdown */}\n <AnimatedPopover offset={4} className=\"w-(--trigger-width)\">\n <FormFieldComponents.DropdownListBox\n renderEmptyState={() => (\n <div className=\"px-3 py-2 text-neutral-500 text-sm\">\n {props.isLoading ? 'Loading...' : inputValue ? 'No results found' : 'No options available'}\n </div>\n )}\n >\n {(item) => {\n const option = item as T & { index: number; id: string }\n if (props.renderOption) {\n return (\n <ListBoxItem id={option.id} textValue={option.label} className=\"outline-none\">\n {props.renderOption({ option, colored: props.colored ?? false, index: option.index })}\n </ListBoxItem>\n )\n }\n\n const color = props.colored ? getColor(stringToNumber(String(option.value))) : null\n return (\n <FormFieldComponents.DropdownItem\n id={option.id}\n textValue={option.label}\n label={option.label}\n secondary={option.secondary}\n color={color}\n />\n )\n }}\n </FormFieldComponents.DropdownListBox>\n </AnimatedPopover>\n </AriaComboBox>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────────\n\nfunction defaultFilterFunction<T extends AutocompleteOption>(options: readonly T[], inputValue: string): T[] {\n if (!inputValue) return [...options]\n return matchSorter(options, inputValue, {\n keys: ['label', 'secondary']\n })\n}\n"}]},{"name":"autosuggest","dependencies":["tw","animated-popover","form-field","icon"],"files":[{"name":"components/autosuggest.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add autosuggest --directory app\n\nimport * as React from 'react'\nimport { ComboBox as AriaComboBox, ListBoxItem } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport type { Key } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { matchSorter } from 'match-sorter'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type AutosuggestOption = {\n value: string\n label: string\n secondary?: string\n}\n\nexport type AutosuggestProps<T extends AutosuggestOption = AutosuggestOption> = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: string | null\n defaultValue?: string | null\n onChange?: (value: string | null) => void\n onCustomValue?: (value: string) => void\n\n // Options\n options: T[]\n renderOption?: (props: { option: T; isCustom: boolean; index: number }) => React.ReactNode\n\n // Filtering\n filterFunction?: ((items: readonly T[], value: string) => T[]) | (keyof T)[]\n\n // Loading\n isLoading?: boolean\n\n // Input\n inputAutocomplete?: React.ComponentProps<'input'>['autoComplete']\n id?: string\n\n // Shape\n shape?: 'rectangle' | 'oval'\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nconst CUSTOM_VALUE_KEY = '__autosuggest_custom__'\n\nexport function Autosuggest<T extends AutosuggestOption = AutosuggestOption>(props: AutosuggestProps<T>) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Controlled state for the selected value\n const [selectedValue, setSelectedValue] = HeadlessForm.useControlledState(\n props.value,\n props.defaultValue ?? null,\n props.onChange\n )\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: selectedValue,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Find if selected value matches an option\n const selectedOption = React.useMemo(() => {\n return props.options.find((option) => option.value === selectedValue) ?? null\n }, [props.options, selectedValue])\n\n // Input value state (what's displayed in the input)\n const [inputValue, setInputValue] = React.useState(() => {\n const initial = props.value ?? props.defaultValue ?? null\n if (initial != null) {\n const option = props.options.find((o) => o.value === initial)\n return option?.label ?? initial\n }\n return ''\n })\n\n // Sync inputValue when external props.value changes\n const lastPropsValue = React.useRef(props.value)\n React.useEffect(() => {\n if (props.value !== lastPropsValue.current) {\n lastPropsValue.current = props.value\n if (props.value != null) {\n const option = props.options.find((o) => o.value === props.value)\n setInputValue(option?.label ?? props.value)\n } else {\n setInputValue('')\n }\n }\n }, [props.value, props.options])\n\n // Filter options based on input value\n const filteredOptions = React.useMemo(() => {\n if (props.isLoading) return []\n\n // When input matches the selected option's label, show all options (not filtered)\n const isShowingSelectedLabel = selectedOption && inputValue === selectedOption.label\n const searchTerm = isShowingSelectedLabel ? '' : inputValue\n\n const filterFn = props.filterFunction\n let filtered: T[]\n\n if (typeof filterFn === 'function') {\n filtered = filterFn(props.options, searchTerm).slice(0, 50)\n } else if (Array.isArray(filterFn)) {\n filtered = matchSorter(props.options, searchTerm, {\n keys: filterFn as string[]\n }).slice(0, 50)\n } else {\n filtered = defaultFilterFunction(props.options, searchTerm).slice(0, 50)\n }\n\n return filtered\n }, [props.options, inputValue, props.filterFunction, props.isLoading, selectedOption])\n\n // Check if we should show a custom value option\n const showCustomOption = React.useMemo(() => {\n if (!inputValue || props.isLoading) return false\n return !filteredOptions.some((option) => option.label === inputValue || option.value === inputValue)\n }, [inputValue, filteredOptions, props.isLoading])\n\n // Items for React Aria (with id for key management), including custom option\n const items = React.useMemo(() => {\n const mapped = filteredOptions.map((option, index) => ({\n ...option,\n index,\n id: option.value,\n isCustom: false\n }))\n\n if (showCustomOption) {\n return [\n { value: inputValue, label: inputValue, index: -1, id: CUSTOM_VALUE_KEY, isCustom: true } as T & {\n index: number\n id: string\n isCustom: boolean\n },\n ...mapped\n ]\n }\n\n return mapped\n }, [filteredOptions, showCustomOption, inputValue])\n\n // Selected key for React Aria\n const selectedKey = selectedValue != null ? (selectedOption ? selectedOption.value : CUSTOM_VALUE_KEY) : null\n\n const handleSelectionChange = (key: Key | null) => {\n if (key === null) {\n // Don't clear - keep the custom value in input\n return\n }\n\n if (key === CUSTOM_VALUE_KEY) {\n // Custom value selected\n setSelectedValue(inputValue)\n props.onCustomValue?.(inputValue)\n } else {\n const option = props.options.find((o) => o.value === String(key))\n setSelectedValue(option?.value ?? null)\n setInputValue(option?.label ?? '')\n }\n commitValidation()\n }\n\n const handleInputChange = (value: string) => {\n setInputValue(value)\n // Update selected value to the input value (allows custom values)\n setSelectedValue(value || null)\n }\n\n const handleBlur = () => {\n // Keep the custom value if entered\n if (inputValue && !selectedOption) {\n setSelectedValue(inputValue)\n props.onCustomValue?.(inputValue)\n }\n commitValidation()\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={selectedValue ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaComboBox\n aria-labelledby={props.label ? labelId : undefined}\n aria-label={!props.label ? 'Autosuggest' : undefined}\n selectedKey={selectedKey}\n onSelectionChange={handleSelectionChange}\n inputValue={inputValue}\n onInputChange={handleInputChange}\n items={items}\n isDisabled={props.isDisabled || props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n allowsCustomValue\n allowsEmptyCollection\n menuTrigger=\"focus\"\n onBlur={handleBlur}\n >\n {/* Input and button wrapper */}\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex w-full items-center cursor-default\"\n >\n <FormFieldComponents.FieldInput\n ref={inputRef}\n autoComplete={props.inputAutocomplete ?? 'off'}\n className=\"pr-1\"\n placeholder={props.placeholder ?? undefined}\n aria-describedby={helpTextId}\n id={props.id}\n />\n <FormFieldComponents.ChevronButton />\n </FormFieldComponents.FieldGroup>\n\n {/* Dropdown */}\n <AnimatedPopover offset={4} className=\"w-(--trigger-width)\">\n <FormFieldComponents.DropdownListBox\n renderEmptyState={() => (\n <div className=\"px-3 py-2 text-neutral-500 text-sm\">\n {props.isLoading ? 'Loading...' : inputValue ? 'No matching suggestions' : 'No options available'}\n </div>\n )}\n >\n {(item) => {\n const option = item as T & { index: number; id: string; isCustom: boolean }\n if (props.renderOption) {\n return (\n <ListBoxItem id={option.id} textValue={option.label} className=\"outline-none\">\n {props.renderOption({ option, isCustom: option.isCustom, index: option.index })}\n </ListBoxItem>\n )\n }\n\n return (\n <FormFieldComponents.DropdownItem\n id={option.id}\n textValue={option.label}\n label={option.label}\n secondary={option.secondary}\n customValuePrefix={option.isCustom ? 'Use' : undefined}\n />\n )\n }}\n </FormFieldComponents.DropdownListBox>\n </AnimatedPopover>\n </AriaComboBox>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction defaultFilterFunction<T extends AutosuggestOption>(options: readonly T[], inputValue: string): T[] {\n if (!inputValue) return [...options]\n return matchSorter(options, inputValue, {\n keys: ['label', 'secondary']\n })\n}\n"}]},{"name":"multiselect","dependencies":["tw","animated-popover","form-field","colors","compose-refs","icon"],"files":[{"name":"components/multiselect.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add multiselect --directory app\n\nimport * as React from 'react'\nimport { ListBox, ListBoxItem } from 'react-aria-components'\nimport type { Selection } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { getColor, stringToNumber } from '../utils/colors'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { useElementSize } from '../utils/use-element-size'\nimport { composeRefs } from '../utils/compose-refs'\nimport { Icon } from './icon'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type MultiselectOption = {\n value: string | number\n label: string\n secondary?: string\n isDisabled?: boolean\n}\n\nexport type MultiselectProps<T extends MultiselectOption = MultiselectOption> = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<T['value'][]>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: T['value'][]\n defaultValue?: T['value'][]\n onChange?: (value: T['value'][]) => void\n\n // Options\n options: T[]\n showSelectAll?: boolean\n renderOption?: (props: { option: T; selected: boolean; colored: boolean; index: number }) => React.ReactNode\n\n colored?: boolean\n shape?: 'rectangle' | 'oval'\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nexport function Multiselect<T extends MultiselectOption = MultiselectOption>(props: MultiselectProps<T>) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const buttonRef = React.useRef<HTMLButtonElement>(null)\n const popoverContentRef = React.useRef<HTMLDivElement>(null)\n const [containerRef, setContainerRef] = React.useState<HTMLButtonElement | null>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n const [isOpen, setIsOpen] = React.useState(false)\n\n // Controlled state management\n const [selectedValues, setSelectedValues] = HeadlessForm.useControlledState(\n props.value,\n props.defaultValue ?? [],\n props.onChange\n )\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: selectedValues,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n buttonRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n // Close dropdown on outside click\n React.useEffect(() => {\n if (!isOpen) return\n const handlePointerDown = (e: PointerEvent) => {\n const target = e.target as Node\n if (popoverContentRef.current?.contains(target) || buttonRef.current?.contains(target)) return\n setIsOpen(false)\n commitValidation()\n }\n document.addEventListener('pointerdown', handlePointerDown)\n return () => document.removeEventListener('pointerdown', handlePointerDown)\n })\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Find selected options\n const selectedOptions = React.useMemo(() => {\n return props.options.filter((option) => selectedValues.includes(option.value))\n }, [props.options, selectedValues])\n\n const containerSize = useElementSize(containerRef)\n\n const handleRemoveOption = (option: T) => {\n const newValues = selectedValues.filter((v) => v !== option.value)\n setSelectedValues(newValues)\n commitValidation()\n }\n\n const handleSelectAll = () => {\n const availableOptions = props.options.filter((o) => !o.isDisabled)\n if (selectedValues.length === availableOptions.length) {\n setSelectedValues([])\n } else {\n setSelectedValues(availableOptions.map((o) => o.value))\n }\n commitValidation()\n }\n\n // Convert selectedValues to a Set<string> for React Aria\n const selectedKeys = React.useMemo(() => {\n return new Set(selectedValues.map((v) => String(v)))\n }, [selectedValues])\n\n const handleSelectionChange = (keys: Selection) => {\n if (keys === 'all') {\n const availableOptions = props.options.filter((o) => !o.isDisabled)\n setSelectedValues(availableOptions.map((o) => o.value))\n } else {\n const keySet = keys as Set<string>\n const newValues = props.options.filter((o) => keySet.has(String(o.value))).map((o) => o.value)\n setSelectedValues(newValues)\n }\n commitValidation()\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden inputs for form submission */}\n {selectedValues.map((value, i) => (\n <HeadlessForm.HiddenInput\n key={i}\n ref={i === 0 ? hiddenInputRef : undefined}\n name={props.name}\n value={value}\n form={props.form}\n />\n ))}\n {selectedValues.length === 0 && (\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value=\"[]\" form={props.form} />\n )}\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n {/* Trigger button */}\n <button\n ref={composeRefs(buttonRef, setContainerRef)}\n type=\"button\"\n role=\"combobox\"\n aria-expanded={isOpen}\n aria-haspopup=\"listbox\"\n aria-labelledby={props.label ? labelId : undefined}\n aria-label={!props.label ? 'Multiselect' : undefined}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n disabled={props.isDisabled}\n onClick={() => {\n if (!props.isDisabled) setIsOpen((prev) => !prev)\n }}\n onBlur={() => {\n if (!isOpen) {\n commitValidation()\n }\n }}\n className={tw(\n 'relative overflow-hidden border transition-all duration-200 max-w-96',\n props.shape === 'oval' ? 'rounded-full' : 'rounded',\n 'focus:outline-none focus:ring-1',\n props.isDisabled && 'border-neutral-100 bg-neutral-50 text-neutral-400 cursor-not-allowed',\n isInvalid && 'border-negative-200 bg-negative-50 focus:border-negative-500 focus:ring-negative-500/20',\n hasWarning && 'border-notice-200 bg-notice-50 focus:border-notice-500 focus:ring-notice-500/20',\n !isInvalid &&\n !hasWarning &&\n !props.isDisabled &&\n 'border-neutral-200 hover:border-neutral-300 focus:border-primary-500 focus:ring-primary-500/20',\n 'w-full cursor-default bg-white py-2 pl-3 pr-10 text-left'\n )}\n >\n <div className=\"block text-sm min-h-5\">\n {selectedOptions.length === 0 ? (\n <span className=\"text-neutral-300 italic\">{props.placeholder ?? 'Select options'}</span>\n ) : (\n <div className=\"flex flex-wrap gap-1.5\">\n {selectedOptions.map((option) => {\n const chipColor = props.colored ? getColor(stringToNumber(String(option.value))) : undefined\n return (\n <span\n key={option.value}\n className={tw(\n 'inline-flex items-center rounded-full font-normal transition-colors max-w-full px-2 py-0.5 text-xs',\n !chipColor && 'bg-primary-500/10 text-primary-500',\n props.isDisabled && 'opacity-50'\n )}\n style={\n chipColor\n ? {\n backgroundColor: `color-mix(in srgb, ${chipColor} 15%, white)`,\n color: `color-mix(in srgb, ${chipColor} 50%, black)`\n }\n : undefined\n }\n >\n <span className=\"truncate\">{option.label}</span>\n {!props.isDisabled && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n handleRemoveOption(option)\n }}\n className=\"inline-flex items-center justify-center rounded-full transition-colors translate-x-1 h-4 w-4 -m-0.5 p-0.5 hover:bg-black/10\"\n aria-label={`Remove ${option.label}`}\n tabIndex={-1}\n >\n <Icon name=\"x-mark\" className=\"h-2 w-2\" />\n </button>\n )}\n </span>\n )\n })}\n </div>\n )}\n </div>\n <span className=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2\">\n <Icon name=\"chevron-up-down\" className=\"h-4 w-4 text-neutral-400\" />\n </span>\n </button>\n\n {/* Options dropdown */}\n {isOpen && !props.isDisabled && (\n <div\n ref={popoverContentRef}\n className={tw(\n 'absolute z-50 mt-1 overflow-hidden rounded bg-white border border-neutral-200 shadow-lg text-sm w-full focus:outline-none'\n )}\n style={{ width: containerSize.width, minWidth: containerSize.width }}\n >\n {props.showSelectAll && (\n <button\n type=\"button\"\n onClick={handleSelectAll}\n className=\"w-full px-3 py-2 text-left hover:bg-neutral-50 border-b border-neutral-100 text-sm font-medium\"\n >\n {selectedValues.length === props.options.filter((o) => !o.isDisabled).length ? 'Deselect All' : 'Select All'}\n </button>\n )}\n <ListBox\n aria-label={props.label ? String(props.label) : 'Multiselect'}\n selectionMode=\"multiple\"\n selectedKeys={selectedKeys}\n onSelectionChange={handleSelectionChange}\n className=\"max-h-60 overflow-y-auto focus:outline-none\"\n items={props.options.map((option, index) => ({ ...option, index, id: String(option.value) }))}\n renderEmptyState={() => <div className=\"px-3 py-2 text-neutral-500 text-sm\">No options available</div>}\n >\n {(item) => {\n const option = item as T & { index: number; id: string }\n const color = props.colored ? getColor(stringToNumber(String(option.value))) : null\n\n if (props.renderOption) {\n return (\n <ListBoxItem\n id={option.id}\n textValue={option.label}\n isDisabled={option.isDisabled}\n className=\"outline-none\"\n >\n {({ isSelected }) => (\n <>\n {props.renderOption!({\n option,\n selected: isSelected,\n colored: props.colored ?? false,\n index: option.index\n })}\n </>\n )}\n </ListBoxItem>\n )\n }\n\n return (\n <FormFieldComponents.DropdownItem\n id={option.id}\n textValue={option.label}\n label={option.label}\n secondary={option.secondary}\n isDisabled={option.isDisabled}\n color={color}\n className=\"px-3\"\n />\n )\n }}\n </ListBox>\n </div>\n )}\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n"},{"name":"utils/use-element-size.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add multiselect --directory app\n\nimport { useMemo, useEffect, useReducer } from 'react'\n\nfunction computeSize(element: HTMLElement | null) {\n if (element === null) return { width: 0, height: 0 }\n const { width, height } = element.getBoundingClientRect()\n return { width, height }\n}\n\nexport function useElementSize(element: HTMLElement | null, unit?: false): { width: number; height: number }\nexport function useElementSize(element: HTMLElement | null, unit: true): { width: string; height: string }\nexport function useElementSize(element: HTMLElement | null, unit = false) {\n const [identity, forceRerender] = useReducer(() => ({}), {})\n\n const size = useMemo(() => computeSize(element), [element, identity])\n\n useEffect(() => {\n if (!element) return\n\n const observer = new ResizeObserver(forceRerender)\n observer.observe(element)\n\n return () => {\n observer.disconnect()\n }\n }, [element])\n\n if (unit) {\n return {\n width: `${size.width}px`,\n height: `${size.height}px`\n }\n }\n\n return size\n}\n"}]},{"name":"multicomplete","dependencies":["tw","animated-popover","form-field","colors","icon"],"files":[{"name":"components/multicomplete.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add multicomplete --directory app\n\nimport * as React from 'react'\nimport { ComboBox as AriaComboBox, Input, ListBox, ListBoxItem } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport type { Key } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { getColor, stringToNumber } from '../utils/colors'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { matchSorter } from 'match-sorter'\nimport { Icon } from './icon'\nimport { useElementSize } from '../utils/use-element-size'\nimport { composeRefs } from '../utils/compose-refs'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type MulticompleteOption = {\n value: string | number\n label: string\n secondary?: string\n}\n\nexport type MulticompleteProps<T extends MulticompleteOption = MulticompleteOption> = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<T['value'][]>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: T['value'][]\n defaultValue?: T['value'][]\n onChange?: (value: T['value'][]) => void\n\n // Options\n options: T[]\n renderOption?: (props: { option: T; colored: boolean; index: number }) => React.ReactNode\n\n colored?: boolean\n\n // Filtering\n filterFunction?: ((items: readonly T[], value: string) => T[]) | (keyof T)[]\n\n // Loading\n isLoading?: boolean\n\n // Input\n inputAutocomplete?: React.ComponentProps<'input'>['autoComplete']\n id?: string\n shape?: 'rectangle' | 'oval'\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nconst EMPTY_ARR: never[] = []\n\nexport function Multicomplete<T extends MulticompleteOption = MulticompleteOption>(props: MulticompleteProps<T>) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const fieldGroupRef = React.useRef<HTMLDivElement>(null)\n const [fieldGroupEl, setFieldGroupEl] = React.useState<HTMLDivElement | null>(null)\n const fieldGroupSize = useElementSize(fieldGroupEl)\n const helpTextId = React.useId()\n const labelId = React.useId()\n const [query, setQuery] = React.useState('')\n\n // Controlled state management (array of values)\n const [selectedValues, setSelectedValues] = HeadlessForm.useControlledState(\n props.value,\n props.defaultValue ?? EMPTY_ARR,\n props.onChange\n )\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: selectedValues,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const isInteractive = !props.isDisabled && !props.isReadonly\n\n // Find selected options\n const selectedOptions = React.useMemo(() => {\n return props.options.filter((option) => selectedValues.includes(option.value))\n }, [props.options, selectedValues])\n\n // Filter unselected options based on query\n const availableOptions = React.useMemo(() => {\n if (props.isLoading) return []\n\n const unselected = props.options.filter((option) => !selectedValues.includes(option.value))\n const filterFn = props.filterFunction\n\n if (typeof filterFn === 'function') {\n return filterFn(unselected, query).slice(0, 50)\n }\n\n if (Array.isArray(filterFn)) {\n return matchSorter(unselected, query, {\n keys: filterFn as string[]\n }).slice(0, 50)\n }\n\n return defaultFilterFunction(unselected, query).slice(0, 50)\n }, [props.options, selectedValues, query, props.filterFunction, props.isLoading])\n\n // Items for React Aria (with id for key management)\n const items = React.useMemo(\n () => availableOptions.map((option, index) => ({ ...option, index, id: String(option.value) })),\n [availableOptions]\n )\n\n const handleRemoveOption = (value: T['value']) => {\n const newValues = selectedValues.filter((v) => v !== value)\n setSelectedValues(newValues)\n commitValidation()\n }\n\n const handleSelectionChange = (key: Key | null) => {\n if (key === null) return\n const option = props.options.find((o) => String(o.value) === String(key))\n if (option && !selectedValues.includes(option.value)) {\n setSelectedValues([...selectedValues, option.value])\n commitValidation()\n }\n setQuery('')\n }\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === 'Backspace' && !query && selectedOptions.length > 0) {\n e.preventDefault()\n handleRemoveOption(selectedOptions[selectedOptions.length - 1]!.value)\n }\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden inputs for form submission */}\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n value={JSON.stringify(selectedValues)}\n form={props.form}\n />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaComboBox\n aria-labelledby={props.label ? labelId : undefined}\n aria-label={!props.label ? 'Multicomplete' : undefined}\n selectedKey={null}\n onSelectionChange={handleSelectionChange}\n inputValue={query}\n onInputChange={setQuery}\n items={items}\n isDisabled={props.isDisabled || props.isReadonly}\n isInvalid={isInvalid}\n allowsEmptyCollection\n menuTrigger=\"focus\"\n onBlur={() => commitValidation()}\n onKeyDown={handleKeyDown}\n >\n {/* Input container with chips */}\n <FormFieldComponents.FieldGroup\n ref={composeRefs(fieldGroupRef, setFieldGroupEl)}\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <div className=\"flex flex-wrap items-center gap-1.5 px-3 py-1.5 w-full\">\n {/* Display selected chips */}\n {selectedOptions.map((option) => {\n const chipColor = props.colored ? getColor(stringToNumber(String(option.value))) : undefined\n return (\n <span\n key={option.value}\n className={tw(\n 'inline-flex items-center rounded-full font-normal transition-colors max-w-full px-2 py-0.5 text-xs',\n !chipColor && 'bg-primary-500/10 text-primary-500',\n props.isDisabled && 'opacity-50'\n )}\n style={\n chipColor\n ? {\n backgroundColor: `color-mix(in srgb, ${chipColor} 15%, white)`,\n color: `color-mix(in srgb, ${chipColor} 50%, black)`\n }\n : undefined\n }\n >\n <span className=\"truncate\">{option.label}</span>\n {isInteractive && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n handleRemoveOption(option.value)\n }}\n className=\"inline-flex items-center justify-center rounded-full transition-colors translate-x-1 h-4 w-4 -m-0.5 p-0.5 hover:bg-black/10\"\n aria-label={`Remove ${option.label}`}\n tabIndex={-1}\n >\n <Icon name=\"x-mark\" className=\"h-2 w-2\" />\n </button>\n )}\n </span>\n )\n })}\n\n {/* Search input */}\n <Input\n ref={inputRef}\n autoComplete={props.inputAutocomplete ?? 'off'}\n className=\"flex-1 min-w-[120px] px-0 bg-transparent border-0 focus:ring-0 focus:outline-none text-sm text-neutral-900 placeholder:text-neutral-400 py-0.5 disabled:cursor-not-allowed\"\n placeholder={selectedOptions.length === 0 ? (props.placeholder ?? 'Search to add...') : ''}\n aria-describedby={helpTextId}\n id={props.id}\n />\n </div>\n <FormFieldComponents.ChevronButton className=\"py-0\" />\n </FormFieldComponents.FieldGroup>\n\n {/* Options dropdown */}\n <AnimatedPopover\n triggerRef={fieldGroupRef}\n offset={4}\n style={{ width: fieldGroupSize.width, minWidth: fieldGroupSize.width }}\n >\n <div\n className={tw(\n 'overflow-hidden rounded bg-white border border-neutral-200 shadow-lg text-sm w-full focus:outline-none'\n )}\n >\n <ListBox\n aria-label={props.label ? String(props.label) : 'Multicomplete'}\n className=\"max-h-60 overflow-y-auto focus:outline-none\"\n renderEmptyState={() => (\n <div className=\"px-3 py-2 text-neutral-500 text-sm\">\n {props.isLoading ? 'Loading...' : query ? 'No results found' : 'No options available'}\n </div>\n )}\n >\n {(item) => {\n const option = item as T & { index: number; id: string }\n const color = props.colored ? getColor(stringToNumber(String(option.value))) : null\n\n if (props.renderOption) {\n return (\n <ListBoxItem id={option.id} textValue={option.label} className=\"outline-none\">\n {props.renderOption({ option, colored: props.colored ?? false, index: option.index })}\n </ListBoxItem>\n )\n }\n\n return (\n <FormFieldComponents.DropdownItem\n id={option.id}\n textValue={option.label}\n label={option.label}\n secondary={option.secondary}\n color={color}\n showCheckIcon={false}\n className=\"px-3\"\n />\n )\n }}\n </ListBox>\n </div>\n </AnimatedPopover>\n </AriaComboBox>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────────\n\nfunction defaultFilterFunction<T extends MulticompleteOption>(options: readonly T[], inputValue: string): T[] {\n if (!inputValue) return [...options]\n return matchSorter(options, inputValue, {\n keys: ['label', 'secondary']\n })\n}\n"}]},{"name":"multisuggest","dependencies":["tw","animated-popover","form-field","icon"],"files":[{"name":"components/multisuggest.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add multisuggest --directory app\n\nimport * as React from 'react'\nimport { ComboBox as AriaComboBox, Input, ListBox, ListBoxItem } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport type { Key } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { matchSorter } from 'match-sorter'\nimport { Icon } from './icon'\nimport { useElementSize } from '../utils/use-element-size'\nimport { composeRefs } from '../utils/compose-refs'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type MultisuggestOption = {\n value: string\n label: string\n secondary?: string\n}\n\nexport type MultisuggestProps<T extends MultisuggestOption = MultisuggestOption> = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string[]>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: string[]\n defaultValue?: string[]\n onChange?: (value: string[]) => void\n onCustomValue?: (value: string) => void\n\n // Options\n options: T[]\n renderOption?: (props: { option: T; isCustom: boolean; index: number }) => React.ReactNode\n\n // Filtering\n filterFunction?: ((items: readonly T[], value: string) => T[]) | (keyof T)[]\n\n // Loading\n isLoading?: boolean\n\n // Input\n inputAutocomplete?: React.ComponentProps<'input'>['autoComplete']\n id?: string\n shape?: 'rectangle' | 'oval'\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nconst CUSTOM_VALUE_KEY = '__multisuggest_custom__'\nconst EMPTY_ARR: never[] = []\n\nexport function Multisuggest<T extends MultisuggestOption = MultisuggestOption>(props: MultisuggestProps<T>) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const fieldGroupRef = React.useRef<HTMLDivElement>(null)\n const [fieldGroupEl, setFieldGroupEl] = React.useState<HTMLDivElement | null>(null)\n const fieldGroupSize = useElementSize(fieldGroupEl)\n const helpTextId = React.useId()\n const labelId = React.useId()\n const [query, setQuery] = React.useState('')\n const queryRef = React.useRef(query)\n queryRef.current = query\n\n // Controlled state management (array of values)\n const [selectedValues, setSelectedValues] = HeadlessForm.useControlledState(\n props.value,\n props.defaultValue ?? EMPTY_ARR,\n props.onChange\n )\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: selectedValues,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const isInteractive = !props.isDisabled && !props.isReadonly\n\n // Find selected options (both from options list and custom values)\n const selectedOptions = React.useMemo(() => {\n return selectedValues.map((val) => {\n const existingOption = props.options.find((opt) => opt.value === val)\n if (existingOption) return existingOption\n return { value: val, label: val } as T\n })\n }, [props.options, selectedValues])\n\n // Filter unselected options based on query\n const availableOptions = React.useMemo(() => {\n if (props.isLoading) return []\n\n const unselected = props.options.filter((option) => !selectedValues.includes(option.value))\n const filterFn = props.filterFunction\n\n if (typeof filterFn === 'function') {\n return filterFn(unselected, query).slice(0, 50)\n }\n\n if (Array.isArray(filterFn)) {\n return matchSorter(unselected, query, {\n keys: filterFn as string[]\n }).slice(0, 50)\n }\n\n return defaultFilterFunction(unselected, query).slice(0, 50)\n }, [props.options, selectedValues, query, props.filterFunction, props.isLoading])\n\n // Check if we should show a custom value option\n const showCustomOption = React.useMemo(() => {\n if (!query || props.isLoading) return false\n const queryNotInOptions = !availableOptions.some((option) => option.label === query || option.value === query)\n const queryNotSelected = !selectedValues.includes(query)\n return queryNotInOptions && queryNotSelected\n }, [query, availableOptions, selectedValues, props.isLoading])\n\n // Items for React Aria (with id for key management), including custom option\n const items = React.useMemo(() => {\n const mapped = availableOptions.map((option, index) => ({\n ...option,\n index,\n id: option.value,\n isCustom: false\n }))\n\n if (showCustomOption) {\n return [\n { value: query, label: query, index: -1, id: CUSTOM_VALUE_KEY, isCustom: true } as T & {\n index: number\n id: string\n isCustom: boolean\n },\n ...mapped\n ]\n }\n\n return mapped\n }, [availableOptions, showCustomOption, query])\n\n const handleRemoveOption = (value: string) => {\n const newValues = selectedValues.filter((v) => v !== value)\n setSelectedValues(newValues)\n commitValidation()\n }\n\n const addValue = (val: string, isCustom: boolean) => {\n if (selectedValues.includes(val)) return\n setSelectedValues([...selectedValues, val])\n if (isCustom) props.onCustomValue?.(val)\n setQuery('')\n commitValidation()\n }\n\n const handleSelectionChange = (key: Key | null) => {\n if (key === null) return\n const keyStr = String(key)\n\n if (keyStr === CUSTOM_VALUE_KEY) {\n addValue(queryRef.current, true)\n } else {\n const option = props.options.find((o) => o.value === keyStr)\n if (option && !selectedValues.includes(option.value)) {\n addValue(option.value, false)\n }\n }\n }\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === 'Backspace' && !query && selectedOptions.length > 0) {\n e.preventDefault()\n handleRemoveOption(selectedOptions[selectedOptions.length - 1]!.value)\n }\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden inputs for form submission */}\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n value={JSON.stringify(selectedValues)}\n form={props.form}\n />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaComboBox\n aria-labelledby={props.label ? labelId : undefined}\n aria-label={!props.label ? 'Multisuggest' : undefined}\n selectedKey={null}\n onSelectionChange={handleSelectionChange}\n inputValue={query}\n onInputChange={setQuery}\n items={items}\n isDisabled={props.isDisabled || props.isReadonly}\n isInvalid={isInvalid}\n allowsEmptyCollection\n allowsCustomValue\n menuTrigger=\"focus\"\n onBlur={() => {\n if (queryRef.current && showCustomOption) {\n addValue(queryRef.current, true)\n }\n commitValidation()\n }}\n onKeyDown={handleKeyDown}\n >\n {/* Input container with chips */}\n <FormFieldComponents.FieldGroup\n ref={composeRefs(fieldGroupRef, setFieldGroupEl)}\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <div className=\"flex flex-wrap items-center gap-1.5 px-3 py-1.5 w-full\">\n {/* Display selected chips */}\n {selectedOptions.map((option) => (\n <span\n key={option.value}\n className={tw(\n 'inline-flex items-center rounded-full font-normal transition-colors max-w-full px-2 py-0.5 text-xs',\n 'bg-primary-500/10 text-primary-500',\n props.isDisabled && 'opacity-50'\n )}\n >\n <span className=\"truncate\">{option.label}</span>\n {isInteractive && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n handleRemoveOption(option.value)\n }}\n className=\"inline-flex items-center justify-center rounded-full transition-colors translate-x-1 h-4 w-4 -m-0.5 p-0.5 hover:bg-black/10\"\n aria-label={`Remove ${option.label}`}\n tabIndex={-1}\n >\n <Icon name=\"x-mark\" className=\"h-2 w-2\" />\n </button>\n )}\n </span>\n ))}\n\n {/* Search input */}\n <Input\n ref={inputRef}\n autoComplete={props.inputAutocomplete ?? 'off'}\n className=\"flex-1 min-w-[120px] px-0 bg-transparent border-0 focus:ring-0 focus:outline-none text-sm text-neutral-900 placeholder:text-neutral-400 py-0.5 disabled:cursor-not-allowed\"\n placeholder={selectedOptions.length === 0 ? (props.placeholder ?? 'Search or type to add...') : ''}\n aria-describedby={helpTextId}\n id={props.id}\n />\n </div>\n <FormFieldComponents.ChevronButton className=\"py-0\" />\n </FormFieldComponents.FieldGroup>\n\n {/* Options dropdown */}\n <AnimatedPopover\n triggerRef={fieldGroupRef}\n offset={4}\n style={{ width: fieldGroupSize.width, minWidth: fieldGroupSize.width }}\n >\n <div\n className={tw(\n 'overflow-hidden rounded bg-white border border-neutral-200 shadow-lg text-sm w-full focus:outline-none'\n )}\n >\n <ListBox\n aria-label={props.label ? String(props.label) : 'Multisuggest'}\n className=\"max-h-60 overflow-y-auto focus:outline-none\"\n renderEmptyState={() => (\n <div className=\"px-3 py-2 text-neutral-500 text-sm\">\n {props.isLoading ? 'Loading...' : query ? 'No matching suggestions' : 'No options available'}\n </div>\n )}\n >\n {(item) => {\n const option = item as T & { index: number; id: string; isCustom: boolean }\n\n if (props.renderOption) {\n return (\n <ListBoxItem id={option.id} textValue={option.label} className=\"outline-none\">\n {props.renderOption({ option, isCustom: option.isCustom, index: option.index })}\n </ListBoxItem>\n )\n }\n\n return (\n <FormFieldComponents.DropdownItem\n id={option.id}\n textValue={option.label}\n label={option.label}\n secondary={option.secondary}\n showCheckIcon={false}\n customValuePrefix={option.isCustom ? 'Add' : undefined}\n />\n )\n }}\n </ListBox>\n </div>\n </AnimatedPopover>\n </AriaComboBox>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────────\n\nfunction defaultFilterFunction<T extends MultisuggestOption>(options: readonly T[], inputValue: string): T[] {\n if (!inputValue) return [...options]\n return matchSorter(options, inputValue, {\n keys: ['label', 'secondary']\n })\n}\n"}]},{"name":"checkbox","dependencies":["tw","form-field","icon"],"files":[{"name":"components/checkbox.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add checkbox --directory app\n\nimport * as React from 'react'\nimport { Checkbox as AriaCheckbox } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\nexport type PlainCheckboxProps = {\n name?: string\n form?: string\n\n isDisabled?: boolean\n isRequired?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<boolean>\n\n checked?: boolean\n defaultChecked?: boolean\n onChange?: (checked: boolean) => void\n\n 'aria-label'?: string\n}\n\nexport type LabeledCheckboxProps = {\n name?: string\n form?: string\n\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n\n isRequired?: boolean\n isDisabled?: boolean\n isIndeterminate?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<boolean>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n checked?: boolean\n defaultChecked?: boolean\n onChange?: (checked: boolean) => void\n\n value?: string\n}\n\n// ── Plain Checkbox ─────────────────────────────────────────────────────────────\n\nexport function PlainCheckbox(props: PlainCheckboxProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n\n const [checked, setChecked] = HeadlessForm.useControlledState(props.checked, props.defaultChecked ?? false, props.onChange)\n\n const { isInvalid: validationInvalid, commitValidation } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: checked,\n name: props.name,\n isRequired: false,\n form: props.form,\n focus() {\n hiddenInputRef.current?.closest('label')?.focus()\n }\n },\n hiddenInputRef\n )\n\n return (\n <AriaCheckbox\n isSelected={checked}\n onChange={(v) => {\n setChecked(v)\n commitValidation()\n }}\n isDisabled={props.isDisabled}\n isInvalid={props.isInvalid || validationInvalid}\n aria-label={props['aria-label']}\n className=\"group inline-flex items-center focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600\"\n >\n {({ isSelected, isIndeterminate, isDisabled }) => (\n <>\n {props.isDisabled || !props.name ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n form={props.form}\n value={checked ? 'true' : 'false'}\n />\n )}\n <CheckboxBox isSelected={isSelected} isIndeterminate={isIndeterminate} isDisabled={isDisabled} />\n </>\n )}\n </AriaCheckbox>\n )\n}\n\nPlainCheckbox.displayName = 'PlainCheckbox'\n\n// ── Labeled Checkbox ───────────────────────────────────────────────────────────\n\nexport function LabeledCheckbox(props: LabeledCheckboxProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n\n const [checked, setChecked] = HeadlessForm.useControlledState(props.checked, props.defaultChecked ?? false, props.onChange)\n\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: checked,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n hiddenInputRef.current?.closest('label')?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <AriaCheckbox\n isSelected={checked}\n isIndeterminate={props.isIndeterminate}\n onChange={(v) => {\n setChecked(v)\n commitValidation()\n }}\n isDisabled={props.isDisabled}\n isInvalid={isInvalid}\n aria-describedby={helpTextId}\n className=\"group relative flex items-start gap-3 select-none focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600\"\n >\n {({ isSelected, isIndeterminate, isDisabled }) => (\n <>\n {props.isDisabled || !props.name ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n form={props.form}\n value={checked ? 'true' : 'false'}\n />\n )}\n <div className=\"shrink-0\">\n <CheckboxBox isSelected={isSelected} isIndeterminate={isIndeterminate} isDisabled={isDisabled} />\n </div>\n <div className=\"flex flex-col justify-center\">\n <div className=\"flex items-center gap-2 justify-between\">\n <FormFieldComponents.Label as=\"span\">{props.label}</FormFieldComponents.Label>\n {props.cornerHint && <div className=\"text-sm text-neutral-500\">{props.cornerHint}</div>}\n </div>\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n UNSAFE_className=\"mt-0.5\"\n />\n </div>\n </>\n )}\n </AriaCheckbox>\n )\n}\n\nLabeledCheckbox.displayName = 'LabeledCheckbox'\n\n// ── Helper components ──────────────────────────────────────────────────────────\n\nfunction CheckboxBox({\n isSelected,\n isIndeterminate,\n isDisabled\n}: {\n isSelected: boolean\n isIndeterminate: boolean\n isDisabled: boolean\n}) {\n return (\n <div\n className={tw(\n 'relative size-5 rounded border transition-colors duration-200',\n isSelected || isIndeterminate ? 'bg-primary-600 border-primary-600' : 'bg-white border-neutral-300',\n !isDisabled && !(isSelected || isIndeterminate) && 'group-hovered:border-neutral-400',\n isDisabled && 'cursor-not-allowed border-neutral-300 bg-neutral-100'\n )}\n >\n {isIndeterminate ? (\n <Icon name=\"minus\" className=\"h-5/6 absolute inset-0 m-auto aspect-square stroke-white text-white\" />\n ) : (\n <Icon name=\"check\" className=\"h-5/6 absolute inset-0 m-auto aspect-square stroke-white text-white\" />\n )}\n </div>\n )\n}\n"}]},{"name":"checkbox-group","dependencies":["tw","form-field","icon","checkbox"],"files":[{"name":"components/checkbox-group.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add checkbox-group --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { LabeledCheckbox } from './checkbox'\n\nexport type CheckboxGroupOption = {\n value: string\n label: React.ReactNode\n description?: React.ReactNode\n isDisabled?: boolean\n}\n\nexport type CheckboxGroupProps = {\n name?: string\n form?: string\n\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string[]>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n value?: string[]\n defaultValue?: string[]\n onChange?: (value: string[]) => void\n\n options: CheckboxGroupOption[]\n orientation?: 'horizontal' | 'vertical'\n showSelectAll?: boolean\n minSelection?: number\n maxSelection?: number\n}\n\nexport function CheckboxGroup(props: CheckboxGroupProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? [], props.onChange)\n\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus is handled by individual checkboxes\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const orientation = props.orientation ?? 'vertical'\n\n const enabledOptions = props.options.filter((o) => !o.isDisabled)\n const allSelected = value.length === enabledOptions.length\n const someSelected = value.length > 0 && !allSelected\n\n const handleOptionToggle = (optionValue: string) => {\n const newValue = value.includes(optionValue) ? value.filter((v) => v !== optionValue) : [...value, optionValue]\n\n if (props.minSelection && newValue.length < props.minSelection) {\n return\n }\n if (props.maxSelection && newValue.length > props.maxSelection) {\n return\n }\n\n setValue(newValue)\n commitValidation()\n }\n\n const handleSelectAll = () => {\n if (allSelected) {\n setValue([])\n } else {\n setValue(enabledOptions.map((o) => o.value))\n }\n commitValidation()\n }\n\n return (\n <div\n className=\"relative\"\n role=\"group\"\n aria-labelledby={props.label ? `${helpTextId}-label` : undefined}\n aria-describedby={helpTextId}\n >\n {value.map((v, i) => (\n <HeadlessForm.HiddenInput\n key={i}\n ref={i === 0 ? hiddenInputRef : undefined}\n name={props.name}\n value={v}\n form={props.form}\n />\n ))}\n {value.length === 0 && <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value=\"\" form={props.form} />}\n\n {props.label && (\n <div className=\"flex justify-between items-center mb-2\">\n <FormFieldComponents.Label as=\"span\" id={`${helpTextId}-label`}>\n {props.label}\n </FormFieldComponents.Label>\n {props.cornerHint && <div className=\"text-sm text-neutral-500\">{props.cornerHint}</div>}\n </div>\n )}\n\n {props.showSelectAll && (\n <div className=\"mb-2 pb-2 border-b border-neutral-200\">\n <LabeledCheckbox\n checked={allSelected}\n isIndeterminate={someSelected}\n onChange={handleSelectAll}\n isDisabled={props.isDisabled}\n label={<span className=\"text-sm font-medium\">Select All</span>}\n />\n </div>\n )}\n\n <div className={tw('flex', orientation === 'vertical' ? 'flex-col space-y-1.5' : 'flex-row flex-wrap gap-4')}>\n {props.options.map((option) => (\n <LabeledCheckbox\n key={option.value}\n label={option.label}\n helpText={option.description ?? null}\n checked={value.includes(option.value)}\n onChange={() => handleOptionToggle(option.value)}\n isDisabled={option.isDisabled || props.isDisabled}\n />\n ))}\n </div>\n\n {(props.minSelection || props.maxSelection) && (\n <div className=\"mt-2 text-xs text-neutral-500\">\n {props.minSelection && props.maxSelection\n ? `Select between ${props.minSelection} and ${props.maxSelection} options`\n : props.minSelection\n ? `Select at least ${props.minSelection} option${props.minSelection > 1 ? 's' : ''}`\n : `Select up to ${props.maxSelection} option${props.maxSelection! > 1 ? 's' : ''}`}\n {' · '}\n {value.length} selected\n </div>\n )}\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nCheckboxGroup.displayName = 'CheckboxGroup'\n"}]},{"name":"radio","dependencies":["tw"],"files":[{"name":"components/radio.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add radio --directory app\n\nimport * as React from 'react'\nimport { Radio as AriaRadio } from 'react-aria-components'\nimport { tw } from '../utils/tw'\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\nexport type PlainRadioProps = {\n value: string\n isDisabled?: boolean\n 'aria-label'?: string\n}\n\nexport type LabeledRadioProps = {\n value: string\n label?: React.ReactNode\n description?: React.ReactNode\n isDisabled?: boolean\n}\n\n// ── Plain Radio ──────────────────────────────────────────────────────────────\n\nexport function PlainRadio(props: PlainRadioProps) {\n return (\n <AriaRadio\n value={props.value}\n isDisabled={props.isDisabled}\n aria-label={props['aria-label']}\n className=\"group inline-flex items-center cursor-pointer data-[disabled]:cursor-not-allowed focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600\"\n >\n {({ isSelected, isDisabled }) => <RadioCircle isSelected={isSelected} isDisabled={isDisabled} />}\n </AriaRadio>\n )\n}\n\nPlainRadio.displayName = 'PlainRadio'\n\n// ── Labeled Radio ────────────────────────────────────────────────────────────\n\nexport function LabeledRadio(props: LabeledRadioProps) {\n return (\n <AriaRadio\n value={props.value}\n isDisabled={props.isDisabled}\n className=\"group flex items-start gap-3 cursor-pointer data-[disabled]:cursor-not-allowed select-none focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600\"\n >\n {({ isSelected, isDisabled }) => (\n <>\n <div className=\"shrink-0\">\n <RadioCircle isSelected={isSelected} isDisabled={isDisabled} />\n </div>\n {props.label && (\n <div className=\"flex flex-col\">\n <span className={tw('text-sm font-medium', isDisabled ? 'text-neutral-400' : 'text-neutral-700')}>\n {props.label}\n </span>\n {props.description && (\n <span className={tw('text-xs mt-0.5', isDisabled ? 'text-neutral-400' : 'text-neutral-500')}>\n {props.description}\n </span>\n )}\n </div>\n )}\n </>\n )}\n </AriaRadio>\n )\n}\n\nLabeledRadio.displayName = 'LabeledRadio'\n\n// ── Helper components ─────────────────────────────────────────────────────────\n\nfunction RadioCircle({ isSelected, isDisabled }: { isSelected: boolean; isDisabled: boolean }) {\n return (\n <div\n className={tw(\n 'size-5 rounded-full border-2 transition-colors duration-200 flex items-center justify-center',\n isSelected ? 'border-primary-600 bg-primary-600' : 'border-neutral-300 bg-white',\n !isDisabled && !isSelected && 'group-hovered:border-neutral-400',\n isDisabled && 'cursor-not-allowed border-neutral-300 bg-neutral-100'\n )}\n >\n <div\n className={tw(\n 'size-2 rounded-full bg-white transition-transform duration-200',\n isSelected ? 'scale-100' : 'scale-0'\n )}\n />\n </div>\n )\n}\n"}]},{"name":"radio-group","dependencies":["tw","form-field"],"files":[{"name":"components/radio-group.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add radio-group --directory app\n\nimport * as React from 'react'\nimport { RadioGroup as AriaRadioGroup } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\n\nexport type RadioGroupProps = {\n name?: string\n form?: string\n\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n value?: string | null\n defaultValue?: string | null\n onChange?: (value: string | null) => void\n\n children?: React.ReactNode\n orientation?: 'horizontal' | 'vertical' | 'grid'\n className?: string\n}\n\nexport function RadioGroup(props: RadioGroupProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? null, props.onChange)\n\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus is handled by AriaRadioGroup\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const orientation = props.orientation ?? 'vertical'\n\n return (\n <div className=\"relative\">\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value ?? ''} form={props.form} />\n\n {props.label && (\n <div className=\"flex justify-between items-center mb-2\">\n <FormFieldComponents.Label as=\"span\" id={labelId}>\n {props.label}\n </FormFieldComponents.Label>\n {props.cornerHint && <div className=\"text-sm text-neutral-500\">{props.cornerHint}</div>}\n </div>\n )}\n\n <AriaRadioGroup\n aria-labelledby={props.label ? labelId : undefined}\n aria-describedby={helpTextId}\n value={value}\n onChange={(newValue) => {\n setValue(newValue)\n commitValidation()\n }}\n isDisabled={props.isDisabled}\n orientation={orientation === 'grid' ? undefined : orientation}\n >\n <div\n className={tw(\n 'flex',\n orientation === 'vertical'\n ? 'flex-col space-y-2'\n : orientation === 'horizontal'\n ? 'flex-row flex-wrap gap-4'\n : 'grid gap-4',\n props.className\n )}\n >\n {props.children}\n </div>\n </AriaRadioGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nRadioGroup.displayName = 'RadioGroup'\n"}]},{"name":"switch","dependencies":["tw","form-field"],"files":[{"name":"components/switch.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add switch --directory app\n\nimport * as React from 'react'\nimport { Switch as AriaSwitch } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\n\n// ── Props ──────────────────────────────────────────────────────────────────────\n\nexport type PlainSwitchProps = {\n name?: string\n form?: string\n\n isDisabled?: boolean\n\n value?: boolean\n defaultValue?: boolean\n onChange?: (value: boolean) => void\n\n 'aria-label'?: string\n}\n\nexport type LabeledSwitchProps = {\n name?: string\n form?: string\n\n label?: React.ReactNode\n description?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<boolean>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n value?: boolean\n defaultValue?: boolean\n onChange?: (value: boolean) => void\n\n labelPosition?: 'left' | 'right'\n}\n\n// ── Plain Switch ───────────────────────────────────────────────────────────────\n\nexport function PlainSwitch(props: PlainSwitchProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? false, props.onChange)\n\n return (\n <AriaSwitch\n isSelected={value}\n onChange={setValue}\n isDisabled={props.isDisabled}\n aria-label={props['aria-label']}\n className=\"group inline-flex items-center cursor-pointer data-[disabled]:cursor-not-allowed focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600\"\n >\n {({ isSelected, isDisabled }) => (\n <>\n {props.isDisabled || !props.name ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n form={props.form}\n value={value ? 'true' : 'false'}\n />\n )}\n <SwitchTrack isSelected={isSelected} isDisabled={isDisabled} />\n </>\n )}\n </AriaSwitch>\n )\n}\n\nPlainSwitch.displayName = 'PlainSwitch'\n\n// ── Labeled Switch ─────────────────────────────────────────────────────────────\n\nexport function LabeledSwitch(props: LabeledSwitchProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? false, props.onChange)\n\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n hiddenInputRef.current?.closest('label')?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const labelPosition = props.labelPosition ?? 'right'\n\n const labelElement = props.label && (\n <div className=\"flex flex-col text-left\">\n <FormFieldComponents.Label as=\"span\">{props.label}</FormFieldComponents.Label>\n {props.description && (\n <span className={tw('text-xs mt-0.5', props.isDisabled ? 'text-neutral-400' : 'text-neutral-500')}>\n {props.description}\n </span>\n )}\n </div>\n )\n\n return (\n <div className=\"relative\">\n <AriaSwitch\n isSelected={value}\n onChange={(checked) => {\n setValue(checked)\n commitValidation()\n }}\n isDisabled={props.isDisabled}\n aria-describedby={helpTextId}\n className=\"group flex items-center gap-3 cursor-pointer data-[disabled]:cursor-not-allowed select-none focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600\"\n >\n {({ isSelected, isDisabled }) => (\n <>\n {props.isDisabled || !props.name ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n form={props.form}\n value={value ? 'true' : 'false'}\n />\n )}\n {labelPosition === 'left' && labelElement}\n <div className=\"shrink-0 flex items-center\">\n <SwitchTrack isSelected={isSelected} isDisabled={isDisabled} />\n </div>\n {labelPosition === 'right' && labelElement}\n {props.cornerHint && props.label && <div className=\"ml-auto text-sm text-neutral-500\">{props.cornerHint}</div>}\n </>\n )}\n </AriaSwitch>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nLabeledSwitch.displayName = 'LabeledSwitch'\n\n// ── Helpers ────────────────────────────────────────────────────────────────────\n\nfunction SwitchTrack({ isSelected, isDisabled }: { isSelected: boolean; isDisabled: boolean }) {\n return (\n <div\n className={tw(\n 'relative inline-flex h-6 w-11 shrink-0 rounded-full border-2 border-transparent transition-colors duration-200',\n isSelected ? 'bg-primary-600' : 'bg-neutral-200',\n isDisabled && 'cursor-not-allowed bg-neutral-300'\n )}\n >\n <span\n className={tw(\n 'pointer-events-none inline-block size-5 rounded-full bg-white shadow-sm ring-1 ring-black/5 transition-transform duration-200',\n isSelected ? 'translate-x-5' : 'translate-x-0'\n )}\n />\n </div>\n )\n}\n"}]},{"name":"file-input","dependencies":["tw","form-field","compose-refs","file-input-utils","icon","headless-file-input"],"files":[{"name":"components/file-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add file-input --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { formatFileSize, validateFileMaxSize, validateFileType } from '../utils/file-input'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessFileInput } from './headless-file-input'\nimport { Icon } from './icon'\n\nexport type FileInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n renderPreview?: (props: FileInputPreviewProps) => React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string | File | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (File object or string URL, held in memory until form submit)\n value?: string | File | null\n defaultValue?: string | File | null\n onChange?: (value: string | File | null, hasError: boolean) => void\n\n // File specific\n accept?: string\n maxSize?: number // in bytes\n}\n\nexport interface FileInputPreviewProps {\n file: File | null\n fileUrl: string | null\n isDisabled: boolean\n onChange(value: string | null | File): unknown\n inputId: string\n name: string | undefined\n accept: string | undefined\n inputRef: React.Ref<HTMLInputElement>\n formatFileSize: (bytes: number) => string\n setDragging: (dragging: boolean) => void\n maxSize?: number\n}\n\nexport function FileInput(props: FileInputProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n const [, setDragging] = React.useState(false)\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? null, (value) => {\n if (props.onChange) {\n props.onChange(value, Boolean(validateFileMaxSize(value, props.maxSize) || validateFileType(value, props.accept)))\n }\n })\n\n const exceededMaxSize = React.useMemo(() => validateFileMaxSize(value, props.maxSize), [value, props.maxSize])\n const formatNotSupported = React.useMemo(() => validateFileType(value, props.accept), [value, props.accept])\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid || Boolean(exceededMaxSize || formatNotSupported)\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const errorMessage = exceededMaxSize || formatNotSupported || validationMessage\n\n const FilePreview = props.renderPreview ?? DefaultFileInputFilePreview\n\n return (\n <HeadlessFileInput\n value={value}\n onChange={(v) => {\n setValue(v)\n commitValidation()\n }}\n >\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <HeadlessFileInput.Preview>\n {(files) => {\n const file = files.length !== 0 ? files[0]!.file : null\n const fileUrl = files.length !== 0 ? files[0]!.fileUrl : null\n\n return (\n <div\n className={tw(\n 'relative',\n hasWarning &&\n '[&_.file-preview-border]:border-notice-300 [&_.file-preview-border]:hover:border-notice-400 [&_.file-preview-border]:text-notice-900 [&_.file-preview-border]:placeholder-notice-300 [&_.file-preview-border]:focus-within:border-notice-500 [&_.file-preview-border]:focus-within:ring-notice-500',\n isInvalid &&\n '[&_.file-preview-border]:border-negative-300 [&_.file-preview-border]:hover:border-negative-400 [&_.file-preview-border]:text-negative-900 [&_.file-preview-border]:placeholder-negative-300 [&_.file-preview-border]:focus-within:border-negative-500 [&_.file-preview-border]:focus-within:ring-negative-500'\n )}\n >\n {FilePreview({\n file,\n fileUrl,\n isDisabled: !!props.isDisabled,\n onChange: (v) => {\n setValue(v)\n commitValidation()\n },\n inputId,\n inputRef,\n name: props.name,\n accept: props.accept,\n formatFileSize,\n setDragging,\n maxSize: props.maxSize\n })}\n </div>\n )\n }}\n </HeadlessFileInput.Preview>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={errorMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </HeadlessFileInput>\n )\n}\n\nfunction DragOverContent() {\n return (\n <div\n className=\"absolute inset-0 p-12 rounded flex items-center justify-center\"\n style={{\n background: `repeating-linear-gradient(45deg, #e5e7eb, #e5e7eb 20px, #d1d5db 20px, #d1d5db 50px)`\n }}\n >\n <div>\n <Icon name=\"cloud-arrow-up\" className=\"h-12 w-12 mx-auto text-gray-400\" />\n <span className=\"mt-2 block text-sm font-semibold text-gray-900\">Upload a File</span>\n </div>\n </div>\n )\n}\n\nexport function DefaultFileInputFilePreview({\n file,\n isDisabled,\n onChange,\n inputId,\n name,\n accept,\n formatFileSize,\n setDragging,\n maxSize\n}: FileInputPreviewProps) {\n return (\n <>\n <HeadlessFileInput.DropZone\n accept={accept}\n name={name}\n inputId={inputId}\n onDragChange={setDragging}\n disabled={isDisabled}\n >\n {({ dragOver }) => (\n <div\n className={tw(\n 'file-preview-border relative block w-full rounded-md text-center focus:outline-none focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',\n 'border-2 border-neutral-200 hover:border-neutral-300',\n file ? '' : 'border-dashed',\n isDisabled && 'border-neutral-100 cursor-not-allowed'\n )}\n >\n {file ? (\n <div className=\"relative text-left\">\n <div className=\"flex items-center justify-between p-3 rounded\">\n <div className=\"flex items-center gap-3\">\n <Icon name=\"document\" className=\"h-8 w-8 text-neutral-400\" />\n <div>\n <p className=\"text-sm font-medium text-neutral-900\">{file.name || 'File'}</p>\n <p className=\"text-xs text-neutral-500\">{formatFileSize(file.size)}</p>\n </div>\n </div>\n </div>\n {dragOver && <DragOverContent />}\n </div>\n ) : dragOver ? (\n <DragOverContent />\n ) : (\n <div className=\"flex items-center justify-between p-3\">\n <div className=\"flex items-center gap-3\">\n <Icon name=\"cloud-arrow-up\" className=\"h-8 w-8 text-neutral-400\" />\n <div className=\"text-left\">\n <p className=\"text-sm text-neutral-600 font-medium\">Click to upload or drag and drop</p>\n <div className=\"text-xs text-neutral-500 mt-0.5\">\n {accept && <span>{accept}</span>}\n {accept && maxSize && <span> • </span>}\n {maxSize && <span>Max: {formatFileSize(maxSize)}</span>}\n </div>\n </div>\n </div>\n </div>\n )}\n </div>\n )}\n </HeadlessFileInput.DropZone>\n\n {file && !isDisabled && (\n <button\n type=\"button\"\n aria-label=\"Remove file\"\n onClick={() => onChange(null)}\n className=\"absolute top-1/2 -translate-y-1/2 right-3 p-1 hover:bg-neutral-50 rounded transition-colors\"\n >\n <Icon name=\"x-mark\" className=\"h-5 w-5 text-neutral-500\" />\n </button>\n )}\n </>\n )\n}\n"}]},{"name":"multi-file-input","dependencies":["tw","form-field","compose-refs","file-input-utils","icon","headless-file-input"],"files":[{"name":"components/multi-file-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add multi-file-input --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { formatFileSize, checkFileType, checkFileMaxSize } from '../utils/file-input'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessFileInput } from './headless-file-input'\nimport { Icon } from './icon'\n\nexport interface UrlFileReference {\n name: string\n url: string\n}\n\nexport type FileLike = File | string | UrlFileReference\n\nexport type MultiFileInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<FileLike[]>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (File objects array or strings, held in memory until form submit)\n value?: FileLike[]\n defaultValue?: FileLike[]\n onChange?: (value: FileLike[], hasError: boolean) => void\n\n // File specific\n accept?: string | null\n maxSize?: number // in bytes per file\n maxFiles?: number\n}\n\nexport function MultiFileInput(props: MultiFileInputProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n const exceededMaxSizeFn = React.useCallback(\n (value: FileLike[]) => {\n for (let i = 0; i < value.length; i++) {\n const v = value[i]\n if (v instanceof File && props.maxSize) {\n const error = checkFileMaxSize(v, props.maxSize)\n if (error) return `File ${i + 1} exceeded max size of ${formatFileSize(props.maxSize)}`\n }\n }\n return null\n },\n [props.maxSize]\n )\n\n const formatNotSupportedFn = React.useCallback(\n (value: FileLike[]) => {\n if (!props.accept) return null\n for (const v of value) {\n if (v instanceof File) {\n const error = checkFileType(v, props.accept)\n if (error) return error\n }\n }\n return null\n },\n [props.accept]\n )\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? [], (value) => {\n if (props.onChange) {\n props.onChange(value, Boolean(exceededMaxSizeFn(value) || formatNotSupportedFn(value)))\n }\n })\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate(files) {\n const exceededMaxSize = exceededMaxSizeFn(files)\n const formatNotSupported = formatNotSupportedFn(files)\n const exceededMaxFiles = props.maxFiles !== undefined && files.length > props.maxFiles ? 'Too many files' : null\n const custom = [props.validate ? props.validate(files) : []].flat()\n\n const errors = [exceededMaxSize, formatNotSupported, exceededMaxFiles, ...custom].filter(Boolean)\n return errors as string[]\n },\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const errorMessage = validationMessage\n\n const handleRemove = (index: number) => {\n const newFiles = value.filter((_, i) => i !== index)\n setValue(newFiles)\n }\n\n return (\n <HeadlessFileInput\n value={value.map((v) => (v instanceof File ? v : typeof v === 'string' ? v : v.url))}\n onChange={(v) => {\n setValue(\n v.map((a) =>\n a instanceof File\n ? a\n : (value.find((b) => (b instanceof File || typeof b === 'string' ? false : b.url === a)) ?? {\n name: a,\n url: a\n })\n )\n )\n commitValidation()\n }}\n multiple\n >\n <HeadlessForm.HiddenInput value=\"\" form={props.form} name={undefined} ref={inputRef} />\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <HeadlessFileInput.Preview>\n {(files) => (\n <>\n {files.length > 0 && (\n <div className=\"space-y-2 mb-3\">\n {files.map((fileItem, index) => {\n const fileName = fileItem.file\n ? fileItem.file.name\n : (value.filter((v) => !(v instanceof File) && typeof v !== 'string') as UrlFileReference[]).find(\n (v) => v.url === fileItem.fileUrl\n )?.name\n return (\n <div\n key={index}\n className=\"flex items-center text-left justify-between px-6 py-2 rounded border border-neutral-200 bg-white\"\n >\n <div className=\"flex items-center gap-2\">\n <Icon name=\"document\" className=\"h-5 w-5 text-neutral-400\" />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-neutral-900 truncate\">{fileName || 'File'}</p>\n {fileItem.file && (\n <p className=\"text-xs text-neutral-500\">{formatFileSize(fileItem.file.size)}</p>\n )}\n </div>\n </div>\n {!props.isDisabled && !props.isReadonly && (\n <button\n type=\"button\"\n onClick={() => handleRemove(index)}\n className=\"p-1 hover:bg-neutral-100 rounded transition-colors\"\n >\n <Icon name=\"x-mark\" className=\"h-4 w-4 text-neutral-500\" />\n </button>\n )}\n </div>\n )\n })}\n </div>\n )}\n </>\n )}\n </HeadlessFileInput.Preview>\n\n <HeadlessFileInput.DropZone\n accept={props.accept ?? undefined}\n name={props.name}\n inputId={inputId}\n disabled={props.isDisabled || props.isReadonly}\n >\n {({ dragOver }) => (\n <div\n className={tw(\n 'relative block w-full rounded-md text-center focus:outline-none focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',\n 'border-2 border-dashed border-neutral-200 hover:border-neutral-300',\n 'px-6 py-2 min-h-[60px] flex items-center justify-center',\n (props.isDisabled || props.isReadonly) && 'border-neutral-100 cursor-not-allowed',\n hasWarning &&\n 'border-notice-300 hover:border-notice-400 text-notice-900 placeholder-notice-300 focus-within:border-notice-500 focus-within:ring-notice-500',\n isInvalid &&\n 'border-negative-300 hover:border-negative-400 text-negative-900 placeholder-negative-300 focus-within:border-negative-500 focus-within:ring-negative-500'\n )}\n >\n {dragOver ? (\n <DragOverContent />\n ) : (\n <div className=\"flex items-center w-full gap-3\">\n <Icon name=\"cloud-arrow-up\" className=\"h-5 w-5 text-neutral-400 flex-shrink-0\" />\n <div className=\"text-left\">\n <p className=\"text-sm font-medium text-neutral-600\">Click to upload or drag and drop</p>\n <p className=\"text-xs text-neutral-500\">\n {value.length > 0 ? 'Add more files' : 'Select files'}\n {props.accept && ` (${props.accept})`}\n {props.maxSize && ` • Max: ${formatFileSize(props.maxSize)}`}\n </p>\n </div>\n </div>\n )}\n </div>\n )}\n </HeadlessFileInput.DropZone>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={errorMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </HeadlessFileInput>\n )\n}\n\nfunction DragOverContent() {\n return (\n <div className=\"flex items-center gap-3\">\n <Icon name=\"cloud-arrow-up\" className=\"h-5 w-5 text-gray-400 flex-shrink-0\" />\n <span className=\"text-sm font-semibold text-gray-900\">Drop files here</span>\n </div>\n )\n}\n"}]},{"name":"image-input","dependencies":["tw","form-field","compose-refs","file-input-utils","icon","headless-file-input"],"files":[{"name":"components/image-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add image-input --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { formatFileSize, checkFileType, checkFileMaxSize } from '../utils/file-input'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessFileInput } from './headless-file-input'\nimport { Icon } from './icon'\n\nexport type ImageInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n emptyState?: React.ReactNode\n renderPreview?: (props: ImageInputPreviewProps) => React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string | File | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (File object or string URL, held in memory until form submit)\n value?: string | File | null\n defaultValue?: string | File | null\n onChange?: (value: string | File | null) => void\n\n // Image specific\n accept?: string\n maxSize?: number // in bytes\n}\n\nexport interface ImageInputPreviewProps {\n fileUrl: string\n isDisabled: boolean\n onChange(value: string | null | File): unknown\n inputId: string\n name: string | undefined\n accept: string | undefined\n inputRef: React.Ref<HTMLInputElement>\n}\n\nexport function ImageInput(props: ImageInputProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n const exceededMaxSizeFn = React.useCallback(\n (value: string | File | null) => {\n if (value && props.maxSize && value instanceof File) {\n return checkFileMaxSize(value, props.maxSize)\n }\n return null\n },\n [props.maxSize]\n )\n\n const formatNotSupportedFn = React.useCallback(\n (value: string | File | null) => {\n if (value instanceof File) {\n return checkFileType(value, props.accept ?? 'image/*')\n }\n return null\n },\n [props.accept]\n )\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? null, (value) => {\n if (props.onChange) {\n props.onChange(value)\n }\n })\n\n const exceededMaxSize = React.useMemo(() => exceededMaxSizeFn(value), [value, exceededMaxSizeFn])\n const formatNotSupported = React.useMemo(() => formatNotSupportedFn(value), [value, formatNotSupportedFn])\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid || Boolean(exceededMaxSize || formatNotSupported)\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const errorMessage = exceededMaxSize || formatNotSupported || validationMessage\n\n const ImagePreview = props.renderPreview ?? DefaultImageInputPreview\n\n const accept = props.accept ?? 'image/*'\n\n const emptyState = props.emptyState ?? (\n <div className=\"px-6 py-12\">\n <Icon name=\"photo\" className=\"h-12 w-12 mx-auto mb-3 text-neutral-400\" />\n <p className=\"text-sm text-neutral-600 font-medium\">Click to upload or drag and drop</p>\n <p className=\"text-xs text-neutral-500 mt-1\">{accept}</p>\n {props.maxSize ? <p className=\"text-xs text-neutral-500 mt-1\">Max size: {formatFileSize(props.maxSize)}</p> : null}\n </div>\n )\n\n return (\n <HeadlessFileInput\n value={value}\n onChange={(v) => {\n setValue(v)\n commitValidation()\n }}\n >\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <HeadlessFileInput.Preview>\n {(files) => {\n const hasFile = files.length > 0\n return (\n <div\n className={tw(\n 'image-input-border relative block w-full rounded-md overflow-hidden text-center focus:outline-none focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',\n 'border-2 border-neutral-200 hover:border-neutral-300 min-h-56',\n !hasFile && 'border-dashed',\n props.isDisabled && 'border-neutral-100 cursor-not-allowed',\n hasWarning &&\n 'border-notice-300 hover:border-notice-400 text-notice-900 placeholder-notice-300 focus-within:border-notice-500 focus-within:ring-notice-500',\n isInvalid &&\n 'border-negative-300 hover:border-negative-400 text-negative-900 placeholder-negative-300 focus-within:border-negative-500 focus-within:ring-negative-500'\n )}\n >\n {hasFile ? (\n ImagePreview({\n isDisabled: !!props.isDisabled,\n fileUrl: files[0]!.fileUrl,\n onChange: (v) => {\n setValue(v)\n commitValidation()\n },\n inputId,\n inputRef,\n name: props.name,\n accept\n })\n ) : (\n <HeadlessFileInput.DropZone\n inputRef={inputRef}\n accept={accept}\n name={props.name}\n inputId={inputId}\n disabled={props.isDisabled}\n className=\"size-full\"\n >\n {({ dragOver }) => (\n <div className=\"relative pointer-events-none\">\n {emptyState}\n {dragOver ? (\n <div className=\"absolute inset-0\">\n <DragOverContent />\n </div>\n ) : null}\n </div>\n )}\n </HeadlessFileInput.DropZone>\n )}\n </div>\n )\n }}\n </HeadlessFileInput.Preview>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={errorMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </HeadlessFileInput>\n )\n}\n\nfunction DragOverContent() {\n return (\n <div\n className=\"absolute inset-0 p-12 rounded flex items-center justify-center\"\n style={{\n background: `repeating-linear-gradient(45deg, #e5e7eb, #e5e7eb 20px, #d1d5db 20px, #d1d5db 50px)`\n }}\n >\n <div>\n <Icon name=\"cloud-arrow-up\" className=\"h-12 w-12 mx-auto text-gray-400\" />\n <span className=\"mt-2 block text-sm font-semibold text-gray-900\">Upload an Image</span>\n </div>\n </div>\n )\n}\n\nexport function DefaultImageInputPreview({\n name,\n accept,\n fileUrl,\n inputRef,\n isDisabled,\n onChange,\n inputId\n}: ImageInputPreviewProps) {\n return (\n <>\n <HeadlessFileInput.DropZone\n inputRef={inputRef}\n accept={accept ?? 'image/*'}\n name={name}\n inputId={inputId}\n disabled={isDisabled}\n >\n {({ dragOver }) => (\n <div className=\"min-h-56 pointer-events-none\">\n {dragOver ? (\n <div className=\"absolute inset-0\">\n <DragOverContent />\n </div>\n ) : null}\n <div className=\"relative\">\n <div\n aria-hidden=\"true\"\n className=\"absolute inset-x-0 top-0 rounded-t-md h-20 bg-gradient-to-b from-white opacity-75 mix-blend-lighten\"\n />\n <img src={fileUrl} alt=\"Preview\" className=\"w-full object-cover rounded-md\" />\n </div>\n </div>\n )}\n </HeadlessFileInput.DropZone>\n <div className=\"absolute top-2 right-2\">\n {!isDisabled ? (\n <button\n type=\"button\"\n aria-label=\"Remove\"\n onClick={() => onChange(null)}\n className=\"p-1 h-5 w-5 flex items-center justify-center bg-gray-800/60 hover:bg-gray-500/60 rounded-full transition-colors\"\n >\n <Icon name=\"x-mark\" className=\"h-4 w-4 text-gray-200\" />\n </button>\n ) : null}\n </div>\n </>\n )\n}\n"}]},{"name":"multi-image-input","dependencies":["tw","form-field","compose-refs","file-input-utils","icon","headless-file-input"],"files":[{"name":"components/multi-image-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add multi-image-input --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { formatFileSize, checkFileType, checkFileMaxSize } from '../utils/file-input'\nimport { useStableAccessor } from '../utils/use-stable-accessor'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessFileInput } from './headless-file-input'\nimport { Icon } from './icon'\n\nexport type MultiImageInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n placeholder?: string\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<(string | File)[]>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (File objects array or strings, held in memory until form submit)\n value?: (string | File)[]\n defaultValue?: (string | File)[]\n onChange?: (value: (string | File)[]) => void\n\n // Image specific\n accept?: string\n maxSize?: number // in bytes per image\n maxImages?: number\n aspectRatio?: number // width/height ratio\n columns?: number // Grid columns for display\n allowReordering?: boolean\n\n renderPreview?: (props: MultiImageInputPreviewProps) => React.ReactNode\n}\n\nexport interface MultiImageInputPreviewProps {\n fileUrl: string\n isDisabled: boolean\n index: number\n onChange(value: string | null | File): unknown\n}\n\nexport function MultiImageInput(props: MultiImageInputProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n const labelRef = React.useRef<HTMLLabelElement>(null)\n\n const exceededMaxSizeFn = React.useCallback(\n (value: (string | File)[]) => {\n for (let i = 0; i < value.length; i++) {\n const v = value[i]\n if (v instanceof File && props.maxSize) {\n const error = checkFileMaxSize(v, props.maxSize)\n if (error) return `Image ${i + 1} exceeded max size of ${formatFileSize(props.maxSize)}`\n }\n }\n return null\n },\n [props.maxSize]\n )\n\n const formatNotSupportedFn = React.useCallback(\n (value: (string | File)[]) => {\n const accept = props.accept ?? 'image/*'\n for (const v of value) {\n if (v instanceof File) {\n const error = checkFileType(v, accept)\n if (error) return error\n }\n }\n return null\n },\n [props.accept]\n )\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? [], (value) => {\n if (props.onChange) {\n props.onChange(value)\n }\n })\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate(files) {\n const exceededMaxSize = exceededMaxSizeFn(files)\n const formatNotSupported = formatNotSupportedFn(files)\n const exceededMaxImages = props.maxImages !== undefined && files.length > props.maxImages ? 'Too many images' : null\n const custom = [props.validate ? props.validate(files) : []].flat()\n\n const errors = [exceededMaxSize, formatNotSupported, exceededMaxImages, ...custom].filter(Boolean)\n return errors as string[]\n },\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n labelRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const errorMessage = validationMessage\n\n // Stable handlers via useStableAccessor to avoid re-creating on every render\n const getValue = useStableAccessor(value)\n const getSetValue = useStableAccessor(setValue)\n\n const handleReorder = React.useCallback(\n (fromIndex: number, toIndex: number) => {\n const newFiles = [...getValue()]\n const [movedFile] = newFiles.splice(fromIndex, 1)\n newFiles.splice(toIndex, 0, movedFile!)\n getSetValue()(newFiles)\n },\n [getValue, getSetValue]\n )\n\n const handleRemove = React.useCallback(\n (index: number) => {\n getSetValue()(getValue().filter((_: string | File, i: number) => i !== index))\n },\n [getValue, getSetValue]\n )\n\n const handleReplace = React.useCallback(\n (index: number, file: string | File) => {\n getSetValue()(getValue().map((f: string | File, i: number) => (i === index ? file : f)))\n },\n [getValue, getSetValue]\n )\n\n const getCommitValidation = useStableAccessor(commitValidation)\n const handleFileInputChange = React.useCallback(\n (v: (string | File)[]) => {\n getSetValue()(v)\n getCommitValidation()()\n },\n [getSetValue, getCommitValidation]\n )\n\n // Stable component reference\n const imagePreview = React.useMemo(() => props.renderPreview ?? DefaultImagePreview, [props.renderPreview])\n\n // Stable keys for grid items\n const fileKeyMapRef = React.useRef(new WeakMap<File, string>())\n const fileKeyCounterRef = React.useRef(0)\n const getFileKey = React.useCallback((item: string | File): string => {\n if (typeof item === 'string') return item\n let key = fileKeyMapRef.current.get(item)\n if (!key) {\n key = `file-${fileKeyCounterRef.current++}`\n fileKeyMapRef.current.set(item, key)\n }\n return key\n }, [])\n\n // Memoize grid style\n const gridStyle = React.useMemo(\n () => ({ gridTemplateColumns: `repeat(${props.columns ?? 4}, minmax(0, 1fr))` }),\n [props.columns]\n )\n\n return (\n <HeadlessFileInput value={value} onChange={handleFileInputChange} multiple>\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <HeadlessFileInput.Preview>\n {(files) => {\n const canAddMore = !props.maxImages || files.length < props.maxImages\n\n return (\n <>\n {/* Drop zone */}\n {canAddMore && (\n <HeadlessFileInput.DropZone\n accept={props.accept ?? 'image/*'}\n name={props.name}\n inputId={inputId}\n inputRef={inputRef}\n ref={labelRef}\n disabled={props.isDisabled}\n >\n {({ dragOver }) => (\n <div\n className={tw(\n 'relative flex flex-col items-center justify-center rounded-md border-2 border-dashed cursor-pointer transition-all duration-200 mb-2 p-8',\n 'focus:outline-none focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2 pointer-events-none',\n 'border-neutral-200 hover:border-neutral-300',\n props.isDisabled && 'border-neutral-100 cursor-not-allowed',\n hasWarning &&\n 'border-notice-300 hover:border-notice-400 text-notice-900 placeholder-notice-300 focus-within:border-notice-500 focus-within:ring-notice-500',\n isInvalid &&\n 'border-negative-300 hover:border-negative-400 text-negative-900 placeholder-negative-300 focus-within:border-negative-500 focus-within:ring-negative-500'\n )}\n >\n {dragOver ? (\n <div className=\"min-h-20\">\n <DragOverContent />\n </div>\n ) : (\n <div className=\"text-center\">\n <Icon name=\"cloud-arrow-up\" className=\"h-10 w-10 mx-auto mb-3 text-neutral-400\" />\n <p className=\"text-sm text-neutral-600 font-medium\">\n {props.placeholder || 'Drop images here or click to browse'}\n </p>\n {props.maxSize && (\n <p className=\"text-xs text-neutral-500 mt-1\">\n Max size: {formatFileSize(props.maxSize)} per image\n </p>\n )}\n {props.maxImages && (\n <p className=\"text-xs text-neutral-500 mt-1\">\n {files.length} / {props.maxImages} images uploaded\n </p>\n )}\n </div>\n )}\n </div>\n )}\n </HeadlessFileInput.DropZone>\n )}\n\n {/* Image previews grid */}\n {files.length > 0 && (\n <div className={tw('grid gap-3')} style={gridStyle}>\n {files.map((fileItem, index) => (\n <GridItem\n key={getFileKey(value[index]!)}\n fileUrl={fileItem.fileUrl}\n index={index}\n aspectRatio={props.aspectRatio ?? 1}\n isDisabled={props.isDisabled ?? false}\n allowReordering={props.allowReordering ?? false}\n onReorder={handleReorder}\n onRemove={handleRemove}\n onReplace={handleReplace}\n ImagePreview={imagePreview}\n />\n ))}\n </div>\n )}\n\n {/* Message when at max capacity */}\n {!canAddMore && (\n <div className=\"mt-3 text-sm text-neutral-500 text-center\">\n Maximum of {props.maxImages} images reached\n </div>\n )}\n </>\n )\n }}\n </HeadlessFileInput.Preview>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={errorMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </HeadlessFileInput>\n )\n}\n\n// ── Memoized sub-components ─────────────────────────────────────────────────\n\nconst GridItem = React.memo(function GridItem({\n fileUrl,\n index,\n aspectRatio,\n isDisabled,\n allowReordering,\n onReorder,\n onRemove,\n onReplace,\n ImagePreview\n}: {\n fileUrl: string\n index: number\n aspectRatio: number\n isDisabled: boolean\n allowReordering: boolean\n onReorder: (fromIndex: number, toIndex: number) => void\n onRemove: (index: number) => void\n onReplace: (index: number, file: string | File) => void\n ImagePreview: (props: MultiImageInputPreviewProps) => React.ReactNode\n}) {\n const handleDragStart = React.useCallback(\n (e: React.DragEvent) => e.dataTransfer.setData('index', index.toString()),\n [index]\n )\n\n const handleDrop = React.useCallback(\n (e: React.DragEvent) => {\n e.preventDefault()\n const fromIndex = parseInt(e.dataTransfer.getData('index'))\n if (fromIndex !== index) onReorder(fromIndex, index)\n },\n [index, onReorder]\n )\n\n const handleChange = React.useCallback(\n (v: string | null | File) => {\n if (v === null) onRemove(index)\n else onReplace(index, v)\n },\n [index, onRemove, onReplace]\n )\n\n return (\n <div\n className=\"relative group overflow-hidden rounded-md\"\n style={{ aspectRatio }}\n draggable={!isDisabled && allowReordering}\n onDragStart={handleDragStart}\n onDragOver={preventDefaultHandler}\n onDrop={handleDrop}\n >\n <ImagePreview isDisabled={isDisabled} fileUrl={fileUrl} index={index} onChange={handleChange} />\n </div>\n )\n})\n\nconst DefaultImagePreview = React.memo(function DefaultImagePreview({\n fileUrl,\n isDisabled,\n onChange\n}: MultiImageInputPreviewProps) {\n return (\n <>\n <img src={fileUrl} alt=\"Preview\" className=\"w-full h-full object-cover rounded-md\" />\n {!isDisabled && (\n <button\n type=\"button\"\n aria-label=\"Remove image\"\n onClick={() => onChange(null)}\n className=\"absolute top-1 right-1 p-1 bg-black/50 hover:bg-red-600 rounded-full transition-all duration-200 hover:scale-110\"\n >\n <Icon name=\"x-mark\" className=\"h-4 w-4 text-white\" />\n </button>\n )}\n </>\n )\n})\n\n// ── Static helpers ──────────────────────────────────────────────────────────\n\nconst DRAG_OVER_STYLE = {\n background: 'repeating-linear-gradient(45deg, #e5e7eb, #e5e7eb 20px, #d1d5db 20px, #d1d5db 50px)'\n} as const\n\nfunction DragOverContent() {\n return (\n <div className=\"absolute inset-0 p-6 rounded-md flex items-center justify-center text-center\" style={DRAG_OVER_STYLE}>\n <div>\n <Icon name=\"cloud-arrow-up\" className=\"h-12 w-12 mx-auto text-gray-400\" />\n <span className=\"mt-2 block text-sm font-semibold text-gray-900\">Drop Image</span>\n </div>\n </div>\n )\n}\n\nfunction preventDefaultHandler(e: React.DragEvent) {\n e.preventDefault()\n}\n"}]},{"name":"pdf-input","dependencies":["tw","form-field","compose-refs","file-input-utils","pdf-dist","icon","headless-file-input","pdf","use-pagination"],"files":[{"name":"components/pdf-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add pdf-input --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { formatFileSize, checkFileType, checkFileMaxSize } from '../utils/file-input'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessFileInput } from './headless-file-input'\nimport { Icon } from './icon'\nimport { Pdf } from './pdf'\n\nexport type PdfInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n emptyState?: React.ReactNode\n renderPreview?: (props: PdfInputPreviewProps) => React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string | File | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (File object or string URL, held in memory until form submit)\n value?: string | File | null\n defaultValue?: string | File | null\n onChange?: (value: string | File | null) => void\n\n maxSize?: number // in bytes\n}\n\nexport interface PdfInputPreviewProps {\n fileUrl: string\n isDisabled: boolean\n onChange(value: string | null | File): unknown\n inputId: string\n name: string | undefined\n inputRef: React.Ref<HTMLInputElement>\n}\n\nexport function PdfInput(props: PdfInputProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n const exceededMaxSizeFn = React.useCallback(\n (value: string | File | null) => {\n if (value && props.maxSize && value instanceof File) {\n return checkFileMaxSize(value, props.maxSize)\n }\n return null\n },\n [props.maxSize]\n )\n\n const formatNotSupportedFn = React.useCallback((value: string | File | null) => {\n if (value instanceof File) {\n return checkFileType(value, 'application/pdf')\n }\n return null\n }, [])\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? null, (value) => {\n if (props.onChange) {\n props.onChange(value)\n }\n })\n\n const exceededMaxSize = React.useMemo(() => exceededMaxSizeFn(value), [value, exceededMaxSizeFn])\n const formatNotSupported = React.useMemo(() => formatNotSupportedFn(value), [value, formatNotSupportedFn])\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid || Boolean(exceededMaxSize || formatNotSupported)\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const errorMessage = exceededMaxSize || formatNotSupported || validationMessage\n\n const PdfPreview = props.renderPreview ?? DefaultPdfInputPreview\n\n const emptyState = props.emptyState ?? (\n <div className=\"px-6 py-12\">\n <Icon name=\"document\" className=\"h-12 w-12 mx-auto mb-3 text-neutral-400\" />\n <p className=\"text-sm text-neutral-600 font-medium\">Click to upload or drag and drop</p>\n <p className=\"text-xs text-neutral-500 mt-1\">PDF</p>\n {props.maxSize ? <p className=\"text-xs text-neutral-500 mt-1\">Max size: {formatFileSize(props.maxSize)}</p> : null}\n </div>\n )\n\n return (\n <HeadlessFileInput\n value={value}\n onChange={(v) => {\n setValue(v)\n commitValidation()\n }}\n >\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <HeadlessFileInput.Preview>\n {(files) => {\n const hasFile = files.length > 0\n return (\n <div\n className={tw(\n 'pdf-input-border relative block w-full rounded-md overflow-hidden text-center focus:outline-none focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',\n 'border-2 border-neutral-200 hover:border-neutral-300 min-h-56',\n !hasFile && 'border-dashed',\n props.isDisabled && 'border-neutral-100 cursor-not-allowed',\n hasWarning &&\n 'border-notice-300 hover:border-notice-400 text-notice-900 placeholder-notice-300 focus-within:border-notice-500 focus-within:ring-notice-500',\n isInvalid &&\n 'border-negative-300 hover:border-negative-400 text-negative-900 placeholder-negative-300 focus-within:border-negative-500 focus-within:ring-negative-500'\n )}\n >\n {hasFile ? (\n PdfPreview({\n isDisabled: !!props.isDisabled,\n fileUrl: files[0]!.fileUrl,\n onChange: (v) => {\n setValue(v)\n commitValidation()\n },\n inputId,\n inputRef,\n name: props.name\n })\n ) : (\n <HeadlessFileInput.DropZone\n inputRef={inputRef}\n accept=\"application/pdf\"\n name={props.name}\n inputId={inputId}\n disabled={props.isDisabled}\n className=\"size-full\"\n >\n {({ dragOver }) => (\n <div className=\"relative pointer-events-none\">\n {emptyState}\n {dragOver ? (\n <div className=\"absolute inset-0\">\n <DragOverContent />\n </div>\n ) : null}\n </div>\n )}\n </HeadlessFileInput.DropZone>\n )}\n </div>\n )\n }}\n </HeadlessFileInput.Preview>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={errorMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </HeadlessFileInput>\n )\n}\n\nfunction DragOverContent() {\n return (\n <div\n className=\"absolute inset-0 p-12 rounded flex items-center justify-center\"\n style={{\n background: `repeating-linear-gradient(45deg, #e5e7eb, #e5e7eb 20px, #d1d5db 20px, #d1d5db 50px)`\n }}\n >\n <div>\n <Icon name=\"cloud-arrow-up\" className=\"h-12 w-12 mx-auto text-gray-400\" />\n <span className=\"mt-2 block text-sm font-semibold text-gray-900\">Upload a PDF</span>\n </div>\n </div>\n )\n}\n\nexport function DefaultPdfInputPreview({ name, fileUrl, inputRef, isDisabled, onChange, inputId }: PdfInputPreviewProps) {\n return (\n <>\n <HeadlessFileInput.DropZone\n inputRef={inputRef}\n accept=\"application/pdf\"\n name={name}\n inputId={inputId}\n disabled={isDisabled}\n >\n {({ dragOver }) => (\n <div className=\"min-h-56 pointer-events-none\">\n {dragOver ? (\n <div className=\"absolute inset-0\">\n <DragOverContent />\n </div>\n ) : null}\n <div className=\"relative\">\n <div\n aria-hidden=\"true\"\n className=\"absolute inset-x-0 top-0 rounded-t-md h-20 bg-gradient-to-b from-white opacity-75 mix-blend-lighten\"\n />\n <Pdf src={fileUrl} className=\"w-full min-h-96 rounded-md border-0\" />\n </div>\n </div>\n )}\n </HeadlessFileInput.DropZone>\n <div className=\"absolute top-2 right-2\">\n {!isDisabled ? (\n <button\n type=\"button\"\n aria-label=\"Remove\"\n onClick={() => onChange(null)}\n className=\"p-1 h-5 w-5 flex items-center justify-center bg-gray-800/60 hover:bg-gray-500/60 rounded-full transition-colors\"\n >\n <Icon name=\"x-mark\" className=\"h-4 w-4 text-gray-200\" />\n </button>\n ) : null}\n </div>\n </>\n )\n}\n"}]},{"name":"dialog","dependencies":["tw","headless-button","compose-refs","use-render-props","icon"],"files":[{"name":"components/dialog.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add dialog --directory app\n\nimport React from 'react'\nimport type { To, RelativeRoutingType } from 'react-router'\nimport { useNavigate } from 'react-router'\nimport { Modal, ModalOverlay, Dialog as AriaDialog, Heading } from 'react-aria-components'\nimport { tw } from '../utils/tw'\nimport { Button } from './button'\n\nexport type DialogProps = {\n children?: React.ReactNode\n title?: React.ReactNode\n isOpen?: boolean\n close?:\n | ((isOpen: boolean) => void)\n | { to: To; replace?: boolean; relative?: RelativeRoutingType; reloadDocument?: boolean }\n size?: 'xs' | 'sm' | 'lg'\n className?: string\n isDismissable?: boolean\n actions?: React.ReactNode\n}\n\ninterface DialogContextValue {\n close(): void\n}\n\nconst DialogContext = React.createContext<DialogContextValue>({ close: () => undefined })\n\nfunction Dialog_({\n children,\n title,\n isOpen = false,\n close,\n size = 'sm',\n className,\n isDismissable = false,\n actions\n}: DialogProps) {\n const navigate = useNavigate()\n\n const onOpenChange = close\n ? typeof close === 'function'\n ? close\n : (open: boolean) => {\n if (!open) navigate(close.to, { replace: close.replace, relative: close.relative })\n }\n : undefined\n\n return (\n <ModalOverlay\n isOpen={isOpen}\n onOpenChange={onOpenChange}\n isDismissable={isDismissable}\n isKeyboardDismissDisabled={!isDismissable}\n className={tw(\n 'fixed inset-0 z-[2000] flex items-center justify-center bg-black/25 backdrop-blur-sm',\n 'transition-opacity duration-300 ease-in-out',\n 'data-[entering]:opacity-0',\n 'data-[exiting]:opacity-0'\n )}\n >\n <Modal\n className={tw(\n 'w-[calc(100%-4rem)] m-auto',\n size === 'lg' ? 'max-w-[1080px]' : size === 'xs' ? 'max-w-[400px]' : 'max-w-[640px]',\n 'transition-all duration-300 ease-in-out',\n 'data-[entering]:scale-95 data-[entering]:opacity-0',\n 'data-[exiting]:scale-95 data-[exiting]:opacity-0'\n )}\n >\n <AriaDialog\n className={tw(\n 'w-full max-h-[calc(100vh-6rem)] bg-white shadow-xl ring-1 ring-black/5 outline-none rounded-xl',\n 'flex flex-col overflow-x-hidden overflow-y-auto',\n className\n )}\n >\n {({ close }) => (\n <DialogContext.Provider value={{ close }}>\n <div className=\"flex items-start justify-between gap-4 pt-6 px-6\">\n {title ? (\n <Heading slot=\"title\" className=\"flex-1 text-lg font-semibold text-neutral-900\">\n {title}\n </Heading>\n ) : (\n <div />\n )}\n </div>\n\n <div className=\"flex flex-col gap-3 px-6 pb-6\">\n <div className=\"\">{children}</div>\n <div className=\"flex gap-2 items-start\">{actions}</div>\n </div>\n </DialogContext.Provider>\n )}\n </AriaDialog>\n </Modal>\n </ModalOverlay>\n )\n}\n\nfunction CancelButton({ children = 'Cancel' }: { children?: React.ReactNode }) {\n const context = React.useContext(DialogContext)\n return (\n <Button color=\"neutral\" onClick={context.close} variant=\"soft\" shape=\"oval\">\n {children}\n </Button>\n )\n}\n\nexport const Dialog = Object.assign(Dialog_, {\n CancelButton\n})\n"}]},{"name":"drawer","dependencies":["tw","headless-button","compose-refs","use-render-props","icon"],"files":[{"name":"components/drawer.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add drawer --directory app\n\nimport React from 'react'\nimport type { To, RelativeRoutingType } from 'react-router'\nimport { useNavigate } from 'react-router'\nimport { Drawer as VaulDrawer } from 'vaul'\nimport { tw } from '../utils/tw'\n\nexport type DrawerProps = {\n children?: React.ReactNode\n title?: React.ReactNode\n isOpen?: boolean\n close?:\n | ((isOpen: boolean) => void)\n | { to: To; replace?: boolean; relative?: RelativeRoutingType; reloadDocument?: boolean }\n size?: 'sm' | 'md' | 'lg'\n className?: string\n isDismissable?: boolean\n position?: 'left' | 'right' | 'top' | 'bottom'\n actions?: React.ReactNode\n contentsContainerClassName?: string\n}\n\nexport function Drawer({\n children,\n title,\n isOpen = false,\n close,\n size = 'md',\n className,\n isDismissable = true,\n position = 'right',\n actions,\n contentsContainerClassName\n}: DrawerProps) {\n const navigate = useNavigate()\n\n const onOpenChange = close\n ? typeof close === 'function'\n ? close\n : (open: boolean) => {\n if (!open) navigate(close.to, { replace: close.replace, relative: close.relative })\n }\n : undefined\n\n return (\n <VaulDrawer.Root open={isOpen} onOpenChange={onOpenChange} direction={position} dismissible={isDismissable}>\n <VaulDrawer.Portal>\n <VaulDrawer.Overlay className=\"fixed inset-0 z-49 bg-black/25 backdrop-blur-sm\" />\n <VaulDrawer.Content\n className={tw(\n 'flex h-auto flex-col text-sm group/drawer-content fixed z-50',\n 'data-[vaul-drawer-direction=bottom]:bottom-2 data-[vaul-drawer-direction=bottom]:left-2 data-[vaul-drawer-direction=bottom]:right-2 data-[vaul-drawer-direction=bottom]:max-h-[80vh]',\n 'data-[vaul-drawer-direction=left]:left-2 data-[vaul-drawer-direction=left]:top-2 data-[vaul-drawer-direction=left]:bottom-2 data-[vaul-drawer-direction=left]:h-[calc(100%-1rem)] data-[vaul-drawer-direction=left]:w-3/4',\n 'data-[vaul-drawer-direction=right]:right-2 data-[vaul-drawer-direction=right]:top-2 data-[vaul-drawer-direction=right]:bottom-2 data-[vaul-drawer-direction=right]:h-[calc(100%-1rem)] data-[vaul-drawer-direction=right]:w-3/4',\n 'data-[vaul-drawer-direction=top]:top-2 data-[vaul-drawer-direction=top]:left-2 data-[vaul-drawer-direction=top]:right-2 data-[vaul-drawer-direction=top]:max-h-[80vh]',\n size === 'sm'\n ? 'data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm'\n : size === 'md'\n ? 'data-[vaul-drawer-direction=left]:sm:max-w-lg data-[vaul-drawer-direction=right]:sm:max-w-lg'\n : 'data-[vaul-drawer-direction=left]:sm:max-w-3xl data-[vaul-drawer-direction=right]:sm:max-w-3xl',\n className\n )}\n style={{ '--initial-transform': 'calc(100% + 8px)' } as React.CSSProperties}\n >\n {isDismissable ? (\n <div\n className={tw(\n 'absolute bg-neutral-300 rounded-full shrink-0 group-data-[vaul-drawer-direction=bottom]/drawer-content:block',\n 'group-data-[vaul-drawer-direction=left]/drawer-content:h-25 group-data-[vaul-drawer-direction=left]/drawer-content:w-1 group-data-[vaul-drawer-direction=left]/drawer-content:right-2 group-data-[vaul-drawer-direction=left]/drawer-content:top-[calc(50%-50px)]',\n 'group-data-[vaul-drawer-direction=right]/drawer-content:h-25 group-data-[vaul-drawer-direction=right]/drawer-content:w-1 group-data-[vaul-drawer-direction=right]/drawer-content:left-2 group-data-[vaul-drawer-direction=right]/drawer-content:top-[calc(50%-50px)]',\n 'group-data-[vaul-drawer-direction=top]/drawer-content:w-25 group-data-[vaul-drawer-direction=top]/drawer-content:h-1 group-data-[vaul-drawer-direction=top]/drawer-content:bottom-2 group-data-[vaul-drawer-direction=top]/drawer-content:left-[calc(50%-50px)]',\n 'group-data-[vaul-drawer-direction=bottom]/drawer-content:w-25 group-data-[vaul-drawer-direction=bottom]/drawer-content:h-1 group-data-[vaul-drawer-direction=bottom]/drawer-content:top-2 group-data-[vaul-drawer-direction=bottom]/drawer-content:left-[calc(50%-50px)]'\n )}\n />\n ) : null}\n\n <div className=\"bg-white size-full pt-6 px-6 pb-6 rounded-xl shadow-xl ring-1 ring-black/5\">\n <div className=\"flex items-start justify-between gap-4 pb-5\">\n {title ? (\n <VaulDrawer.Title className=\"flex-1 text-xl font-semibold text-neutral-900\">{title}</VaulDrawer.Title>\n ) : (\n <div />\n )}\n <div className=\"flex gap-2 items-start\">{actions}</div>\n </div>\n\n <div className={tw(contentsContainerClassName)}>{children}</div>\n </div>\n </VaulDrawer.Content>\n </VaulDrawer.Portal>\n </VaulDrawer.Root>\n )\n}\n"}]},{"name":"alert-dialog","dependencies":["tw","spinner","button-context","get-button-classes","headless-button","compose-refs","use-prevent-default","use-render-props","use-spin-delay","use-stable-accessor","icon","button-group","button"],"files":[{"name":"components/alert-dialog.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add alert-dialog --directory app\n\nimport * as React from 'react'\nimport { Dialog, Modal, ModalOverlay, Heading } from 'react-aria-components'\nimport { tw } from '../utils/tw'\nimport { Icon } from './icon'\nimport { Button } from './button'\nimport { ButtonGroupContext } from './button-group'\nimport { ButtonContext } from './helpers/button-context'\n\nexport type AlertDialogProps = {\n title: string\n description?: string\n variant?: 'info' | 'positive' | 'neutral' | 'negative' | 'notice'\n icon?: React.ReactNode\n children?: React.ReactNode | ((props: { close(): void }) => React.ReactNode)\n isOpen?: boolean\n onOpenChange?: (isOpen: boolean) => void\n isDismissable?: boolean\n className?: string\n}\n\nconst variantStyles = {\n info: {\n container: 'border-info-500/20',\n icon: 'text-info-500',\n iconBg: 'bg-info-500/10',\n header: 'text-info-500'\n },\n positive: {\n container: 'border-positive-200',\n icon: 'text-positive-600',\n iconBg: 'bg-positive-100',\n header: 'text-positive-800'\n },\n notice: {\n container: 'border-notice-200',\n icon: 'text-notice-600',\n iconBg: 'bg-notice-100',\n header: 'text-notice-800'\n },\n negative: {\n container: 'border-negative-200',\n icon: 'text-negative-600',\n iconBg: 'bg-negative-100',\n header: 'text-negative-800'\n },\n neutral: {\n container: 'border-neutral-200',\n icon: 'text-neutral-600',\n iconBg: 'bg-neutral-100',\n header: 'text-neutral-700'\n }\n}\n\nconst defaultIconNames = {\n info: 'information-circle',\n positive: 'check',\n neutral: 'information-circle',\n negative: 'x-mark',\n notice: 'exclamation-triangle'\n} as const\n\ninterface AlertDialogContextValue {\n close(): void\n}\n\nconst AlertDialogContext = React.createContext<AlertDialogContextValue>({ close: () => undefined })\n\nfunction AlertDialog_({\n description,\n title,\n variant = 'info',\n icon,\n isOpen,\n onOpenChange,\n isDismissable = true,\n className,\n children\n}: AlertDialogProps) {\n const displayIcon = icon !== undefined ? icon : <Icon name={defaultIconNames[variant]} className=\"h-8 w-8\" />\n const styles = variantStyles[variant]\n\n return (\n <ModalOverlay\n isOpen={isOpen}\n onOpenChange={onOpenChange}\n isDismissable={isDismissable}\n isKeyboardDismissDisabled={!isDismissable}\n className={tw(\n 'fixed inset-0 z-[4000] flex items-center justify-center bg-black/25 backdrop-blur-sm',\n 'transition-opacity duration-200 ease-out',\n 'data-[entering]:opacity-0',\n 'data-[exiting]:opacity-0'\n )}\n >\n <Modal\n className={tw(\n 'w-[calc(100%-2rem)] max-w-sm',\n 'transition-all duration-200 ease-out',\n 'data-[entering]:scale-95 data-[entering]:opacity-0 data-[entering]:translate-y-2',\n 'data-[exiting]:scale-95 data-[exiting]:opacity-0 data-[exiting]:translate-y-2'\n )}\n >\n <Dialog\n className={tw('outline-none bg-white rounded-xl shadow-xl ring-1 ring-black/5 w-full', className)}\n role=\"alertdialog\"\n >\n {({ close }) => (\n <AlertDialogContext.Provider value={{ close }}>\n <ButtonGroupContext.Provider\n value={{ orientation: 'horizontal', alignment: 'center', fullWidth: true, className: 'flex-nowrap' }}\n >\n <ButtonContext.Provider value={{ shape: 'rectangle', variant: 'soft', fullWidth: true }}>\n <div className={tw('border rounded-xl p-6', styles.container)}>\n <div className=\"flex flex-col items-center text-center\">\n {displayIcon && (\n <div className={tw('w-12 h-12 rounded-xl flex items-center justify-center mb-4', styles.iconBg)}>\n <div className={tw(styles.icon)}>{displayIcon}</div>\n </div>\n )}\n <Heading\n slot=\"title\"\n className={tw('text-xl font-semibold', description ? 'mb-2' : 'mb-5', styles.header)}\n >\n {title}\n </Heading>\n {description && <div className=\"text-sm text-neutral-600 mb-5\">{description}</div>}\n {typeof children === 'function' ? children({ close }) : children}\n </div>\n </div>\n </ButtonContext.Provider>\n </ButtonGroupContext.Provider>\n </AlertDialogContext.Provider>\n )}\n </Dialog>\n </Modal>\n </ModalOverlay>\n )\n}\n\nAlertDialog_.displayName = 'AlertDialog'\n\nfunction CancelButton({ children = 'Cancel' }: { children?: React.ReactNode }) {\n const context = React.useContext(AlertDialogContext)\n return (\n <Button color=\"neutral\" onClick={context.close} variant=\"soft\" shape=\"rectangle\">\n {children}\n </Button>\n )\n}\n\nexport const AlertDialog = Object.assign(AlertDialog_, {\n CancelButton\n})\n"}]},{"name":"menu","dependencies":["tw","animated-popover"],"files":[{"name":"components/menu.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add menu --directory app\n\nimport * as React from 'react'\nimport {\n Menu as AriaMenu,\n MenuItem as AriaMenuItem,\n MenuSection as AriaMenuSection,\n MenuTrigger as AriaMenuTrigger,\n Pressable,\n Separator as AriaSeparator\n} from 'react-aria-components'\nimport { Link, useHref, type RelativeRoutingType, type To } from 'react-router'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport { tw } from '../utils/tw'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type MenuProps = {\n /** Trigger element. Can be a ReactNode or render function receiving `{ isOpen }`. */\n Button: React.ReactNode | ((props: { isOpen: boolean }) => React.ReactElement)\n children?: React.ReactNode\n className?: string\n containerClassName?: string\n placement?: 'bottom' | 'bottom start' | 'bottom end' | 'top' | 'top start' | 'top end' | 'left' | 'right'\n}\n\nexport type MenuItemProps = {\n children?: React.ReactNode\n className?: string\n isDisabled?: boolean\n id?: string\n textValue?: string\n type?: 'button' | 'submit'\n onAction?: () => void\n onClick?: ((e: React.MouseEvent<Element & HTMLOrSVGElement, MouseEvent>) => void) | undefined\n form?: string\n name?: string\n value?: string\n}\n\nexport type MenuLinkItemProps = {\n to: To\n relative?: RelativeRoutingType\n children?: React.ReactNode\n className?: string\n isDisabled?: boolean\n target?: React.HTMLAttributeAnchorTarget\n rel?: string\n id?: string\n textValue?: string\n}\n\nexport type MenuGroupProps = {\n children?: React.ReactNode\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction itemClassName({ isFocused, isDisabled }: { isFocused: boolean; isDisabled: boolean }, className?: string) {\n return tw(\n isFocused ? 'bg-neutral-50 text-neutral-900' : 'text-neutral-700',\n 'group flex w-full items-center px-4 py-2 text-sm gap-2 outline-none',\n isDisabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',\n className\n )\n}\n\n// ── MenuMain ──────────────────────────────────────────────────────────────────\n\nfunction MenuMain({ Button: ButtonProp, children, className, containerClassName, placement = 'bottom end' }: MenuProps) {\n const [isOpen, setIsOpen] = React.useState(false)\n\n const triggerContent = typeof ButtonProp === 'function' ? ButtonProp({ isOpen }) : ButtonProp\n\n return (\n <div className={tw('relative inline-block text-left', containerClassName)}>\n <AriaMenuTrigger isOpen={isOpen} onOpenChange={setIsOpen}>\n <Pressable>\n {React.isValidElement(triggerContent) ? (\n triggerContent\n ) : (\n <button type=\"button\" className=\"inline-flex items-center focus:outline-none\">\n {triggerContent}\n </button>\n )}\n </Pressable>\n <AnimatedPopover placement={placement} offset={8}>\n <AriaMenu\n className={tw(\n 'overflow-hidden min-w-56 w-max rounded-lg bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none',\n className\n )}\n autoFocus=\"first\"\n >\n {children}\n </AriaMenu>\n </AnimatedPopover>\n </AriaMenuTrigger>\n </div>\n )\n}\n\nMenuMain.displayName = 'Menu'\n\n// ── MenuItem ──────────────────────────────────────────────────────────────────\n\nfunction Item({\n children,\n className,\n isDisabled,\n id,\n textValue,\n type,\n onAction,\n onClick,\n form,\n name,\n value\n}: MenuItemProps) {\n const itemRef = React.useRef<HTMLDivElement>(null)\n const isSubmit = type === 'submit'\n\n return (\n <AriaMenuItem\n id={id}\n ref={isSubmit ? itemRef : undefined}\n onClick={onClick}\n textValue={textValue ?? (typeof children === 'string' ? children : undefined)}\n onAction={\n isSubmit\n ? () => {\n onAction?.()\n const targetForm = form\n ? (document.getElementById(form) as HTMLFormElement | null)\n : (itemRef.current?.closest('form') ?? null)\n if (!targetForm) return\n targetForm.requestSubmit()\n }\n : onAction\n }\n isDisabled={isDisabled}\n className={(renderProps) => itemClassName(renderProps, className)}\n >\n {children}\n {isSubmit && <input type=\"submit\" className=\"sr-only\" name={name} value={value ?? ''} form={form} />}\n </AriaMenuItem>\n )\n}\n\n// ── LinkItem ──────────────────────────────────────────────────────────────────\n\nfunction LinkItem({ to, children, className, relative, isDisabled, target, rel, id, textValue }: MenuLinkItemProps) {\n return (\n <AriaMenuItem\n id={id}\n href={useHref(to, { relative })}\n target={target}\n rel={rel}\n textValue={textValue ?? (typeof children === 'string' ? children : undefined)}\n isDisabled={isDisabled}\n className={(renderProps) => itemClassName(renderProps, className)}\n render={(props) => <Link {...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)} relative={relative} to={to} />}\n >\n {children}\n </AriaMenuItem>\n )\n}\n\n// ── Group ─────────────────────────────────────────────────────────────────────\n\nfunction Group({ children }: MenuGroupProps) {\n return <AriaMenuSection className=\"py-1\">{children}</AriaMenuSection>\n}\n\n// ── Separator ─────────────────────────────────────────────────────────────────\n\nfunction MenuSeparator() {\n return <AriaSeparator className=\"border-t border-neutral-950/5\" />\n}\n\n// ── Export ─────────────────────────────────────────────────────────────────────\n\nexport const Menu = Object.assign(MenuMain, {\n Item,\n LinkItem,\n Group,\n Separator: MenuSeparator\n})\n"}]},{"name":"tabs","dependencies":["tw","use-tab-indicator"],"files":[{"name":"components/tabs.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add tabs --directory app\n\nimport * as React from 'react'\nimport { Tabs as AriaTabs, TabList as AriaTabList, Tab as AriaTab, TabPanel as AriaTabPanel } from 'react-aria-components'\nimport type { Key } from 'react-aria-components'\nimport { tw } from '../utils/tw'\nimport {\n TAB_BASE_CLASSES,\n TAB_DISABLED_CLASSES,\n TAB_INACTIVE_CLASSES,\n TAB_OVAL_BASE_CLASSES,\n TAB_OVAL_SELECTED_CLASSES,\n tabColorMap,\n tabListContainerClasses,\n tabListInnerClasses,\n getIndicatorClasses,\n useAnimatedIndicator\n} from '../utils/use-tab-indicator'\nimport type { TabColor, TabVariant } from '../utils/use-tab-indicator'\n\n// --- Types ---\n\nexport type TabGroupProps = {\n children: React.ReactNode\n className?: string\n defaultSelectedKey?: Key\n selectedKey?: Key\n onSelectionChange?: (key: Key) => void\n keyboardActivation?: 'automatic' | 'manual'\n orientation?: 'horizontal' | 'vertical'\n isDisabled?: boolean\n}\n\nexport type TabListProps = {\n children: React.ReactNode\n className?: string\n color?: TabColor\n variant?: TabVariant\n}\n\nexport type TabProps = {\n id: Key\n children: React.ReactNode\n className?: string\n isDisabled?: boolean\n}\n\nexport type TabPanelsProps = {\n children: React.ReactNode\n className?: string\n}\n\nexport type TabPanelProps = {\n id: Key\n children: React.ReactNode\n className?: string\n shouldForceMount?: boolean\n}\n\n// --- TabGroup ---\n\nfunction TabGroup({ children, className, ...props }: TabGroupProps) {\n return (\n <AriaTabs {...props} className={tw('w-full', className)}>\n {children}\n </AriaTabs>\n )\n}\n\nTabGroup.displayName = 'TabGroup'\n\n// --- TabList ---\n\nconst TabListContext = React.createContext<{ color: TabColor; variant: TabVariant }>({\n color: 'primary',\n variant: 'underline'\n})\n\nfunction TabList({ children, className, color = 'primary', variant = 'underline' }: TabListProps) {\n const containerRef = React.useRef<HTMLDivElement>(null)\n const indicatorRef = useAnimatedIndicator(containerRef, '[data-selected]', 'data-selected')\n\n return (\n <TabListContext.Provider value={{ color, variant }}>\n <div ref={containerRef} className={tabListContainerClasses[variant]}>\n <AriaTabList className={tw(tabListInnerClasses[variant], className)}>{children}</AriaTabList>\n <div ref={indicatorRef} className={getIndicatorClasses(variant, color)} style={{ opacity: 0 }} />\n </div>\n </TabListContext.Provider>\n )\n}\n\nTabList.displayName = 'TabList'\n\n// --- Tab ---\n\nfunction TabItem({ children, className, ...props }: TabProps) {\n const { color, variant } = React.useContext(TabListContext)\n const colors = tabColorMap[color]\n const focusClasses = `outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:${colors.outline}`\n\n return (\n <AriaTab\n {...props}\n className={({ isSelected, isDisabled }) =>\n tw(\n variant === 'oval' ? TAB_OVAL_BASE_CLASSES : TAB_BASE_CLASSES,\n focusClasses,\n isSelected\n ? variant === 'oval'\n ? TAB_OVAL_SELECTED_CLASSES\n : `${colors.text} font-bold`\n : `${TAB_INACTIVE_CLASSES} data-[hovered]:text-neutral-700`,\n isDisabled && TAB_DISABLED_CLASSES,\n className\n )\n }\n >\n {children}\n </AriaTab>\n )\n}\n\nTabItem.displayName = 'Tab'\n\n// --- TabPanels ---\n\nfunction TabPanels({ children, className }: TabPanelsProps) {\n return <div className={tw('mt-2', className)}>{children}</div>\n}\n\nTabPanels.displayName = 'TabPanels'\n\n// --- TabPanel ---\n\nfunction TabPanelItem({ children, className, ...props }: TabPanelProps) {\n return (\n <AriaTabPanel {...props} className={className}>\n {children}\n </AriaTabPanel>\n )\n}\n\nTabPanelItem.displayName = 'TabPanel'\n\n// --- Export ---\n\nexport const Tabs = {\n Group: TabGroup,\n List: TabList,\n Tab: TabItem,\n Panels: TabPanels,\n Panel: TabPanelItem\n}\n"}]},{"name":"link-tabs","dependencies":["tw","use-tab-indicator"],"files":[{"name":"components/link-tabs.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add link-tabs --directory app\n\nimport * as React from 'react'\nimport { NavLink } from 'react-router'\nimport type { To } from 'react-router'\nimport { tw } from '../utils/tw'\nimport {\n TAB_BASE_CLASSES,\n TAB_DISABLED_CLASSES,\n TAB_INACTIVE_CLASSES,\n TAB_OVAL_BASE_CLASSES,\n TAB_OVAL_SELECTED_CLASSES,\n tabColorMap,\n tabListContainerClasses,\n tabListInnerClasses,\n getIndicatorClasses,\n useAnimatedIndicator\n} from '../utils/use-tab-indicator'\nimport type { TabColor, TabVariant } from '../utils/use-tab-indicator'\n\n// --- Types ---\n\nexport type LinkTabListProps = {\n children: React.ReactNode\n className?: string\n color?: TabColor\n variant?: TabVariant\n}\n\nexport type LinkTabProps = {\n to: To\n children: React.ReactNode\n className?: string\n disabled?: boolean\n replace?: boolean\n end?: boolean\n}\n\n// --- LinkTabList ---\n\nconst LinkTabListContext = React.createContext<{ color: TabColor; variant: TabVariant }>({\n color: 'primary',\n variant: 'underline'\n})\n\nfunction LinkTabList({ children, className, color = 'primary', variant = 'underline' }: LinkTabListProps) {\n const containerRef = React.useRef<HTMLDivElement>(null)\n const indicatorRef = useAnimatedIndicator(containerRef, '[aria-current=\"page\"]', 'aria-current')\n\n return (\n <LinkTabListContext.Provider value={{ color, variant }}>\n <div ref={containerRef} className={tabListContainerClasses[variant]}>\n <div role=\"tablist\" className={tw(tabListInnerClasses[variant], className)}>\n {children}\n </div>\n <div ref={indicatorRef} className={getIndicatorClasses(variant, color)} style={{ opacity: 0 }} />\n </div>\n </LinkTabListContext.Provider>\n )\n}\n\nLinkTabList.displayName = 'LinkTabList'\n\n// --- LinkTab ---\n\nfunction LinkTab({ to, children, className, disabled, replace, end }: LinkTabProps) {\n const { color, variant } = React.useContext(LinkTabListContext)\n const colors = tabColorMap[color]\n const baseClasses = variant === 'oval' ? TAB_OVAL_BASE_CLASSES : TAB_BASE_CLASSES\n const focusClasses = `outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:${colors.outline}`\n\n if (disabled) {\n return (\n <span\n role=\"tab\"\n aria-disabled=\"true\"\n className={tw(baseClasses, 'outline-none', TAB_INACTIVE_CLASSES, TAB_DISABLED_CLASSES, className)}\n >\n {children}\n </span>\n )\n }\n\n return (\n <NavLink\n to={to}\n replace={replace}\n end={end}\n role=\"tab\"\n className={({ isActive }) =>\n tw(\n baseClasses,\n focusClasses,\n isActive\n ? variant === 'oval'\n ? TAB_OVAL_SELECTED_CLASSES\n : `${colors.text} font-bold`\n : `${TAB_INACTIVE_CLASSES} hover:text-neutral-700`,\n className\n )\n }\n >\n {children}\n </NavLink>\n )\n}\n\nLinkTab.displayName = 'LinkTab'\n\n// --- Export ---\n\nexport const LinkTabs = {\n List: LinkTabList,\n Tab: LinkTab\n}\n"}]},{"name":"stepper","dependencies":["tw","icon"],"files":[{"name":"components/stepper.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add stepper --directory app\n\nimport * as React from 'react'\nimport { Icon } from './icon'\nimport { tw } from '../utils/tw'\n\n// ── Types ────────────────────────────────────────────────────────────────────────\n\nexport type StepperGroupProps = {\n children: React.ReactNode\n className?: string\n activeStep?: number\n orientation?: 'horizontal' | 'vertical'\n}\n\nexport type StepProps = {\n children: React.ReactNode\n className?: string\n completed?: boolean\n error?: boolean\n}\n\nexport type StepLabelProps = {\n children: React.ReactNode\n className?: string\n}\n\n// ── Context ─────────────────────────────────────────────────────────────────────\n\nconst StepperContext = React.createContext<{\n activeStep: number\n orientation: 'horizontal' | 'vertical'\n totalSteps: number\n}>({\n activeStep: 0,\n orientation: 'horizontal',\n totalSteps: 0\n})\n\nconst StepContext = React.createContext<{\n completed: boolean\n active: boolean\n error: boolean\n index: number\n}>({\n completed: false,\n active: false,\n error: false,\n index: 0\n})\n\n// ── StepperGroup ────────────────────────────────────────────────────────────────\n\nfunction StepperGroup({ children, className, activeStep = 0, orientation = 'horizontal' }: StepperGroupProps) {\n const steps = React.Children.toArray(children).filter((child) => React.isValidElement(child))\n const totalSteps = steps.length\n\n return (\n <StepperContext.Provider value={{ activeStep, orientation, totalSteps }}>\n <div\n className={tw('flex', orientation === 'horizontal' ? 'flex-row' : 'flex-col', className)}\n role=\"group\"\n aria-label=\"Progress\"\n >\n {steps.map((step, idx) => {\n if (React.isValidElement<StepProps & { index?: number; isLast?: boolean }>(step)) {\n return React.cloneElement(step, {\n ...step.props,\n index: idx,\n isLast: idx === totalSteps - 1,\n key: idx\n })\n }\n return step\n })}\n </div>\n </StepperContext.Provider>\n )\n}\n\nStepperGroup.displayName = 'StepperGroup'\n\n// ── Step ────────────────────────────────────────────────────────────────────────\n\nfunction Step({\n children,\n className,\n completed = false,\n error = false,\n index = 0,\n isLast\n}: StepProps & { index?: number; isLast?: boolean }) {\n const { activeStep, orientation } = React.useContext(StepperContext)\n const active = activeStep === index\n\n const firstConnectorColor = error ? 'bg-negative-500' : completed || active ? 'bg-primary-500' : 'bg-neutral-300'\n const lastConnectorColor = error ? 'bg-negative-500' : completed ? 'bg-primary-500' : 'bg-neutral-300'\n\n if (orientation === 'horizontal') {\n return (\n <StepContext.Provider value={{ completed, active, error, index }}>\n <div className={tw('flex flex-row items-start flex-1 relative', className)}>\n {index !== 0 ? (\n <div className=\"flex flex-1 items-center pt-4 w-1/2 absolute left-0\">\n <div className={tw('h-0.5 w-full transition-colors', firstConnectorColor)} />\n </div>\n ) : null}\n {!isLast ? (\n <div className=\"flex flex-1 items-center pt-4 w-1/2 absolute right-0\">\n <div className={tw('h-0.5 w-full transition-colors', lastConnectorColor)} />\n </div>\n ) : null}\n <div className=\"flex flex-col items-center w-full relative\">{children}</div>\n </div>\n </StepContext.Provider>\n )\n }\n\n return (\n <StepContext.Provider value={{ completed, active, error, index }}>\n <div className={tw('relative flex flex-col min-h-10', className)}>\n {index !== 0 ? (\n <div className=\"flex flex-1 justify-center pl-4 h-1/2 absolute top-0\">\n <div className={tw('w-0.5 h-full transition-colors', firstConnectorColor)} />\n </div>\n ) : null}\n {!isLast ? (\n <div className=\"flex flex-1 items-center pl-4 h-1/2 absolute bottom-0\">\n <div className={tw('w-0.5 h-full transition-colors', lastConnectorColor)} />\n </div>\n ) : null}\n <div className=\"flex flex-row w-full relative\">{children}</div>\n </div>\n </StepContext.Provider>\n )\n}\n\nStep.displayName = 'Step'\n\n// ── StepLabel ───────────────────────────────────────────────────────────────────\n\nfunction StepLabel({ children, className }: StepLabelProps) {\n const { completed, active, error, index } = React.useContext(StepContext)\n const { orientation } = React.useContext(StepperContext)\n\n const iconBg = error\n ? 'bg-negative-500 border-negative-500 text-white'\n : completed\n ? 'bg-primary-500 border-primary-500 text-white'\n : active\n ? 'bg-primary-500 border-primary-500 text-white'\n : 'bg-white border-neutral-300 text-neutral-600'\n\n const textColor = error ? 'text-negative-600' : active ? 'text-neutral-900' : 'text-neutral-500'\n\n return (\n <div className={tw('flex items-center', orientation === 'horizontal' ? 'flex-col text-center' : 'gap-3', className)}>\n <div\n className={tw('flex size-8 shrink-0 items-center justify-center rounded-full border-2 transition-colors', iconBg)}\n aria-current={active ? 'step' : undefined}\n >\n {completed ? (\n <Icon name=\"check\" className=\"size-4\" aria-hidden=\"true\" />\n ) : (\n <span className=\"text-sm font-semibold\">{index + 1}</span>\n )}\n </div>\n <div className={tw('flex flex-col', orientation === 'horizontal' ? 'mt-2' : '')}>\n <span className={tw('text-sm font-medium', textColor)}>{children}</span>\n </div>\n </div>\n )\n}\n\nStepLabel.displayName = 'StepLabel'\n\n// ── Export ───────────────────────────────────────────────────────────────────────\n\nexport const Stepper = {\n Group: StepperGroup,\n Step,\n Label: StepLabel\n}\n"}]},{"name":"avatar","dependencies":["tw"],"files":[{"name":"components/avatar.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add avatar --directory app\n\nimport * as React from 'react'\nimport { tw } from '../utils/tw'\n\nexport type AvatarShape = 'circle' | 'rounded' | 'square'\n\nexport type AvatarProps = {\n alt?: string\n children?: React.ReactNode\n className?: string\n src?: string\n shape?: AvatarShape\n}\n\nexport const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(\n ({ alt, children, className, src, shape = 'circle' }, ref) => {\n const [imgError, setImgError] = React.useState(false)\n\n const showImage = src && !imgError\n const childrenToRender = children || (alt && stringAvatar(alt))\n\n return (\n <div\n ref={ref}\n className={tw(\n 'size-10 relative inline-flex items-center justify-center overflow-hidden font-medium text-white select-none ring-1 ring-black/5',\n !showImage && 'bg-neutral-300',\n shape === 'circle' && 'rounded-full',\n shape === 'rounded' && 'rounded',\n shape === 'square' && 'rounded-none',\n className\n )}\n >\n {showImage ? (\n <img className=\"size-full absolute inset-0 object-cover\" alt={alt} src={src} onError={() => setImgError(true)} />\n ) : null}\n {!showImage && childrenToRender ? <span>{childrenToRender}</span> : null}\n </div>\n )\n }\n)\n\nAvatar.displayName = 'Avatar'\n\nfunction stringAvatar(name: string) {\n const names = name.split(' ')\n const first = names[0]\n const last = names[names.length - 1]\n\n if (!first) return ''\n\n const initials = names.length === 1 ? (first[0] ?? '') : `${first[0] ?? ''}${last?.[0] ?? ''}`\n\n return initials.toUpperCase()\n}\n"}]},{"name":"chip","dependencies":["tw","icon"],"files":[{"name":"components/chip.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add chip --directory app\n\nimport * as React from 'react'\nimport { tw } from '../utils/tw'\nimport { Icon } from './icon'\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\nexport type ChipProps = {\n children: React.ReactNode\n onRemove?: () => void\n isDisabled?: boolean\n size?: 'sm' | 'md' | 'lg'\n color?: 'stone' | 'info' | 'green' | 'yellow' | 'orange' | 'red'\n colorMode?: 'light' | 'dark'\n className?: string\n style?: React.CSSProperties\n}\n\nexport type ChipButtonProps = {\n children: React.ReactNode\n disabled?: boolean\n size?: 'sm' | 'md' | 'lg'\n color?: 'stone' | 'info' | 'green' | 'yellow' | 'orange' | 'red'\n colorMode?: 'light' | 'dark'\n className?: string\n} & Omit<React.ComponentProps<'button'>, 'disabled' | 'size' | 'color'>\n\n// ── Chip ──────────────────────────────────────────────────────────────────────\n\nexport function Chip(props: ChipProps) {\n const {\n children,\n onRemove,\n isDisabled = false,\n size = 'md',\n color = 'stone',\n colorMode = 'light',\n className,\n style\n } = props\n\n const colorClasses = colorMode === 'dark' ? darkColorClasses : lightColorClasses\n\n return (\n <div\n className={tw(\n 'inline-flex items-center rounded-full font-normal transition-colors max-w-full',\n sizeClasses[size],\n !isDisabled && !style && colorClasses[color],\n isDisabled && 'opacity-50 cursor-not-allowed',\n className\n )}\n style={style}\n >\n <span className=\"truncate\">{children}</span>\n {onRemove && (\n <button\n type=\"button\"\n onClick={onRemove}\n disabled={isDisabled}\n className={tw(\n 'inline-flex items-center justify-center rounded-full transition-colors translate-x-1',\n removeBtnSizeClasses[size],\n !isDisabled && 'hover:bg-neutral-950/10',\n isDisabled && 'cursor-not-allowed'\n )}\n aria-label=\"Remove\"\n tabIndex={-1}\n >\n <Icon name=\"x-mark\" className={removeIconSizeClasses[size]} aria-hidden=\"true\" />\n </button>\n )}\n </div>\n )\n}\n\n// ── ChipButton ────────────────────────────────────────────────────────────────\n\nexport const ChipButton = React.forwardRef<HTMLButtonElement, ChipButtonProps>((props, ref) => {\n const { children, size = 'md', color = 'stone', colorMode = 'light', className, disabled, ...rest } = props\n\n const colorClasses = colorMode === 'dark' ? darkHoverColorClasses : lightHoverColorClasses\n\n return (\n <button\n ref={ref}\n type=\"button\"\n disabled={disabled}\n className={tw(\n 'inline-flex items-center rounded-full font-normal transition-colors',\n sizeClasses[size],\n !disabled && colorClasses[color],\n disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',\n className\n )}\n {...rest}\n >\n {children}\n </button>\n )\n})\n\nChipButton.displayName = 'ChipButton'\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nconst sizeClasses = {\n sm: 'px-2 py-0.5 text-xs',\n md: 'px-2.5 py-1 text-sm',\n lg: 'px-3 py-1.5 text-base'\n}\n\nconst lightColorClasses = {\n stone: 'bg-stone-100 text-stone-700',\n info: 'bg-info-100 text-info-700',\n green: 'bg-green-100 text-green-700',\n yellow: 'bg-yellow-100 text-yellow-700',\n orange: 'bg-orange-100 text-orange-700',\n red: 'bg-red-100 text-red-700'\n}\n\nconst darkColorClasses = {\n stone: 'bg-stone-600 text-white',\n info: 'bg-info-600 text-white',\n green: 'bg-green-600 text-white',\n yellow: 'bg-yellow-600 text-white',\n orange: 'bg-orange-600 text-white',\n red: 'bg-red-600 text-white'\n}\n\nconst lightHoverColorClasses = {\n stone: 'bg-stone-100 text-stone-700 hover:bg-stone-200',\n info: 'bg-info-100 text-info-700 hover:bg-info-200',\n green: 'bg-green-100 text-green-700 hover:bg-green-200',\n yellow: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200',\n orange: 'bg-orange-100 text-orange-700 hover:bg-orange-200',\n red: 'bg-red-100 text-red-700 hover:bg-red-200'\n}\n\nconst darkHoverColorClasses = {\n stone: 'bg-stone-600 text-white hover:bg-stone-700',\n info: 'bg-info-600 text-white hover:bg-info-700',\n green: 'bg-green-600 text-white hover:bg-green-700',\n yellow: 'bg-yellow-600 text-white hover:bg-yellow-700',\n orange: 'bg-orange-600 text-white hover:bg-orange-700',\n red: 'bg-red-600 text-white hover:bg-red-700'\n}\n\nconst removeBtnSizeClasses = {\n sm: 'size-4 -m-0.5 p-0.5',\n md: 'size-5 -m-1 p-1',\n lg: 'size-6 -m-1.5 p-1.5'\n}\n\nconst removeIconSizeClasses = {\n sm: 'size-2',\n md: 'size-3',\n lg: 'size-4'\n}\n"}]},{"name":"enum-chip","dependencies":["tw","colors","icon","chip"],"files":[{"name":"components/enum-chip.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add enum-chip --directory app\n\nimport * as React from 'react'\nimport { Chip, type ChipProps } from './chip'\nimport { getColor, stringToNumber } from '../utils/enum-colors'\n\nexport type EnumChipProps = {\n options: string[] | Readonly<string[]>\n value: string\n enumLabels?: { [key: string]: string }\n} & Omit<ChipProps, 'children'>\n\nexport function EnumChip({ options, enumLabels, value, style, ...props }: EnumChipProps) {\n const colorIndex = Object.values(options).indexOf(value)\n const colorSeed = colorIndex >= 0 ? colorIndex : stringToNumber(value)\n const color = getColor(colorSeed)\n\n const label = value ? (enumLabels ? enumLabels[value] : value) : ''\n\n return (\n <Chip\n {...props}\n style={{\n backgroundColor: `${color}19`,\n color: color,\n ...style\n }}\n >\n {label}\n </Chip>\n )\n}\n"},{"name":"utils/enum-colors.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add enum-chip --directory app\n\nexport { COLORS, getColor, stringToNumber } from './colors'\n"}]},{"name":"img","dependencies":["tw"],"files":[{"name":"components/img.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add img --directory app\n\nimport * as React from 'react'\nimport { tw } from '../utils/tw'\n\nexport type ImageFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'\nexport type ImageShape = 'rounded' | 'rectangle' | 'circle'\n\nexport type ImgProps = {\n src: string\n alt?: string\n className?: string\n fit?: ImageFit\n shape?: ImageShape\n style?: React.CSSProperties\n onError?: React.ReactEventHandler<HTMLImageElement>\n}\n\nconst fitClasses: Record<ImageFit, string> = {\n cover: 'object-cover object-center',\n contain: 'object-contain',\n fill: 'object-fill',\n none: 'object-none',\n 'scale-down': 'object-scale-down'\n}\n\nconst shapeClasses: Record<ImageShape, string> = {\n rounded: 'rounded',\n rectangle: 'rounded-none',\n circle: 'rounded-full'\n}\n\nexport const Img = React.forwardRef<HTMLImageElement, ImgProps>(\n ({ src, alt, className, fit = 'cover', shape = 'rounded', style, onError }, ref) => {\n return (\n <img\n ref={ref}\n src={src}\n alt={alt}\n className={tw('w-full h-full', fitClasses[fit], shapeClasses[shape], className)}\n style={style}\n onError={onError}\n />\n )\n }\n)\n\nImg.displayName = 'Img'\n"}]},{"name":"labeled-value","dependencies":["tw"],"files":[{"name":"components/labeled-value.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add labeled-value --directory app\n\nimport * as React from 'react'\nimport { tw } from '../utils/tw'\nimport {\n type Iso,\n dateFns,\n timeFns,\n dateTimeFns,\n instantFns,\n zonedDateTimeFns,\n yearMonthFns,\n monthDayFns,\n durationFns\n} from 'iso-fns'\n\ntype LabeledValueBaseProps = {\n label?: React.ReactNode\n align?: 'start' | 'end'\n labelAlign?: 'start' | 'end'\n orientation?: 'horizontal' | 'vertical'\n emptyText?: React.ReactNode\n className?: string\n labelClassName?: string\n valueClassName?: string\n id?: string\n 'aria-labelledby'?: string\n 'aria-describedby'?: string\n}\n\ntype RangeValue<T> = { start: T; end: T }\n\ntype StringProps = {\n value: string | null | undefined\n formatOptions?: never\n}\n\ntype NumberProps = {\n value: number | RangeValue<number> | null | undefined\n formatOptions?: Intl.NumberFormatOptions\n}\n\ntype BooleanProps = {\n value: boolean | null | undefined\n formatOptions?: {\n trueLabel?: string\n falseLabel?: string\n }\n}\n\ntype ReactNodeProps = {\n value: React.ReactNode\n formatOptions?: never\n}\n\ntype StringListProps = {\n value: string[] | null | undefined\n formatOptions?: Intl.ListFormatOptions\n}\n\ntype DateProps = {\n value: Iso.Date | RangeValue<Iso.Date> | null | undefined\n formatOptions?: {\n dateFormat?: string\n }\n}\n\ntype TimeProps = {\n value: Iso.Time | RangeValue<Iso.Time> | null | undefined\n formatOptions?: {\n timeFormat?: string\n }\n}\n\ntype DateTimeProps = {\n value: Iso.DateTime | RangeValue<Iso.DateTime> | null | undefined\n formatOptions?: {\n dateTimeFormat?: string\n }\n}\n\ntype InstantProps = {\n value: Iso.Instant | RangeValue<Iso.Instant> | null | undefined\n formatOptions?: {\n dateTimeFormat?: string\n timeZone?: string\n }\n}\n\ntype ZonedDateTimeProps = {\n value: Iso.ZonedDateTime | RangeValue<Iso.ZonedDateTime> | null | undefined\n formatOptions?: {\n dateTimeFormat?: string\n }\n}\n\ntype YearMonthProps = {\n value: Iso.YearMonth | RangeValue<Iso.YearMonth> | null | undefined\n formatOptions?: {\n dateFormat?: string\n }\n}\n\ntype MonthDayProps = {\n value: Iso.MonthDay | RangeValue<Iso.MonthDay> | null | undefined\n formatOptions?: {\n dateFormat?: string\n }\n}\n\ntype DurationProps = {\n value: Iso.Duration | RangeValue<Iso.Duration> | null | undefined\n formatOptions?: {\n style?: 'short' | 'long'\n }\n}\n\ntype LabeledValueFormatProps =\n | StringProps\n | NumberProps\n | BooleanProps\n | ReactNodeProps\n | StringListProps\n | DateProps\n | TimeProps\n | DateTimeProps\n | InstantProps\n | ZonedDateTimeProps\n | YearMonthProps\n | MonthDayProps\n | DurationProps\n\nexport type LabeledValueProps = LabeledValueBaseProps & LabeledValueFormatProps\n\nexport const LabeledValue = React.forwardRef<HTMLDivElement, LabeledValueProps>(function LabeledValue(props, ref) {\n const {\n label,\n align = 'start',\n labelAlign = 'start',\n orientation = 'vertical',\n className,\n labelClassName,\n valueClassName,\n id,\n 'aria-labelledby': ariaLabelledBy,\n 'aria-describedby': ariaDescribedBy\n } = props\n\n const labelId = React.useId()\n const actualLabelId = id ?? labelId\n\n const formattedValue = FormattedValue(props)\n\n return (\n <div\n ref={ref}\n className={tw('relative', orientation === 'horizontal' ? 'flex items-baseline gap-4' : 'block', className)}\n >\n {label && (\n <LabeledValueLabel\n id={actualLabelId}\n className={tw(\n orientation === 'horizontal' ? 'shrink-0' : 'mb-1',\n labelAlign === 'end' && 'text-right',\n labelClassName\n )}\n >\n {label}\n </LabeledValueLabel>\n )}\n\n <div\n className={tw(\n 'text-sm text-neutral-900 min-h-4 max-w-full break-words overflow-wrap-anywhere',\n align === 'end' && 'text-right',\n !formattedValue && 'text-neutral-400',\n valueClassName\n )}\n aria-labelledby={ariaLabelledBy ?? (label ? actualLabelId : undefined)}\n aria-describedby={ariaDescribedBy}\n >\n {formattedValue}\n </div>\n </div>\n )\n})\n\nLabeledValue.displayName = 'LabeledValue'\n\nfunction FormattedValue(props: LabeledValueFormatProps & { emptyText?: React.ReactNode }) {\n if (props.value == null || props.value === '') {\n return <>{props.emptyText}</>\n }\n\n if (React.isValidElement(props.value)) {\n return <>{props.value}</>\n }\n\n if (typeof props.value === 'boolean') {\n const boolProps = props as BooleanProps\n return <>{props.value ? (boolProps.formatOptions?.trueLabel ?? 'Yes') : (boolProps.formatOptions?.falseLabel ?? 'No')}</>\n }\n\n if (Array.isArray(props.value)) {\n return <ListValue {...(props as StringListProps)} />\n }\n\n if (typeof props.value === 'object' && props.value && 'start' in props.value && 'end' in props.value) {\n if (typeof props.value.start === 'number') {\n return <NumberValue {...(props as NumberProps)} />\n }\n if (typeof props.value.start === 'string' && dateFns.isValid(props.value.start)) {\n return <DateValue {...(props as DateProps)} />\n }\n if (typeof props.value.start === 'string' && timeFns.isValid(props.value.start)) {\n return <TimeValue {...(props as TimeProps)} />\n }\n if (typeof props.value.start === 'string' && dateTimeFns.isValid(props.value.start)) {\n return <DateTimeValue {...(props as DateTimeProps)} />\n }\n if (typeof props.value.start === 'string' && instantFns.isValid(props.value.start)) {\n return <InstantValue {...(props as InstantProps)} />\n }\n if (typeof props.value.start === 'string' && zonedDateTimeFns.isValid(props.value.start)) {\n return <ZonedDateTimeValue {...(props as ZonedDateTimeProps)} />\n }\n if (typeof props.value.start === 'string' && yearMonthFns.isValid(props.value.start)) {\n return <YearMonthValue {...(props as YearMonthProps)} />\n }\n if (typeof props.value.start === 'string' && monthDayFns.isValid(props.value.start)) {\n return <MonthDayValue {...(props as MonthDayProps)} />\n }\n if (typeof props.value.start === 'string' && durationFns.isValid(props.value.start)) {\n return <DurationValue {...(props as DurationProps)} />\n }\n }\n\n if (typeof props.value === 'string') {\n if (dateFns.isValid(props.value)) {\n return <DateValue {...(props as DateProps)} />\n }\n if (timeFns.isValid(props.value)) {\n return <TimeValue {...(props as TimeProps)} />\n }\n if (dateTimeFns.isValid(props.value)) {\n return <DateTimeValue {...(props as DateTimeProps)} />\n }\n if (instantFns.isValid(props.value)) {\n return <InstantValue {...(props as InstantProps)} />\n }\n if (zonedDateTimeFns.isValid(props.value)) {\n return <ZonedDateTimeValue {...(props as ZonedDateTimeProps)} />\n }\n if (yearMonthFns.isValid(props.value)) {\n return <YearMonthValue {...(props as YearMonthProps)} />\n }\n if (monthDayFns.isValid(props.value)) {\n return <MonthDayValue {...(props as MonthDayProps)} />\n }\n if (durationFns.isValid(props.value)) {\n return <DurationValue {...(props as DurationProps)} />\n }\n\n return <>{props.value}</>\n }\n\n if (typeof props.value === 'number') {\n return <NumberValue {...(props as NumberProps)} />\n }\n\n return <>{String(props.value)}</>\n}\n\nfunction NumberValue(props: NumberProps) {\n const formatter = React.useMemo(() => new Intl.NumberFormat('en-us', props.formatOptions ?? {}), [props.formatOptions])\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${formatter.format(props.value.start)} – ${formatter.format(props.value.end)}`\n } else if (typeof props.value === 'number') {\n return formatter.format(props.value)\n } else {\n return props.value ?? null\n }\n }, [props.value, formatter])\n return <>{formatted}</>\n}\n\nfunction ListValue(props: StringListProps) {\n const formatter = React.useMemo(\n () => new Intl.ListFormat('en-us', props.formatOptions ?? { style: 'short' }),\n [props.formatOptions]\n )\n const formatted = React.useMemo(() => {\n if (props.value && props.value.length) {\n return formatter.format(props.value)\n } else {\n return null\n }\n }, [props.value, formatter])\n return <>{formatted}</>\n}\n\nfunction DateValue(props: DateProps) {\n const format = props.formatOptions?.dateFormat ?? 'M/d/yyyy'\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${dateFns.format(props.value.start, format)} – ${dateFns.format(props.value.end, format)}`\n } else if (typeof props.value === 'string') {\n return dateFns.format(props.value, format)\n } else {\n return null\n }\n }, [props.value, format])\n return <>{formatted}</>\n}\n\nfunction TimeValue(props: TimeProps) {\n const format = props.formatOptions?.timeFormat ?? 'h:mm a'\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${timeFns.format(props.value.start, format)} – ${timeFns.format(props.value.end, format)}`\n } else if (typeof props.value === 'string') {\n return timeFns.format(props.value, format)\n } else {\n return null\n }\n }, [props.value, format])\n return <>{formatted}</>\n}\n\nfunction DateTimeValue(props: DateTimeProps) {\n const format = props.formatOptions?.dateTimeFormat ?? 'M/d/yyyy h:mm a'\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${dateTimeFns.format(props.value.start, format)} – ${dateTimeFns.format(props.value.end, format)}`\n } else if (typeof props.value === 'string') {\n return dateTimeFns.format(props.value, format)\n } else {\n return null\n }\n }, [props.value, format])\n return <>{formatted}</>\n}\n\nfunction InstantValue(props: InstantProps) {\n const timeZone = props.formatOptions?.timeZone\n const format = props.formatOptions?.dateTimeFormat ?? (timeZone ? 'M/d/yyyy h:mm a z' : 'M/d/yyyy h:mm a')\n\n const formatted = React.useMemo(() => {\n const formatInstant = (instant: Iso.Instant) => {\n if (timeZone) {\n const zonedDateTime = instantFns.toZonedDateTime(instant, timeZone)\n return zonedDateTimeFns.format(zonedDateTime, format)\n }\n return instantFns.chain(instant).toZonedDateTime('UTC').format(format).value()\n }\n\n if (props.value && typeof props.value === 'object') {\n return `${formatInstant(props.value.start)} – ${formatInstant(props.value.end)}`\n } else if (typeof props.value === 'string') {\n return formatInstant(props.value)\n } else {\n return null\n }\n }, [props.value, format, timeZone])\n return <>{formatted}</>\n}\n\nfunction ZonedDateTimeValue(props: ZonedDateTimeProps) {\n const format = props.formatOptions?.dateTimeFormat ?? 'M/d/yyyy h:mm a z'\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${zonedDateTimeFns.format(props.value.start, format)} – ${zonedDateTimeFns.format(props.value.end, format)}`\n } else if (typeof props.value === 'string') {\n return zonedDateTimeFns.format(props.value, format)\n } else {\n return null\n }\n }, [props.value, format])\n return <>{formatted}</>\n}\n\nfunction YearMonthValue(props: YearMonthProps) {\n const format = props.formatOptions?.dateFormat ?? 'MMMM yyyy'\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${yearMonthFns.format(props.value.start, format)} – ${yearMonthFns.format(props.value.end, format)}`\n } else if (typeof props.value === 'string') {\n return yearMonthFns.format(props.value, format)\n } else {\n return null\n }\n }, [props.value, format])\n return <>{formatted}</>\n}\n\nfunction MonthDayValue(props: MonthDayProps) {\n const format = props.formatOptions?.dateFormat ?? 'MMMM d'\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${monthDayFns.format(props.value.start, format)} – ${monthDayFns.format(props.value.end, format)}`\n } else if (typeof props.value === 'string') {\n return monthDayFns.format(props.value, format)\n } else {\n return null\n }\n }, [props.value, format])\n return <>{formatted}</>\n}\n\nfunction DurationValue(props: DurationProps) {\n const durationFormat = props.formatOptions?.style ?? 'short'\n\n const formatted = React.useMemo(() => {\n const formatDuration = (duration: Iso.Duration) => {\n const fields = durationFns.getFields(duration)\n\n const partFormatters: Record<string, Record<string, string>> = {\n years: {\n long: Math.abs(fields.years) > 1 ? `${fields.years} years` : `${fields.years} year`,\n short: `${fields.years}y`\n },\n months: {\n long: Math.abs(fields.months) > 1 ? `${fields.months} months` : `${fields.months} month`,\n short: `${fields.months}M`\n },\n weeks: {\n long: Math.abs(fields.weeks) > 1 ? `${fields.weeks} weeks` : `${fields.weeks} week`,\n short: `${fields.weeks}w`\n },\n days: {\n long: Math.abs(fields.days) > 1 ? `${fields.days} days` : `${fields.days} day`,\n short: `${fields.days}d`\n },\n hours: {\n long: Math.abs(fields.hours) > 1 ? `${fields.hours} hours` : `${fields.hours} hour`,\n short: `${fields.hours}h`\n },\n minutes: {\n long: Math.abs(fields.minutes) > 1 ? `${fields.minutes} minutes` : `${fields.minutes} minute`,\n short: `${fields.minutes}m`\n },\n seconds: {\n long: Math.abs(fields.seconds) > 1 ? `${fields.seconds} seconds` : `${fields.seconds} second`,\n short: `${fields.seconds}s`\n },\n milliseconds: {\n long:\n Math.abs(fields.milliseconds) > 1 ? `${fields.milliseconds} milliseconds` : `${fields.milliseconds} millisecond`,\n short: `${fields.milliseconds}ms`\n }\n }\n\n const parts = Object.entries(partFormatters)\n .map(([key, value]) => (fields[key as keyof typeof fields] !== 0 ? value[durationFormat] : null))\n .filter(Boolean) as string[]\n if (!parts.length) {\n return durationFormat === 'long' ? '0 seconds' : '0s'\n } else {\n return parts.join(' ')\n }\n }\n\n if (props.value && typeof props.value === 'object') {\n return `${formatDuration(props.value.start)} – ${formatDuration(props.value.end)}`\n } else if (typeof props.value === 'string') {\n return formatDuration(props.value)\n } else {\n return null\n }\n }, [props.value, durationFormat])\n return <>{formatted}</>\n}\n\nexport function LabeledValueLabel(props: React.ComponentProps<'label'>) {\n return (\n <label\n {...props}\n className={tw('block uppercase text-xs tracking-wider font-medium text-neutral-500', props.className)}\n />\n )\n}\n"}]},{"name":"inline-alert","dependencies":["tw","icon"],"files":[{"name":"components/inline-alert.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add inline-alert --directory app\n\nimport * as React from 'react'\nimport { tw } from '../utils/tw'\nimport { Icon } from './icon'\n\nexport type InlineAlertProps = {\n children?: React.ReactNode\n variant: 'info' | 'positive' | 'notice' | 'negative' | 'neutral'\n icon?: React.ReactNode\n className?: string\n title?: React.ReactNode\n description?: React.ReactNode\n actions?: React.ReactNode\n}\n\nconst variantClasses = {\n info: 'bg-info-50 text-info-800 border-info-200',\n positive: 'bg-positive-50 text-positive-800 border-positive-200',\n notice: 'bg-notice-50 text-notice-800 border-notice-200',\n negative: 'bg-negative-50 text-negative-800 border-negative-200',\n neutral: 'bg-neutral-50 text-neutral-700 border-neutral-200'\n}\n\nconst defaultIconNames = {\n info: 'information-circle',\n positive: 'check',\n notice: 'exclamation-triangle',\n negative: 'x-mark',\n neutral: 'information-circle'\n} as const\n\nexport function InlineAlert({ children, actions, variant, icon, className, title, description }: InlineAlertProps) {\n const displayIcon = icon !== undefined ? icon : <Icon name={defaultIconNames[variant]} className=\"h-5 w-5\" />\n\n return (\n <div\n className={tw(\n 'rounded-lg border px-4 py-3 flex gap-3 items-start justify-between text-left',\n variantClasses[variant],\n className\n )}\n role=\"alert\"\n >\n <div className=\"flex gap-3 items-start\">\n {displayIcon && <div className=\"flex-shrink-0\">{displayIcon}</div>}\n <div className=\"flex-1 gap-1\">\n {title && <div className={tw('font-semibold text-sm')}>{title}</div>}\n {description && <div className=\"text-sm\">{description}</div>}\n {children && <div className=\"text-sm\">{children}</div>}\n </div>\n </div>\n {actions}\n </div>\n )\n}\n"}]},{"name":"pdf","dependencies":["tw","pdf-dist","icon","use-pagination"],"files":[{"name":"components/pdf.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add pdf --directory app\n\nimport React from 'react'\nimport { usePdf } from 'react-pdf-hook'\nimport { tw } from '../utils/tw'\nimport { usePagination } from '../utils/use-pagination'\nimport { Icon } from './icon'\nimport pdfjs from './helpers/pdf-dist.client'\n\nexport interface PdfProps extends React.ComponentProps<'div'> {\n src: string\n onDownload?: () => void\n}\n\nexport function Pdf({ src, onDownload, className, ...props }: PdfProps) {\n const containerRef = React.useRef<HTMLDivElement>(null)\n const [pageNumber, setPageNumber] = React.useState(0)\n\n const { pdf, canvasRef } = usePdf(pdfjs, src, pageNumber + 1, { width: containerRef.current?.clientWidth })\n\n const numPages = pdf?.numPages\n\n return (\n <div ref={containerRef} className={tw('relative overflow-hidden border border-neutral-300', className)} {...props}>\n <canvas\n ref={canvasRef}\n style={{ display: !pdf ? 'none' : undefined, maxWidth: '100%' }}\n className={tw('pdf-page', onDownload && 'cursor-pointer')}\n onClick={onDownload}\n />\n {!pdf || !containerRef.current?.clientWidth ? <div className=\"w-full h-full m-auto rounded\" /> : null}\n {numPages && numPages > 1 ? (\n <div className=\"flex absolute bottom-5 w-full justify-center\">\n <Pagination page={pageNumber} setPageNumber={setPageNumber} count={numPages} />\n </div>\n ) : null}\n </div>\n )\n}\n\nPdf.displayName = 'Pdf'\n\nfunction Pagination({ page, count, setPageNumber }: { page: number; count: number; setPageNumber: (n: number) => void }) {\n const pages = usePagination({ currentPage: page, pageCount: count })\n\n return (\n <div className=\"inline-flex items-center gap-1 pointer-events-auto\">\n <button\n type=\"button\"\n onClick={() => setPageNumber(page > 0 ? page - 1 : 0)}\n disabled={page === 0}\n className=\"p-1 rounded bg-white border border-neutral-300 hover:bg-neutral-50 disabled:opacity-50 disabled:cursor-not-allowed\"\n aria-label=\"Previous page\"\n >\n <Icon name=\"chevron-left\" className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n\n {pages.map((p, i) => (\n <button\n type=\"button\"\n key={p.readonly ? `ellipsis-${i}` : p.label}\n onClick={() => setPageNumber(p.readonly ? page : (p.value as number))}\n disabled={p.readonly}\n className={tw(\n 'min-w-[28px] bg-white h-7 px-2 text-xs font-medium rounded border transition-colors',\n p.selected\n ? 'bg-primary-500 text-white border-primary-500'\n : p.readonly\n ? 'border-transparent cursor-default'\n : 'border-neutral-300 hover:bg-neutral-50'\n )}\n >\n {p.label}\n </button>\n ))}\n\n <button\n type=\"button\"\n onClick={() => setPageNumber(page < count - 1 ? page + 1 : page)}\n disabled={page === count - 1}\n className=\"p-1 rounded bg-white border border-neutral-300 hover:bg-neutral-50 disabled:opacity-50 disabled:cursor-not-allowed\"\n aria-label=\"Next page\"\n >\n <Icon name=\"chevron-right\" className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n )\n}\n"}]},{"name":"container","dependencies":["tw"],"files":[{"name":"components/container.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add container --directory app\n\nimport * as React from 'react'\nimport { tw } from '../utils/tw'\n\n// ── Props ────────────────────────────────────────────────────────────────────\n\nexport type ContainerSize = 'lg' | 'md' | 'sm'\nexport type ContainerElevation = 0 | -1 | -2 | 'inherit'\n\nexport type ContainerProps = React.ComponentProps<'div'> & {\n size?: ContainerSize\n elevation?: ContainerElevation\n}\n\nexport type ContainerSectionProps = ContainerProps & {\n label?: React.ReactNode\n actions?: React.ReactNode\n}\n\nexport type SectionProps = {\n label?: React.ReactNode\n actions?: React.ReactNode\n children?: React.ReactNode\n}\n\n// ── Container ────────────────────────────────────────────────────────────────\n\nexport function Container({ size = 'lg', elevation = 0, className, ...props }: ContainerProps) {\n return <div {...props} className={tw(elevationClasses[String(elevation)], sizeClasses[size], className)} />\n}\n\nContainer.displayName = 'Container'\n\n// ── ContainerSection ─────────────────────────────────────────────────────────\n\nexport function ContainerSection({ label, actions, ...props }: ContainerSectionProps) {\n return (\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex items-start justify-between\">\n <h3 className=\"text-lg font-semibold\">{label}</h3>\n {actions}\n </div>\n <Container {...props} />\n </div>\n )\n}\n\nContainerSection.displayName = 'ContainerSection'\n\n// ── Section ──────────────────────────────────────────────────────────────────\n\nexport function Section({ label, actions, children }: SectionProps) {\n return (\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex items-start justify-between\">\n <h3 className=\"text-lg font-semibold\">{label}</h3>\n {actions}\n </div>\n <div>{children}</div>\n </div>\n )\n}\n\nSection.displayName = 'Section'\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nconst sizeClasses: Record<ContainerSize, string> = {\n lg: 'p-6 rounded-lg',\n md: 'p-3 rounded',\n sm: 'p-2 rounded-sm'\n}\n\nconst elevationClasses: Record<string, string> = {\n '0': 'bg-white ring-1 ring-neutral-950/5',\n '-1': 'bg-neutral-50 ring-1 ring-neutral-950/5',\n '-2': 'bg-neutral-100 ring-1 ring-neutral-950/10',\n inherit: ''\n}\n\nexport function getSizeClassesForContainer(size: ContainerSize) {\n return sizeClasses[size]\n}\n"}]},{"name":"disclosure","dependencies":["tw","icon","container"],"files":[{"name":"components/disclosure.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add disclosure --directory app\n\nimport * as React from 'react'\nimport {\n Disclosure as AriaDisclosure,\n DisclosurePanel as AriaDisclosurePanel,\n DisclosureGroup,\n DisclosureStateContext,\n Button,\n Heading\n} from 'react-aria-components'\nimport { tw } from '../utils/tw'\nimport { type ContainerSize } from './container'\nimport { Icon } from './icon'\n\nexport type DisclosureProps = {\n id?: string | number\n title: React.ReactNode\n children: React.ReactNode\n className?: string\n size?: ContainerSize\n isDisabled?: boolean\n defaultOpen?: boolean\n isOpen?: boolean\n onOpenChange?: (isOpen: boolean) => void\n}\n\nfunction DisclosureRaw({\n id,\n title,\n className,\n defaultOpen,\n isOpen,\n onOpenChange,\n children,\n size = 'md',\n isDisabled = false\n}: DisclosureProps) {\n const paddingX = size === 'lg' ? 'px-6' : size === 'md' ? 'px-3' : 'px-2'\n const paddingBottom = size === 'lg' ? 'pb-6' : size === 'md' ? 'pb-3' : 'pb-2'\n\n return (\n <AriaDisclosure\n id={id}\n defaultExpanded={defaultOpen}\n isExpanded={isOpen}\n onExpandedChange={onOpenChange}\n isDisabled={isDisabled}\n >\n <DisclosureHeader size={size} isDisabled={isDisabled} className={className}>\n {title}\n </DisclosureHeader>\n <AriaDisclosurePanel className=\"h-(--disclosure-panel-height) motion-safe:transition-[height] duration-200 ease-in-out overflow-clip mt-1\">\n <div className={tw(paddingX, paddingBottom)}>{children}</div>\n </AriaDisclosurePanel>\n </AriaDisclosure>\n )\n}\n\nfunction DisclosureHeader({\n size,\n isDisabled,\n className,\n children\n}: {\n size: ContainerSize\n isDisabled: boolean\n className?: string\n children: React.ReactNode\n}) {\n const { isExpanded } = React.useContext(DisclosureStateContext)!\n\n return (\n <Heading>\n <Button\n slot=\"trigger\"\n className={tw(\n 'flex w-full items-center justify-start text-left transition-colors gap-2 font-medium',\n 'hover:bg-neutral-50',\n size === 'lg' ? 'px-4 py-2.5' : size === 'md' ? 'px-2.5 py-1.5' : 'px-2 py-1',\n 'rounded-lg',\n isDisabled && 'opacity-50 cursor-not-allowed hover:bg-transparent',\n className\n )}\n >\n <Icon\n name=\"chevron-right\"\n className={tw(\n 'shrink-0 transition-transform duration-200 ease-in-out',\n size === 'md' || size === 'lg' ? 'size-4' : 'size-3',\n isExpanded && 'rotate-90',\n isDisabled ? 'text-neutral-300' : 'text-neutral-500'\n )}\n />\n <span\n className={tw(\n size === 'lg' ? 'text-lg font-semibold' : size === 'md' ? 'text-base font-medium' : 'text-sm font-medium',\n isDisabled ? 'text-neutral-400' : 'text-neutral-700'\n )}\n >\n {children}\n </span>\n </Button>\n </Heading>\n )\n}\n\nexport const Disclosure = Object.assign(DisclosureRaw, {\n Group: DisclosureGroup\n})\n\nObject.defineProperty(DisclosureRaw, 'name', { value: 'Disclosure' })\n"}]},{"name":"use-pagination","files":[{"name":"utils/use-pagination.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-pagination --directory app\n\nimport * as React from 'react'\n\nexport interface PaginationPage {\n label: string\n value: number | null\n readonly: boolean\n selected: boolean\n}\n\nexport function usePagination({ currentPage, pageCount }: { currentPage: number; pageCount: number }): PaginationPage[] {\n return React.useMemo(() => {\n function getPage(i: number): PaginationPage {\n return { label: `${i + 1}`, value: i, readonly: false, selected: currentPage === i }\n }\n\n const first: PaginationPage = { label: '1', value: 0, readonly: false, selected: currentPage === 0 }\n const ellipsis: PaginationPage = { label: '...', value: null, readonly: true, selected: false }\n const last: PaginationPage = {\n label: `${pageCount}`,\n value: pageCount - 1,\n readonly: false,\n selected: currentPage === pageCount - 1\n }\n\n if (pageCount < 8) {\n return Array.from({ length: pageCount }, (_, i) => getPage(i))\n } else if (currentPage < 5) {\n const leading = Array.from({ length: 5 }, (_, i) => getPage(i))\n return [...leading, ellipsis, last]\n } else if (currentPage > pageCount - 5) {\n const trailing = Array.from({ length: 5 }, (_, i) => getPage(i + pageCount - 4))\n return [first, ellipsis, ...trailing.filter((p) => p.value! <= pageCount - 1)]\n } else {\n const siblings = Array.from({ length: 3 }, (_, i) => getPage(i + currentPage - 1))\n return [first, ellipsis, ...siblings, ellipsis, last]\n }\n }, [pageCount, currentPage])\n}\n"}]},{"name":"form","files":[{"name":"components/form.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add form --directory app\n\nimport { forwardRef } from 'react'\nimport {\n type FormProps as ReactRouterFormProps,\n Form as ReactRouterForm,\n data,\n type ClientActionFunctionArgs\n} from 'react-router'\nimport { HeadlessForm, type StandardSchema, type ParseFormDataOptions, type ParseFormDataResult } from '@maestro-js/form'\nimport { serverOnly$ } from 'vite-env-only/macros'\n\nconst _Form = forwardRef<HTMLFormElement, ReactRouterFormProps>(function Form({ children, ...props }, ref) {\n return (\n <HeadlessForm as={ReactRouterForm} ref={ref} {...props}>\n {children}\n </HeadlessForm>\n )\n})\n\nexport const Form = Object.assign(_Form, {\n HiddenInput: HeadlessForm.HiddenInput,\n HiddenFileInput: HeadlessForm.HiddenFileInput,\n HiddenSelect: HeadlessForm.HiddenSelect,\n useControlledState: HeadlessForm.useControlledState,\n useTopLevelFormValidation: HeadlessForm.useTopLevelFormValidation,\n useReset: HeadlessForm.useReset,\n clientSafeParse: HeadlessForm.parseFormDataClientSide,\n serverSafeParse<T>(\n schema: StandardSchema<T, T>,\n formData: FormData | Promise<FormData>,\n options?: {\n handleFile: ParseFormDataOptions<T>['handleFile']\n }\n ): Promise<ParseFormDataResult<T>> {\n return HeadlessForm.parseFormData(schema, formData, {\n ...options,\n jsonSchemaLibraryOptions: {\n unrepresentable: 'any',\n override: (ctx) => {\n const meta = ctx.zodSchema.meta()\n const custom = meta?.jsonSchema\n\n if (custom && typeof custom === 'object') {\n Object.assign(ctx.jsonSchema, custom)\n }\n }\n }\n })\n },\n validationError: serverOnly$(validationError) as typeof validationError,\n customValidationError: serverOnly$(customValidationError) as typeof customValidationError,\n composeValidators: HeadlessForm.composeValidators,\n FormError\n})\n\nasync function validationError(\n {\n issues,\n formData,\n formId\n }: {\n issues: { path?: ((string | number | symbol) | { key: string | number | symbol })[] | undefined; message: string }[]\n formData: FormData\n formId?: string | null\n },\n init: RequestInit = {}\n) {\n console.error(issues)\n return data(\n {\n success: false as const,\n formSubmissionError: {\n issues: issues.map((i) => ({\n path: i.path?.map((p) => (p && typeof p === 'object' && 'key' in p ? p.key : p))?.join('.') ?? '',\n message: i.message\n })),\n formId: formId ?? formData.get('__formId')\n }\n },\n { status: 422, ...init }\n )\n}\n\nasync function customValidationError(\n { message, formData }: { message: string; formData: FormData },\n init?: number | ResponseInit\n) {\n console.error(message)\n return data(\n {\n success: false as const,\n formSubmissionError: {\n issues: [{ path: '', message: message }],\n formId: formData.get('__formId')\n }\n },\n { status: typeof init === 'number' ? init : 422, ...(typeof init === 'object' ? init : {}) }\n )\n}\n\nfunction FormError({ formId, children }: { formId?: string; children(message: string): React.ReactNode }) {\n const serverErrors = HeadlessForm.useServerErrors(formId)\n const formErrors = serverErrors.filter((s) => !s.path)\n return formErrors.length ? <>{children(formErrors[0]!.message)}</> : null\n}\n"}]},{"name":"toast","dependencies":["tw","headless-button","compose-refs","use-render-props","icon"],"files":[{"name":"components/toast.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add toast --directory app\n\nimport * as React from 'react'\nimport { flushSync } from 'react-dom'\nimport {\n Text,\n UNSTABLE_Toast as AriaToast,\n UNSTABLE_ToastContent as ToastContent,\n UNSTABLE_ToastQueue as ToastQueue,\n UNSTABLE_ToastRegion as AriaToastRegion\n} from 'react-aria-components'\nimport { tw } from '../utils/tw'\nimport { Icon } from './icon'\nimport { HeadlessButton } from './helpers/headless-button'\n\nexport interface Toast {\n title: string\n description?: string\n variant?: 'info' | 'positive' | 'warning' | 'negative' | 'notice'\n}\n\nconst queue = new ToastQueue<Toast>({\n wrapUpdate(fn) {\n if ('startViewTransition' in document) {\n document.startViewTransition(() => {\n flushSync(fn)\n })\n } else {\n fn()\n }\n }\n})\n\nexport function showToast(\n toast: Toast,\n options?: {\n /** Handler that is called when the toast is closed, either by the user or after a timeout. */\n onClose?: () => void\n /** A timeout to automatically close the toast after, in milliseconds. */\n timeout?: number | null\n }\n) {\n return queue.add(toast, {\n ...options,\n timeout: typeof options?.timeout === 'undefined' ? 5_000 : (options?.timeout ?? undefined)\n })\n}\n\nexport function ToastRegion({ toast }: { toast?: Toast | null }) {\n return (\n <>\n <style>\n {`\n::view-transition-new(.toast):only-child {\n animation: maestro-toast-slide-in 400ms;\n}\n\n::view-transition-old(.toast):only-child {\n animation: maestro-toast-slide-out 400ms;\n animation-fill-mode: forwards;\n}\n\n@keyframes maestro-toast-slide-out {\n to {\n translate: 0 100%;\n opacity: 0;\n visibility: hidden;\n }\n}\n\n@keyframes maestro-toast-slide-in {\n from {\n translate: 0 100%;\n opacity: 0;\n }\n}\n `}\n </style>\n <AriaToastRegion\n className=\"fixed right-4 bottom-4 sm:right-6 sm:bottom-6 z-[6000] flex flex-col gap-2 pointer-events-none outline-none\"\n queue={queue}\n >\n {({ toast }) => <ToastItem toast={toast} />}\n </AriaToastRegion>\n {toast ? <DeclaredToast toast={toast} /> : null}\n </>\n )\n}\n\nToastRegion.displayName = 'ToastRegion'\n\n// --- Internal Components ---\n\nfunction ToastItem({ toast }: { toast: { content: Toast; key: string } }) {\n const variant = toast.content.variant || 'info'\n const hasDescription = !!toast.content.description\n\n return (\n <AriaToast\n toast={toast}\n style={{ viewTransitionName: toast.key } as React.CSSProperties}\n className={tw(\n 'pointer-events-auto',\n 'flex gap-3 rounded-lg px-4 py-3 shadow-lg',\n 'min-w-[300px] max-w-md',\n '[view-transition-class:toast]',\n hasDescription ? 'items-start' : 'items-center',\n variantClasses[variant]\n )}\n >\n <Icon name={variantIconNames[variant]} className={tw('h-5 w-5 shrink-0', hasDescription && 'mt-0.5')} />\n <ToastContent className=\"flex flex-1 flex-col min-w-0\">\n <Text slot=\"title\" className=\"font-semibold text-sm\">\n {toast.content.title}\n </Text>\n {toast.content.description && (\n <Text slot=\"description\" className=\"mt-1 text-sm\">\n {toast.content.description}\n </Text>\n )}\n </ToastContent>\n <HeadlessButton\n onClick={() => queue.close(toast.key)}\n className={tw(\n 'flex-shrink-0 rounded p-1 transition-colors',\n 'hover:bg-black/5 focus:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',\n variantCloseOutlineClasses[variant]\n )}\n >\n <Icon name=\"x-mark\" className=\"h-5 w-5\" />\n </HeadlessButton>\n </AriaToast>\n )\n}\n\nfunction DeclaredToast({\n toast,\n options\n}: {\n toast: Toast\n options?: {\n onClose?: () => void\n timeout?: number | null\n }\n}) {\n React.useEffect(() => {\n setTimeout(() => {\n showToast(toast, options)\n }, 0)\n }, [toast, options])\n return null\n}\n\n// --- Variant Maps ---\n\nconst variantClasses = {\n info: 'bg-info-50 text-info-800 ring-1 ring-info-200',\n positive: 'bg-positive-50 text-positive-800 ring-1 ring-positive-200',\n warning: 'bg-notice-50 text-notice-800 ring-1 ring-notice-200',\n negative: 'bg-negative-50 text-negative-800 ring-1 ring-negative-200',\n notice: 'bg-neutral-50 text-neutral-700 ring-1 ring-neutral-200'\n}\n\nconst variantIconNames = {\n info: 'information-circle',\n positive: 'check',\n warning: 'exclamation-triangle',\n negative: 'x-mark',\n notice: 'information-circle'\n} as const\n\nconst variantCloseOutlineClasses = {\n info: 'focus-visible:outline-info-500',\n positive: 'focus-visible:outline-positive-600',\n warning: 'focus-visible:outline-notice-600',\n negative: 'focus-visible:outline-negative-600',\n notice: 'focus-visible:outline-neutral-400'\n}\n"}]},{"name":"table","files":[{"name":"components/table/index.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { createServerTable } from './server-table'\nimport { TableContainer } from './table-container'\nimport { TableSelectFilter } from './table-filters/select-filter'\nimport { TableSelectSort } from './table-filters/sort'\nimport {\n TableBody,\n TableCell,\n TableDescription,\n TableFooter,\n TableMain,\n TableRow,\n TableTitle,\n TableTitleLink\n} from './table-primitives'\nimport { TableActions, TableHeader, TableSearch } from './table-toolbar'\nimport { TableActionButton, TableMenu, TableSelectionActions, TableSelectionAction } from './table-actions'\nimport { useClientTable } from './use-client-table'\nimport { useServerTable } from './use-server-table'\n\nexport const Table = Object.assign(TableContainer, {\n useClientTable,\n useServerTable,\n createServerTable,\n Main: TableMain,\n Search: TableSearch,\n Body: TableBody,\n Row: TableRow,\n Cell: TableCell,\n Header: TableHeader,\n Footer: TableFooter,\n Title: TableTitle,\n TitleLink: TableTitleLink,\n Description: TableDescription,\n SelectFilter: TableSelectFilter,\n SelectSort: TableSelectSort,\n ActionButton: TableActionButton,\n Menu: TableMenu,\n SelectionActions: TableSelectionActions,\n SelectionAction: TableSelectionAction,\n Actions: TableActions\n})\n"},{"name":"components/table/table-types.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport type { ReactNode } from 'react'\nimport type { Iso } from 'iso-fns'\nimport type { DateRangeRootAdditionalProps } from '../date-range'\n\ntype PaginationState = { limit: number; skip: number }\ntype SetterOrUpdater<T> = T | ((prev: T) => T)\n\nexport type StandardTable<\n FC extends Record<string, any> = Record<string, any>,\n SC extends Record<string, any> = Record<string, any>\n> = {\n getRows(): Record<string, any>[]\n getRowCount(): number\n getFilterConfig(): FC\n getSortConfig(): { [K in keyof SC]: null }\n state: {\n filters: { [K in keyof FC]: FC[K] | null | undefined }\n sort: (keyof SC & string) | null\n search: string\n pagination: PaginationState\n rowSelection?: Set<string>\n }\n setFilters(val: SetterOrUpdater<{ [K in keyof FC]: FC[K] | null | undefined }>): void\n setSort(val: any): void\n setSearch(val: SetterOrUpdater<string>): void\n setPagination(val: SetterOrUpdater<PaginationState>): void\n toggleAllRowSelection?(): void\n toggleRowSelection?(row: Record<string, any>): void\n isRowSelected?(row: Record<string, any>): boolean\n clearRowSelection?(): void\n}\n\ntype DefaultTableFilter<V = any> = {\n label: string\n type?: never\n options: { label: string; value: V; className?: string; icon?: ReactNode }[]\n autoComplete?: boolean\n}\n\ntype DateRangeTableFilter = {\n label: string\n type: 'date-range'\n options?: Pick<DateRangeRootAdditionalProps, 'shouldDisableDate' | 'shouldDisableEndDate' | 'shouldDisableStartDate'> & {\n quickPresets?: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]\n }\n}\n\nexport type TableFilterLocalization<V = any> = 0 extends 1 & V\n ? DefaultTableFilter | DateRangeTableFilter\n : [V] extends [{ startDate: Iso.Date; endDate: Iso.Date }]\n ? DateRangeTableFilter\n : DefaultTableFilter<V>\n\nexport type TableLocalization<\n FC extends Record<string, any> = Record<string, any>,\n SC extends Record<string, any> = Record<string, any>\n> = {\n filters: { [K in keyof FC]: TableFilterLocalization<FC[K]> }\n sorts: { [key in keyof SC]: { label: string } }\n searchPlaceholder?: string\n}\n"},{"name":"components/table/table-primitives.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { NavLink, type NavLinkProps } from 'react-router'\nimport { twMerge } from 'tailwind-merge'\nimport { Icon } from '../icon'\nimport React from 'react'\nimport { PlainCheckbox } from '../checkbox'\nimport type { StandardTable } from './table-types'\n\n// --- Layout ---\n\nexport function TableMain({\n className,\n ...props\n}: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {\n return <div className={twMerge('max-w-full h-auto', className)} {...props} />\n}\n\nexport function TableBody({\n className,\n ...props\n}: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {\n return (\n <div className=\"w-full\">\n <div\n className={twMerge(\n 'flex flex-col flex-nowrap w-full',\n '[&>*+*>*]:border-t [&>*+*>*]:border-gray-950/5' /** select table cells for all but the first row */,\n className\n )}\n {...props}\n />\n </div>\n )\n}\n\nexport function TableCell({\n className,\n ...props\n}: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {\n return <div className={twMerge('basis-0 grow min-w-0 py-3.5 px-2', className)} {...props} />\n}\n\n// --- Row ---\n\nconst TableRowContext = React.createContext<{ table: StandardTable; multiSelect?: boolean }>(null as any)\nexport const TableRowContextProvider = TableRowContext.Provider\n\ninterface TableRowProps {\n row: Record<string, any>\n children?: React.ReactNode\n className?: string\n hideMobileSpacer?: boolean\n ['aria-label']?: string\n}\n\nexport function TableRow({ row, children, className, hideMobileSpacer, 'aria-label': ariaLabel }: TableRowProps) {\n const { table, multiSelect } = React.useContext(TableRowContext)\n const selected = table.isRowSelected?.(row) ?? false\n\n return (\n <div\n className={twMerge('flex flex-row w-full h-auto', selected && 'bg-gray-50', className)}\n onClick={(e) => {\n const isTouchableDevice = navigator.maxTouchPoints > 0\n const isSmallScreen = !window.matchMedia('(min-width: 640px)').matches\n\n const target = e.target as HTMLElement\n const isInteractiveElement = target.closest('input, label, button, a')\n\n if ((isTouchableDevice || isSmallScreen) && !isInteractiveElement) {\n const title = e.currentTarget.querySelector('[data-row-title*=\"true\"]')\n const titleLink = title instanceof HTMLAnchorElement ? title : title?.querySelector('a')\n titleLink?.click()\n }\n }}\n role=\"row\"\n aria-label={ariaLabel}\n >\n {multiSelect ? (\n <div className=\"relative shrink-0 hidden md:flex items-start pt-3.5 pl-4 pr-2 *:scale-90\">\n {selected ? <div className=\"absolute inset-y-0 left-0 w-0.5 bg-primary-600\" /> : null}\n <PlainCheckbox checked={selected} onChange={() => table.toggleRowSelection?.(row)} />\n </div>\n ) : null}\n {!hideMobileSpacer ? <TableCell className={twMerge('relative grow-0 w-3', multiSelect && 'md:hidden')} /> : null}\n\n {children}\n </div>\n )\n}\n\n// --- Title / Description ---\n\nexport interface TableTitleProps extends React.DetailedHTMLProps<\n React.HTMLAttributes<HTMLParagraphElement>,\n HTMLParagraphElement\n> {}\n\nexport function TableTitle({ className, ...props }: TableTitleProps) {\n return <p {...props} data-row-title=\"true\" className={twMerge('truncate text-sm font-medium text-blue-600', className)} />\n}\n\nexport function TableTitleLink({ className, ...props }: NavLinkProps) {\n return (\n <NavLink\n {...props}\n data-row-title=\"true\"\n className={(b) =>\n twMerge('truncate text-sm font-medium text-blue-600', typeof className === 'function' ? className(b) : className)\n }\n />\n )\n}\n\nexport interface TableDescriptionProps extends React.DetailedHTMLProps<\n React.HTMLAttributes<HTMLDivElement>,\n HTMLDivElement\n> {}\n\nexport function TableDescription({ className, ...props }: TableDescriptionProps) {\n return <div {...props} className={twMerge('truncate text-sm text-gray-500', className)} />\n}\n\n// --- Footer ---\n\nexport interface TableFooterProps {\n table: StandardTable\n hideResultCount?: boolean\n}\n\nexport function TableFooter({ table, hideResultCount }: TableFooterProps) {\n const { limit, skip } = table.state.pagination\n const rowCount = table.getRowCount()\n\n const pageCount = Math.ceil(limit ? rowCount / limit : 1)\n const pageNumber = limit > 0 ? Math.floor(skip / limit) : 0\n\n const goToPage = (page: number) => {\n table.setPagination({ limit, skip: page * (limit || 25) })\n }\n\n const canGoPrevious = pageNumber > 0\n const canGoNext = pageNumber < pageCount - 1\n\n const showingFrom = skip + 1\n const showingTo = limit ? Math.min(skip + limit, rowCount) : rowCount\n const pages = getPaginationRange(pageCount, pageNumber)\n\n return (\n <nav className=\"flex items-center justify-between border-t border-gray-950/5 px-4 py-2\">\n {!hideResultCount ? (\n <p className=\"text-xs text-gray-400 tabular-nums\">\n {showingFrom}–{showingTo} of {rowCount}\n </p>\n ) : (\n <div />\n )}\n {pageCount > 1 && (\n <div className=\"flex items-center gap-0.5\">\n <button\n type=\"button\"\n onClick={() => canGoPrevious && goToPage(pageNumber - 1)}\n disabled={!canGoPrevious}\n className=\"inline-flex items-center justify-center rounded-md p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 disabled:opacity-30 disabled:hover:bg-transparent\"\n aria-label=\"Previous page\"\n >\n <Icon name=\"chevron-left\" className=\"size-3.5\" />\n </button>\n <div className=\"flex items-center gap-0.5 max-sm:hidden\">\n {pages.map((page, i) => {\n if (page === '...') {\n return (\n <span key={`ellipsis-${i}`} className=\"px-1 text-xs text-gray-400\">\n ...\n </span>\n )\n }\n const isSelected = page === pageNumber\n return (\n <button\n type=\"button\"\n key={page}\n onClick={() => goToPage(page)}\n className={twMerge(\n 'inline-flex items-center justify-center size-7 rounded-full text-xs tabular-nums',\n isSelected\n ? 'bg-gray-100 text-gray-900 font-medium'\n : 'text-gray-400 hover:bg-gray-50 hover:text-gray-600'\n )}\n >\n {page + 1}\n </button>\n )\n })}\n </div>\n <span className=\"text-xs tabular-nums text-gray-400 sm:hidden px-1\">\n {pageNumber + 1}/{pageCount}\n </span>\n <button\n type=\"button\"\n onClick={() => canGoNext && goToPage(pageNumber + 1)}\n disabled={!canGoNext}\n className=\"inline-flex items-center justify-center rounded-md p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 disabled:opacity-30 disabled:hover:bg-transparent\"\n aria-label=\"Next page\"\n >\n <Icon name=\"chevron-right\" className=\"size-3.5\" />\n </button>\n </div>\n )}\n </nav>\n )\n}\n\nfunction getPaginationRange(pageCount: number, currentPage: number): (number | '...')[] {\n const delta = 1\n const range: (number | '...')[] = []\n\n for (let i = 0; i < pageCount; i++) {\n if (i === 0 || i === pageCount - 1 || (i >= currentPage - delta && i <= currentPage + delta)) {\n range.push(i)\n } else if (range[range.length - 1] !== '...') {\n range.push('...')\n }\n }\n\n return range\n}\n"},{"name":"components/table/table-actions.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport React from 'react'\nimport { twMerge } from 'tailwind-merge'\nimport { Button, type ButtonProps } from '../button'\nimport { Menu, type MenuProps, type MenuItemProps, type MenuLinkItemProps } from '../menu'\nimport { Icon } from '../icon'\n\n// ── TableActionButton ────────────────────────────────────────────────────────\n// Primary action button for the table toolbar (e.g. \"Add person\").\n// Defaults to contained/primary/sm — pass variant/color/size to override.\n\nexport type TableActionButtonProps = ButtonProps\n\nexport function TableActionButton({ size = 'md', children, ...props }: TableActionButtonProps) {\n return (\n <Button size={size} {...props}>\n {children}\n </Button>\n )\n}\n\n// ── TableMenu ────────────────────────────────────────────────────────────────\n// Secondary actions dropdown for the table toolbar (e.g. \"More\").\n// Renders a neutral outlined trigger with a chevron, and a standard Menu popover.\n\nexport type TableMenuProps = {\n label?: string\n children?: React.ReactNode\n placement?: MenuProps['placement']\n}\n\nfunction TableMenuMain({ label = 'More', children, placement }: TableMenuProps) {\n return (\n <Menu\n Button={\n <Button variant=\"outlined\" color=\"neutral\" size=\"md\">\n {label}\n <Icon name=\"chevron-down\" className=\"size-4 text-neutral-400\" />\n </Button>\n }\n placement={placement}\n >\n {children}\n </Menu>\n )\n}\n\nexport const TableMenu = Object.assign(TableMenuMain, {\n Item: Menu.Item,\n LinkItem: Menu.LinkItem,\n Group: Menu.Group,\n Separator: Menu.Separator\n})\n\n// ── TableSelectionAction ────────────────────────────────────────────────────\n// Compact action button for the multi-select selection bar context.\n\nexport function TableSelectionActions({ children }: { children: React.ReactNode }) {\n return <div className=\"flex items-center gap-1\">{children}</div>\n}\n\nexport type TableSelectionActionProps = React.ComponentProps<'button'>\n\nexport function TableSelectionAction({ children, className, ...props }: TableSelectionActionProps) {\n return (\n <button\n type=\"button\"\n {...props}\n className={twMerge(\n 'inline-flex items-center rounded px-2 py-0.5 text-sm font-medium text-primary-700 hover:text-primary-900 hover:bg-primary-100 disabled:opacity-50 disabled:cursor-not-allowed',\n className\n )}\n >\n {children}\n </button>\n )\n}\n"},{"name":"components/table/table-container.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport {\n TableBody,\n TableCell,\n TableDescription,\n TableFooter,\n TableMain,\n TableRow,\n TableRowContextProvider,\n TableTitle,\n TableTitleLink\n} from './table-primitives'\nimport type { StandardTable } from './table-types'\nimport { TableHeader, type TableHeaderProps, TableSearch } from './table-toolbar'\nimport { TableDateRangeFilter } from './table-filters/date-range-filter'\nimport { TableSelectSort } from './table-filters/sort'\nimport { TableSelectFilter } from './table-filters/select-filter'\nimport type { TableFilterLocalization, TableLocalization } from './table-types'\nimport { twMerge } from 'tailwind-merge'\nimport React from 'react'\nimport type { Iso } from 'iso-fns'\n\nexport function TableContainer<TTable extends StandardTable>({\n table,\n localization,\n children,\n actions,\n noRowElement = <StandardNoRows />,\n multiSelect,\n multiSelectActions,\n hideActionsOnMobile = true,\n stickyHeader = true,\n hideSort,\n hideSearch,\n tableBodyClassName,\n headerAction,\n hideResultCount,\n totalRows: totalRowsProp,\n ...props\n}: React.ComponentProps<'div'> & {\n table: TTable\n localization: TableLocalization<ReturnType<TTable['getFilterConfig']>, ReturnType<TTable['getSortConfig']>>\n actions?: React.ReactNode\n noRowElement?: React.ReactNode\n multiSelect?: boolean\n hideActionsOnMobile?: boolean\n hideSort?: boolean\n hideSearch?: boolean\n stickyHeader?: boolean\n multiSelectActions?: React.ReactNode\n tableBodyClassName?: string\n headerAction?: TableHeaderProps['action']\n hideResultCount?: boolean\n totalRows?: number\n}) {\n const rows = table.getRows()\n const filterConfig = table.getFilterConfig()\n const sortConfig = table.getSortConfig()\n const filterKeys = Object.keys(filterConfig).filter((k) => k in localization.filters)\n const sortKeys = Object.keys(sortConfig).filter((k) => k !== 'mostRelevant' && k in localization.sorts)\n\n const hasFilters = React.useMemo(() => {\n const filters = table.state.filters ?? {}\n const hasActiveFilters = Object.entries(filters).some(([, value]) => value != null)\n const hasSort = table.state.sort != null\n const hasSearch = (table.state.search ?? '') !== ''\n return hasActiveFilters || hasSort || hasSearch\n }, [table.state.filters, table.state.sort, table.state.search])\n\n return (\n <div {...props}>\n {/* Search + actions outside the card */}\n <TableSearch\n table={table}\n placeholder={localization.searchPlaceholder}\n actions={actions}\n hasFilters={hasFilters}\n hideSearch={hideSearch}\n hideActionsOnMobile={hideActionsOnMobile}\n />\n\n {/* Bordered card container */}\n <div className=\"rounded-lg border border-gray-950/10 overflow-hidden\">\n <TableMain className={twMerge(stickyHeader && 'relative')}>\n <TableHeader\n table={table}\n multiSelect={multiSelect}\n multiSelectActions={multiSelectActions}\n sticky={stickyHeader}\n hide={!!(hideSort && filterKeys.length === 0 && !multiSelect)}\n action={headerAction}\n >\n <div className={twMerge('flex flex-nowrap items-center gap-x-1', hideSort && 'mr-1')}>\n {filterKeys.map((f) => {\n const filter = localization.filters[f] as TableFilterLocalization\n if (filter.type === 'date-range') {\n return (\n <TableDateRangeFilter\n table={table}\n key={f}\n name={f}\n label={filter.label}\n options={filter.options}\n value={table.state.filters[f] as { startDate: Iso.Date; endDate: Iso.Date }}\n />\n )\n } else {\n return (\n <TableSelectFilter\n table={table}\n label={filter.label}\n name={f}\n options={filter.options}\n autoComplete={filter.autoComplete}\n value={table.state.filters[f]}\n key={f}\n />\n )\n }\n })}\n </div>\n <div className={twMerge('flex flex-nowrap items-center ml-1 mr-1', hideSort && 'hidden')}>\n <TableSelectSort\n table={table}\n value={table.state.sort}\n options={sortKeys.map((s) => ({ value: s, label: localization.sorts[s]?.label ?? s }))}\n search={table.state.search}\n />\n </div>\n </TableHeader>\n <TableRowContextProvider value={{ table, multiSelect }}>\n {rows.length ? <TableBody className={tableBodyClassName}>{children}</TableBody> : noRowElement}\n </TableRowContextProvider>\n {rows.length ? <TableFooter table={table} hideResultCount={hideResultCount} /> : null}\n </TableMain>\n </div>\n </div>\n )\n}\n\nexport function BasicTable({\n table,\n noRowElement = <StandardNoRows />,\n children,\n shouldHideFooter,\n ...props\n}: React.ComponentProps<'div'> & {\n table: StandardTable\n noRowElement?: React.ReactNode\n shouldHideFooter?: boolean\n}) {\n const rows = table.getRows()\n return (\n <div {...props}>\n <div className=\"rounded-lg border border-gray-950/10 overflow-hidden\">\n <TableMain>\n <TableRowContextProvider value={{ table }}>\n {rows.length ? <TableBody>{children}</TableBody> : noRowElement}\n </TableRowContextProvider>\n {rows.length > 0 && !shouldHideFooter ? <TableFooter table={table} /> : null}\n </TableMain>\n </div>\n </div>\n )\n}\n\nfunction StandardNoRows() {\n return (\n <div className=\"text-center py-8\">\n <h3 className=\"mt-2 text-sm font-semibold text-gray-900\">No results</h3>\n <p className=\"mt-1 text-sm text-gray-500\">No results were found matching your query.</p>\n </div>\n )\n}\n"},{"name":"components/table/table-toolbar.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { twMerge } from 'tailwind-merge'\nimport { type To, Link, useNavigate } from 'react-router'\nimport { Icon } from '../icon'\nimport { OptionalLink, type OptionalLinkProps } from '../optional-link'\nimport { usePress } from '@react-aria/interactions'\nimport React from 'react'\nimport { mergeProps } from '@react-aria/utils'\nimport { HeadlessButton } from '../helpers/headless-button'\nimport { PlainCheckbox } from '../checkbox'\nimport { Button, Menu, MenuItem, MenuTrigger, Popover } from 'react-aria-components'\nimport type { StandardTable } from './table-types'\n\n// --- Header ---\n\nexport interface TableHeaderProps {\n table: StandardTable\n multiSelect?: boolean\n multiSelectActions?: React.ReactNode\n children: React.ReactNode\n sticky?: boolean\n hide?: boolean\n action?: React.ReactNode\n}\n\nexport function TableHeader({ table, multiSelect, multiSelectActions, children, sticky, hide, action }: TableHeaderProps) {\n const rowCount = table.getRows().length\n const selectionSize = table.state.rowSelection?.size ?? 0\n const hasSelection = multiSelect && selectionSize > 0\n\n if (hide) return null\n\n return (\n <div\n className={twMerge(\n 'bg-gray-100 border-b border-gray-950/10 py-0.5',\n sticky && 'sticky z-20 md:z-[2] top-0',\n hasSelection && 'bg-primary-50 border-primary-200'\n )}\n >\n <div className=\"flex items-center gap-2 pl-4 pr-1.5 min-h-9\">\n {action}\n {multiSelect ? (\n <div className=\"flex items-center *:scale-90\">\n <PlainCheckbox\n checked={rowCount > 0 ? selectionSize === rowCount : false}\n onChange={() => table.toggleAllRowSelection?.()}\n isDisabled={rowCount === 0}\n />\n </div>\n ) : null}\n {hasSelection ? (\n <>\n <span className=\"text-sm text-primary-700 font-medium\">{selectionSize} selected</span>\n <button\n type=\"button\"\n onClick={() => table.clearRowSelection?.()}\n className=\"text-sm text-gray-500 hover:text-gray-700 flex items-center gap-1\"\n >\n <Icon name=\"x-mark\" className=\"size-4\" />\n <span className=\"max-sm:hidden\">Clear</span>\n </button>\n <div className=\"grow\" />\n {multiSelectActions}\n </>\n ) : (\n <>\n <div className=\"grow\" />\n <div className=\"flex overflow-x-scroll scrollbar-hide md:overflow-x-visible h-full gap-2\">{children}</div>\n </>\n )}\n </div>\n </div>\n )\n}\n\n// --- Search ---\n\ninterface TableSearchProps {\n table: StandardTable\n actions?: React.ReactNode\n placeholder?: string | null\n hideActionsOnMobile?: boolean\n hasFilters?: boolean\n hideSearch?: boolean\n}\n\nexport function TableSearch({ table, actions, placeholder, hideActionsOnMobile, hasFilters, hideSearch }: TableSearchProps) {\n const [inputValue, setInputValue] = React.useState(table.state.search ?? '')\n\n React.useEffect(() => {\n setInputValue(table.state.search ?? '')\n }, [table.state.search])\n\n const commitSearch = React.useCallback(\n (value: string) => {\n table.setSearch(value)\n },\n [table]\n )\n\n const clearAll = () => {\n table.setSearch('')\n }\n\n return (\n <div className=\"w-full mb-3\">\n <div\n className={twMerge(\n 'flex',\n hideActionsOnMobile ? 'md:flex-row-reverse md:gap-x-2 flex-wrap' : 'flex-row-reverse gap-x-2'\n )}\n >\n <div className={twMerge(hideActionsOnMobile && 'basis-full md:basis-auto')}>{actions}</div>\n {hideSearch ? null : (\n <form\n className=\"flex grow\"\n onSubmit={(e) => {\n e.preventDefault()\n commitSearch(inputValue)\n }}\n >\n <div className=\"grow relative max-w-2xl\" role=\"search\">\n <div className=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3\">\n <Icon name=\"magnifying-glass\" className=\"size-4 text-gray-400\" aria-hidden=\"true\" />\n </div>\n <input\n className={twMerge(\n 'w-full max-w-2xl pl-9 rounded-md border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-black/10 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 text-sm/6 max-sm:text-base/6',\n hasFilters ? 'pr-8' : 'pr-3'\n )}\n enterKeyHint=\"search\"\n placeholder={placeholder || undefined}\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n />\n {table.state.search ? (\n <button\n type=\"button\"\n onClick={clearAll}\n className=\"absolute inset-y-0 right-0 flex items-center pr-2.5\"\n aria-label=\"Clear search\"\n >\n <Icon name=\"x-mark\" className=\"size-4 text-gray-400 hover:text-gray-600\" />\n </button>\n ) : null}\n </div>\n </form>\n )}\n </div>\n </div>\n )\n}\n\n// --- Actions ---\n\nexport type NavigationAction = { label: string; to: To; replace?: boolean; preventScrollReset?: boolean; disabled?: boolean }\nexport type ButtonAction = { label: string; onClick: () => void; disabled?: boolean }\nexport type ActionParams = NavigationAction | ButtonAction\n\nexport type TableActionProps = {\n actions: (ActionParams | null | undefined)[] | null | undefined\n maxVisibleActions?: number\n compact?: boolean\n}\n\nfunction isNavigationAction(action: ActionParams): action is NavigationAction {\n return 'to' in action\n}\n\nexport function TableActions({ actions: rawActions, maxVisibleActions = 4, compact }: TableActionProps) {\n const navigate = useNavigate()\n const actions = (rawActions?.filter(Boolean) || []) as ActionParams[]\n\n // If we have more actions than the max, always use dropdown on desktop too\n const useDropdownOnDesktop = actions.length > maxVisibleActions\n\n const dropdownTriggerClassName = compact\n ? 'inline-flex items-center rounded px-2 py-0.5 text-sm font-medium text-primary-700 hover:text-primary-900 hover:bg-primary-100 truncate'\n : twMerge(\n 'inline-flex items-center rounded bg-white px-2 py-1 text-sm font-semibold text-gray-900',\n 'shadow-sm ring-1 ring-black/10',\n 'disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-white',\n 'hover:bg-gray-50 truncate'\n )\n\n return (\n <>\n <div className={useDropdownOnDesktop ? '' : 'xl:hidden'}>\n {actions.length ? (\n <MenuTrigger>\n <Button className={dropdownTriggerClassName}>\n Actions\n <Icon name=\"chevron-down\" className=\"h-5 w-5\" />\n </Button>\n <Popover offset={4} className=\"w-48 rounded-md shadow-lg ring-1 ring-black/10 bg-white z-20\">\n <Menu className=\"outline-none py-1\">\n {actions.map((a) => (\n <MenuItem\n key={a.label}\n isDisabled={a.disabled}\n onAction={() => {\n if (isNavigationAction(a)) {\n navigate(a.to, { replace: a.replace, preventScrollReset: a.preventScrollReset })\n } else {\n a.onClick()\n }\n }}\n className={({ isFocused }) =>\n twMerge(\n 'text-sm px-4 py-2 cursor-pointer outline-none',\n isFocused ? 'text-white bg-primary-500' : 'text-gray-900',\n a.disabled && 'opacity-50 cursor-not-allowed'\n )\n }\n >\n {a.label}\n </MenuItem>\n ))}\n </Menu>\n </Popover>\n </MenuTrigger>\n ) : null}\n </div>\n {!useDropdownOnDesktop ? (\n <div className=\"gap-x-2 hidden xl:flex\">\n {actions.map((a) =>\n isNavigationAction(a) ? (\n <ActionLink\n to={a.to}\n replace={a.replace}\n key={a.label}\n preventScrollReset={a.preventScrollReset}\n disabled={a.disabled}\n compact={compact}\n >\n {a.label}\n </ActionLink>\n ) : (\n <ActionButton key={a.label} onClick={a.onClick} disabled={a.disabled} compact={compact}>\n {a.label}\n </ActionButton>\n )\n )}\n </div>\n ) : null}\n </>\n )\n}\n\nfunction ActionLink(props: OptionalLinkProps & { compact?: boolean }) {\n const ref = React.useRef<HTMLAnchorElement>(null)\n const { isPressed, pressProps } = usePress({\n ref\n })\n const { compact, ...linkProps } = props\n return (\n <OptionalLink\n {...mergeProps(pressProps, linkProps)}\n ref={ref}\n className={twMerge(\n props.className,\n props.disabled && 'cursor-not-allowed opacity-30',\n compact\n ? 'inline-flex items-center rounded px-2 py-0.5 text-sm font-medium text-primary-700 hover:text-primary-900 hover:bg-primary-100 truncate'\n : twMerge(\n 'inline-flex items-center rounded bg-white px-2 py-1 text-sm font-semibold text-gray-900',\n 'shadow-sm ring-1 ring-black/10',\n 'hover:bg-gray-50 truncate'\n ),\n isPressed && 'scale-[.98]'\n )}\n >\n {props.children}\n </OptionalLink>\n )\n}\n\ntype ActionButtonProps = React.ComponentProps<typeof HeadlessButton> & { compact?: boolean }\n\nfunction ActionButton(props: ActionButtonProps) {\n const { className, compact, ...restProps } = props\n return (\n <HeadlessButton\n {...restProps}\n className={twMerge(\n typeof className === 'function' ? className({ isPressed: false }) : className,\n props.disabled && 'cursor-not-allowed opacity-30',\n compact\n ? 'inline-flex items-center rounded px-2 py-0.5 text-sm font-medium text-primary-700 hover:text-primary-900 hover:bg-primary-100 truncate'\n : twMerge(\n 'inline-flex items-center rounded bg-white px-2 py-1 text-sm font-semibold text-gray-900',\n 'shadow-sm ring-1 ring-black/10',\n 'hover:bg-gray-50 truncate data-[pressed=true]:scale-[.98]'\n )\n )}\n >\n {props.children}\n </HeadlessButton>\n )\n}\n"},{"name":"components/table/headless-templated-row-table.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { matchSorter, type MatchSorterOptions } from 'match-sorter'\n\nexport declare namespace HeadlessTemplatedRowTable {\n type RowData = Record<string, any>\n\n type Feature<\n TData extends RowData,\n FeatureState extends Record<string, any>,\n FeatureOptions extends Record<string, any>,\n FeatureExtras extends Record<string, any>\n > = (<\n TableState extends Record<string, any>,\n TableOptions extends Record<string, any>,\n TableExtras extends Record<string, any>\n >(\n table: Table<TData, TableState, TableOptions, TableExtras>\n ) => void) & {\n ['~feature']: {\n state: FeatureState\n options: FeatureOptions\n extras: FeatureExtras\n }\n }\n\n type FeatureBase<TData extends RowData> = Feature<TData, {}, {}, {}>\n\n type ExtractFeatureChanges<F extends FeatureBase<RowData>> =\n F extends Feature<infer FTData, infer FState, infer FOptions, infer FExtras>\n ? {\n TData: FTData\n State: FState\n Options: FOptions\n Extras: FExtras\n }\n : never\n\n type ExtractAllFeatureChanges<Features extends FeatureBase<RowData>[]> = Features extends [\n infer F extends FeatureBase<RowData>,\n ...infer Rest extends FeatureBase<RowData>[]\n ]\n ? ExtractFeatureChanges<F> & ExtractAllFeatureChanges<Rest>\n : {\n TData: RowData\n State: {}\n Options: {}\n Extras: {}\n }\n\n type ApplyFeature<T extends TableBase<RowData>, F extends FeatureBase<RowData>> =\n T extends Table<infer TData, infer State, infer Options, infer Extras>\n ? F extends Feature<infer FTData, infer FState, infer FOptions, infer FExtras>\n ? Table<TData & FTData, State & FState, Options & FOptions, Extras & FExtras>\n : never\n : never\n\n type ApplyFeatures<T extends TableBase<RowData>, Features extends FeatureBase<RowData>[]> = Features extends [\n infer F extends FeatureBase<RowData>,\n ...infer Rest extends FeatureBase<RowData>[]\n ]\n ? ApplyFeatures<ApplyFeature<T, F>, Rest>\n : T\n\n type TableOptions<TData extends RowData, Features extends FeatureBase<TData>[]> = {\n rows: TData[]\n features: [...Features]\n initialState?: Partial<ExtractAllFeatureChanges<Features>['State']>\n state?: Partial<ExtractAllFeatureChanges<Features>['State']>\n } & ExtractAllFeatureChanges<Features>['Options']\n\n type Table<\n TData extends RowData,\n State extends Record<string, any>,\n Options extends Record<string, any>,\n Extras extends Record<string, any>\n > = {\n getRows(): TData[]\n setRows(rows: TData[]): void\n getOptions(): Options & { state?: Partial<State>; initialState?: Partial<State> }\n setOptions(options: Options): void\n state: State\n setState(update: State | ((previous: State) => State)): void\n subscribe: (onStoreChange: () => void) => () => void\n } & Extras\n\n type TableBase<TData extends RowData> = Table<TData, {}, {}, {}>\n\n type ReactTable<\n TData extends RowData,\n Features extends FeatureBase<TData>[] = []\n > = HeadlessTemplatedRowTable.ApplyFeatures<HeadlessTemplatedRowTable.TableBase<TData>, Features>\n\n type FilterPredicate<\n TData extends RowData,\n Name extends string = string,\n Value = Name extends keyof TData ? TData[Name] : any\n > = (row: TData, value: Value, name: string) => boolean\n\n type FilterConfig<TData extends RowData, FC extends Record<string, any> = Record<string, any>> = {\n [K in keyof FC & string]: FilterPredicate<TData, K>\n }\n\n type ExtractFilterValue<C extends FilterPredicate<any, any, any>> = Parameters<C>[1]\n\n type FilterState<TData extends RowData, Config extends FilterConfig<TData>> = {\n [key in keyof Config]: ExtractFilterValue<Config[key]> | null | undefined\n }\n\n type SortComparator<TData extends RowData, Name extends string = string> = (\n row1: TData,\n row2: TData,\n name: string\n ) => number\n\n type SortConfig<TData extends RowData, SC extends Record<string, any> = Record<string, any>> = {\n [K in keyof SC & string]: SortComparator<TData, K>\n }\n\n type SortState<Config extends Record<string, SortComparator<any, any>>> = (keyof Config & string) | null\n\n type PaginationState = { limit: number; skip: number }\n\n type FilterFeature<TData extends RowData, Config extends FilterConfig<TData>> = Feature<\n TData,\n { filters: FilterState<TData, Config> },\n {\n manualFiltering?: boolean\n onFiltersChange?: (\n val: FilterState<TData, Config> | ((prev: FilterState<TData, Config>) => FilterState<TData, Config>)\n ) => void\n },\n {\n getFilterConfig(): { [key in keyof Config]: ExtractFilterValue<Config[key]> }\n setFilters: (\n val: FilterState<TData, Config> | ((prev: FilterState<TData, Config>) => FilterState<TData, Config>)\n ) => void\n }\n >\n\n type SearchFeature<TData extends RowData> = Feature<\n TData,\n { search: string },\n {\n manualSearch?: boolean\n onSearchChange?: (val: string | ((prev: string) => string)) => void\n },\n {\n setSearch: (val: string | ((prev: string) => string)) => void\n }\n >\n\n type SortFeature<TData extends RowData, Config extends SortConfig<TData>> = Feature<\n TData,\n { sort: SortState<Config> },\n {\n manualSort?: boolean\n onSortChange?: (val: SortState<Config> | ((prev: SortState<Config>) => SortState<Config>)) => void\n },\n {\n getSortConfig(): { [key in keyof Config]: null }\n setSort: (val: SortState<Config> | ((prev: SortState<Config>) => SortState<Config>)) => void\n }\n >\n\n type PaginationFeature<TData extends RowData> = Feature<\n TData,\n { pagination: PaginationState },\n {\n manualPagination?: boolean\n onPaginationChange?: (val: PaginationState | ((prev: PaginationState) => PaginationState)) => void\n },\n {\n setPagination: (val: PaginationState | ((prev: PaginationState) => PaginationState)) => void\n getPrePaginationRows(): TData[]\n }\n >\n\n type RowSelectionFeature<TData extends RowData> = Feature<\n TData,\n { rowSelection: Set<string> },\n {\n onRowSelectionChange?: (val: Set<string>) => void\n },\n {\n toggleRowSelection: (row: TData) => void\n toggleAllRowSelection: () => void\n isRowSelected: (row: TData) => boolean\n clearRowSelection: () => void\n }\n >\n\n type RowCountFeature<TData extends RowData> = Feature<TData, {}, { rowCount?: number }, { getRowCount(): number }>\n}\n\nexport const HeadlessTemplatedRowTable = {\n createTable,\n filtering,\n search,\n sort,\n pagination,\n rowSelection,\n rowCount\n}\n\n// ============================================================\n// createTable\n// ============================================================\n\nfunction createTable<\n TData extends HeadlessTemplatedRowTable.RowData,\n Features extends HeadlessTemplatedRowTable.FeatureBase<TData>[]\n>(\n initialState: HeadlessTemplatedRowTable.TableOptions<TData, Features>\n): HeadlessTemplatedRowTable.ApplyFeatures<HeadlessTemplatedRowTable.TableBase<TData>, Features> {\n let rows = initialState.rows\n let { rows: _rows, features: _features, ...options } = initialState\n\n const getRows = () => rows\n const getOptions = () => options\n\n function setRows(updatedRows: TData[]) {\n if (rows !== updatedRows) {\n rows = updatedRows\n }\n }\n function setOptions(updatedOptions: Omit<HeadlessTemplatedRowTable.TableOptions<TData, Features>, 'rows' | 'features'>) {\n options = { ...options, ...updatedOptions }\n if (updatedOptions.state) {\n table.state = { ...table.state, ...updatedOptions.state }\n }\n subscription.notify()\n }\n\n const subscription = createSubscription()\n let flushing = false\n let pendingNotify = false\n\n function setState<S>(val: S | ((previous: S) => S)) {\n if (typeof val === 'function') {\n // @ts-expect-error\n table.state = val(table.state)\n } else {\n // @ts-expect-error\n table.state = val\n }\n if (flushing) {\n pendingNotify = true\n return\n }\n flushing = true\n subscription.notify()\n let flushCount = 0\n while (pendingNotify) {\n if (++flushCount > 100) {\n console.warn('Table setState: exceeded 100 flush iterations — possible infinite notification loop')\n pendingNotify = false\n break\n }\n pendingNotify = false\n subscription.notify()\n }\n\n flushing = false\n }\n\n const table: HeadlessTemplatedRowTable.TableBase<TData> = {\n getRows,\n setRows,\n getOptions,\n setOptions,\n state: {},\n setState,\n subscribe: subscription.subscribe\n }\n\n initialState.features.forEach((feature) => feature(table))\n\n // @ts-expect-error\n return table\n}\n\n// ============================================================\n// Client Filtering\n// ============================================================\n\ntype FilterFeature<\n TData extends HeadlessTemplatedRowTable.RowData,\n Config extends HeadlessTemplatedRowTable.FilterConfig<TData>\n> = HeadlessTemplatedRowTable.FilterFeature<TData, Config>\n\nfunction filtering<\n TData extends HeadlessTemplatedRowTable.RowData,\n Config extends HeadlessTemplatedRowTable.FilterConfig<TData>\n>(filterConfig: Config): FilterFeature<TData, Config> {\n const filterKeys = Object.keys(filterConfig) as (keyof Config extends string ? keyof Config : never)[]\n const defaultFilterState = Object.fromEntries(filterKeys.map((key) => [key, null]))\n const tableFilterConfig = defaultFilterState as { [key in keyof Config]: null }\n\n return defineFeature<TData, FilterFeature<TData, Config>>((table) => {\n table.state.filters = {\n ...defaultFilterState,\n ...table.getOptions().initialState?.['filters']\n } as HeadlessTemplatedRowTable.FilterState<TData, Config>\n\n const prevGetFilterConfig = table['getFilterConfig']\n table.getFilterConfig = () => ({ ...(prevGetFilterConfig?.() ?? {}), ...tableFilterConfig })\n\n table.setFilters = (val) => {\n const onFiltersChange = table.getOptions().onFiltersChange\n if (onFiltersChange) onFiltersChange(val)\n else\n table.setState((prev) => ({\n ...prev,\n filters: typeof val === 'function' ? val(prev.filters) : val\n }))\n }\n\n const applyFilters = memo((rows: TData[], filters: HeadlessTemplatedRowTable.FilterState<TData, Config>) =>\n rows.filter((row) =>\n filterKeys.every((f) => {\n const value = filters[f]\n if (value == null) return true\n return filterConfig[f]!(row, value, f)\n })\n )\n )\n\n const originalGetRows = table.getRows\n table.getRows = () => {\n if (table.getOptions().manualFiltering) return originalGetRows()\n return applyFilters(originalGetRows(), table.state.filters)\n }\n })\n}\n\n// ============================================================\n// Client Search\n// ============================================================\n\ntype SearchFeature<TData extends HeadlessTemplatedRowTable.RowData> = HeadlessTemplatedRowTable.SearchFeature<TData>\n\nfunction search<TData extends HeadlessTemplatedRowTable.RowData>(options: MatchSorterOptions<TData>): SearchFeature<TData> {\n return defineFeature<TData, SearchFeature<TData>>((table) => {\n const applySearch = memo((rows: TData[], query: string) => {\n return query ? query.split(' ').reduce((results, term) => matchSorter(results, term, options), rows) : rows\n })\n\n table.state.search = (table.getOptions().initialState?.['search'] as string) ?? ''\n\n table.setSearch = (val) => {\n const onSearchChange = table.getOptions().onSearchChange\n if (onSearchChange) onSearchChange(val)\n else table.setState((prev) => ({ ...prev, search: typeof val === 'function' ? val(prev.search) : val }))\n }\n\n const originalGetRows = table.getRows\n table.getRows = () => {\n if (table.getOptions().manualSearch) return originalGetRows()\n return applySearch(originalGetRows(), table.state.search)\n }\n })\n}\n\n// ============================================================\n// Client Sort\n// ============================================================\n\ntype SortFeature<\n TData extends HeadlessTemplatedRowTable.RowData,\n Config extends HeadlessTemplatedRowTable.SortConfig<TData>\n> = HeadlessTemplatedRowTable.SortFeature<TData, Config>\n\nfunction sort<TData extends HeadlessTemplatedRowTable.RowData, Config extends HeadlessTemplatedRowTable.SortConfig<TData>>(\n config: Config,\n defaultSort?: HeadlessTemplatedRowTable.SortState<Config>\n): SortFeature<TData, Config> {\n const tableSortConfig = Object.fromEntries(Object.keys(config).map((f) => [f, null])) as {\n [key in keyof Config]: null\n }\n\n return defineFeature<TData, SortFeature<TData, Config>>((table) => {\n table.state.sort =\n (table.getOptions().initialState?.['sort'] as HeadlessTemplatedRowTable.SortState<Config>) ?? defaultSort ?? null\n\n const prevGetSortConfig = table['getSortConfig']\n table.getSortConfig = () => ({ ...(prevGetSortConfig?.() ?? {}), ...tableSortConfig })\n\n table.setSort = (val) => {\n const onSortChange = table.getOptions().onSortChange\n if (onSortChange) onSortChange(val)\n else table.setState((prev) => ({ ...prev, sort: typeof val === 'function' ? val(prev.sort) : val }))\n }\n\n const applySort = memo((rows: TData[], sort: HeadlessTemplatedRowTable.SortState<Config>) =>\n sort && config[sort] ? [...rows].sort((a, b) => config[sort]!(a, b, sort)) : rows\n )\n\n const originalGetRows = table.getRows\n table.getRows = () => {\n if (table.getOptions().manualSort) return originalGetRows()\n return applySort(originalGetRows(), table.state.sort)\n }\n })\n}\n\n// ============================================================\n// Client Pagination\n// ============================================================\n\ntype PaginationFeature<TData extends HeadlessTemplatedRowTable.RowData> = HeadlessTemplatedRowTable.PaginationFeature<TData>\n\nfunction pagination<TData extends HeadlessTemplatedRowTable.RowData>(defaultLimit: number): PaginationFeature<TData> {\n return defineFeature<TData, PaginationFeature<TData>>((table) => {\n table.state.pagination = (table.getOptions().initialState?.[\n 'pagination'\n ] as HeadlessTemplatedRowTable.PaginationState) ?? {\n limit: defaultLimit,\n skip: 0\n }\n\n table.setPagination = (val) => {\n const onPaginationChange = table.getOptions().onPaginationChange\n if (onPaginationChange) onPaginationChange(val)\n else table.setState((prev) => ({ ...prev, pagination: typeof val === 'function' ? val(prev.pagination) : val }))\n }\n\n const originalGetRows = table.getRows\n table.getPrePaginationRows = originalGetRows\n table.getRows = () => {\n if (table.getOptions().manualPagination) return originalGetRows()\n const { skip, limit } = table.state.pagination\n return originalGetRows().slice(skip, skip + limit)\n }\n })\n}\n\n// ============================================================\n// Client Row Selection\n// ============================================================\n\ntype RowSelectionFeature<TData extends HeadlessTemplatedRowTable.RowData> =\n HeadlessTemplatedRowTable.RowSelectionFeature<TData>\n\nfunction rowSelection<TData extends HeadlessTemplatedRowTable.RowData>(\n getRowId: (row: TData) => string\n): RowSelectionFeature<TData> {\n return defineFeature<TData, RowSelectionFeature<TData>>((table) => {\n table.state.rowSelection = (table.getOptions().initialState?.['rowSelection'] as Set<string>) ?? new Set<string>()\n\n const getSelection = () => table.state.rowSelection\n const setSelection = (next: Set<string>) => {\n const onRowSelectionChange = table.getOptions().onRowSelectionChange\n if (onRowSelectionChange) onRowSelectionChange(next)\n else table.setState((prev) => ({ ...prev, rowSelection: next }))\n }\n\n table.toggleRowSelection = (row: TData) => {\n const id = getRowId(row)\n const next = new Set(getSelection())\n if (next.has(id)) next.delete(id)\n else next.add(id)\n setSelection(next)\n }\n\n table.toggleAllRowSelection = () => {\n const rows = table.getRows()\n const rowIds = new Set(rows.map(getRowId))\n const allSelected = rowIds.size > 0 && [...rowIds].every((id) => getSelection().has(id))\n setSelection(allSelected ? new Set<string>() : rowIds)\n }\n\n table.isRowSelected = (row: TData) => {\n return getSelection().has(getRowId(row))\n }\n\n table.clearRowSelection = () => {\n setSelection(new Set<string>())\n }\n })\n}\n\n// ============================================================\n// Row Count\n// ============================================================\n\ntype RowCountFeature<TData extends HeadlessTemplatedRowTable.RowData> = HeadlessTemplatedRowTable.RowCountFeature<TData>\n\nfunction rowCount<TData extends HeadlessTemplatedRowTable.RowData>(): RowCountFeature<TData> {\n return defineFeature<TData, RowCountFeature<TData>>((table) => {\n table.getRowCount = () => {\n return (\n table.getOptions().rowCount ??\n ('getPrePaginationRows' in table ? table.getPrePaginationRows().length : table.getRows().length)\n )\n }\n })\n}\n\n// ============================================================\n// Helpers\n// ============================================================\n\nfunction memo<TArgs extends any[], TResult>(fn: (...args: TArgs) => TResult): (...args: TArgs) => TResult {\n let lastArgs: TArgs | undefined\n let lastResult: TResult\n\n return (...args: TArgs) => {\n if (lastArgs && args.length === lastArgs.length && args.every((arg, i) => arg === lastArgs![i])) {\n return lastResult\n }\n lastArgs = args\n lastResult = fn(...args)\n return lastResult\n }\n}\n\nfunction createSubscription(callback?: () => void) {\n type Subscriber = () => void\n const subscribers = new Set<Subscriber>(callback ? [callback] : [])\n function subscribe(onStoreChange: () => void) {\n subscribers.add(onStoreChange)\n function unsubscribe() {\n subscribers.delete(onStoreChange)\n }\n\n return unsubscribe\n }\n function notify() {\n subscribers.forEach((s) => s())\n }\n return { subscribe, notify }\n}\n\nfunction defineFeature<\n TData extends HeadlessTemplatedRowTable.RowData,\n F extends HeadlessTemplatedRowTable.Feature<TData, {}, {}, {}>\n>(\n callback: <\n TableState extends Record<string, any>,\n TableOptions extends Record<string, any>,\n TableExtras extends Record<string, any>\n >(\n table: HeadlessTemplatedRowTable.ApplyFeature<\n HeadlessTemplatedRowTable.Table<TData, TableState, TableOptions, TableExtras>,\n F\n >\n ) => void\n): F {\n // @ts-expect-error\n return (table) => callback(table)\n}\n"},{"name":"components/table/use-client-table.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport React from 'react'\nimport { HeadlessTemplatedRowTable } from './headless-templated-row-table'\nimport type { MatchSorterOptions } from 'match-sorter'\n\ntype ClientFeatures<\n TData extends HeadlessTemplatedRowTable.RowData,\n FC extends HeadlessTemplatedRowTable.FilterConfig<TData> = {},\n SC extends HeadlessTemplatedRowTable.SortConfig<TData> = {}\n> = [\n ReturnType<typeof HeadlessTemplatedRowTable.rowCount<TData>>,\n ReturnType<typeof HeadlessTemplatedRowTable.filtering<TData, HeadlessTemplatedRowTable.FilterConfig<TData, FC>>>,\n ReturnType<typeof HeadlessTemplatedRowTable.search<TData>>,\n ReturnType<typeof HeadlessTemplatedRowTable.sort<TData, SC>>,\n ReturnType<typeof HeadlessTemplatedRowTable.rowSelection<TData>>,\n ReturnType<typeof HeadlessTemplatedRowTable.pagination<TData>>\n]\n\nexport function useClientTable<\n TData extends HeadlessTemplatedRowTable.RowData,\n FC extends HeadlessTemplatedRowTable.FilterConfig<NoInfer<TData>> = {},\n SC extends HeadlessTemplatedRowTable.SortConfig<NoInfer<TData>> = {}\n>({\n rows: rowsInput,\n filters,\n search,\n sorts,\n getRowId,\n defaultPageSize,\n onFiltersChange,\n onSearchChange,\n onSortChange,\n onPaginationChange,\n onRowSelectionChange,\n manualFiltering,\n manualSearch,\n manualSort,\n manualPagination\n}: {\n rows: TData[]\n filters?: FC & HeadlessTemplatedRowTable.FilterConfig<NoInfer<TData>, FC>\n search?: MatchSorterOptions<TData>\n sorts?: SC & HeadlessTemplatedRowTable.SortConfig<NoInfer<TData>, SC>\n getRowId: (row: TData) => string\n defaultPageSize?: number\n manualFiltering?: boolean\n onFiltersChange?: (\n val:\n | HeadlessTemplatedRowTable.FilterState<TData, FC>\n | ((prev: HeadlessTemplatedRowTable.FilterState<TData, FC>) => HeadlessTemplatedRowTable.FilterState<TData, FC>)\n ) => void\n manualSearch?: boolean\n onSearchChange?: (val: string | ((prev: string) => string)) => void\n manualSort?: boolean\n onSortChange?: (\n val:\n | HeadlessTemplatedRowTable.SortState<SC>\n | ((prev: HeadlessTemplatedRowTable.SortState<SC>) => HeadlessTemplatedRowTable.SortState<SC>)\n ) => void\n manualPagination?: boolean\n onPaginationChange?: (val: any) => void\n onRowSelectionChange?: (val: Set<string>) => void\n}): HeadlessTemplatedRowTable.ReactTable<TData, ClientFeatures<TData, FC, SC>> {\n const tableRef = React.useRef(null as HeadlessTemplatedRowTable.ReactTable<TData, ClientFeatures<TData, FC, SC>> | null)\n\n const options = React.useMemo(() => {\n const applyState = (key: string, val: any) => {\n tableRef.current!.setState((prev: any) => ({\n ...prev,\n [key]: typeof val === 'function' ? val(prev[key]) : val\n }))\n }\n const resetDependentState = (resetPagination: boolean) => {\n tableRef.current!.setState((prev: any) => {\n const resetPag = resetPagination && !manualPagination && prev.pagination?.skip !== 0\n const resetSel = prev.rowSelection?.size > 0\n if (!resetPag && !resetSel) return prev\n return {\n ...prev,\n ...(resetPag ? { pagination: { ...prev.pagination, skip: 0 } } : {}),\n ...(resetSel ? { rowSelection: new Set() } : {})\n }\n })\n }\n\n return {\n onFiltersChange: (val: any) => {\n if (onFiltersChange) onFiltersChange(val)\n else applyState('filters', val)\n resetDependentState(true)\n },\n onSearchChange: (val: any) => {\n if (onSearchChange) onSearchChange(val)\n else applyState('search', val)\n resetDependentState(true)\n },\n onSortChange: (val: any) => {\n if (onSortChange) onSortChange(val)\n else applyState('sort', val)\n resetDependentState(true)\n },\n onPaginationChange: (val: any) => {\n if (onPaginationChange) onPaginationChange(val)\n else applyState('pagination', val)\n resetDependentState(false)\n },\n onRowSelectionChange,\n manualFiltering,\n manualSearch,\n manualSort,\n manualPagination\n }\n }, [\n onFiltersChange,\n onSearchChange,\n onSortChange,\n onPaginationChange,\n onRowSelectionChange,\n manualFiltering,\n manualSearch,\n manualSort,\n manualPagination\n ])\n\n if (!tableRef.current) {\n tableRef.current = HeadlessTemplatedRowTable.createTable({\n features: [\n HeadlessTemplatedRowTable.filtering<TData, HeadlessTemplatedRowTable.FilterConfig<TData, FC>>(\n (filters ?? {}) as HeadlessTemplatedRowTable.FilterConfig<TData, FC>\n ),\n HeadlessTemplatedRowTable.search<TData>(search ?? {}),\n HeadlessTemplatedRowTable.sort<TData, SC>((sorts ?? {}) as SC),\n HeadlessTemplatedRowTable.rowCount(),\n HeadlessTemplatedRowTable.rowSelection<TData>(getRowId),\n HeadlessTemplatedRowTable.pagination<TData>(defaultPageSize ?? 10)\n ],\n rows: rowsInput,\n ...options\n }) as HeadlessTemplatedRowTable.ReactTable<TData, ClientFeatures<TData, FC, SC>>\n }\n const table = tableRef.current\n\n const state = React.useSyncExternalStore(\n table.subscribe,\n () => table.state,\n () => table.state\n )\n table.state = state\n\n React.useEffect(() => {\n table.setRows(rowsInput)\n }, [rowsInput])\n React.useEffect(() => {\n table.setOptions(options)\n }, [options])\n\n return table\n}\n"},{"name":"components/table/use-server-table.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport React from 'react'\nimport { useSearchParams as useSearchParamsRR } from 'react-router'\nimport { HeadlessTemplatedRowTable } from './headless-templated-row-table'\nimport { SearchParams } from '../../utils/use-query-state'\nimport { type ServerTable } from './server-table'\n\ntype ServerFilterConfig<TData extends HeadlessTemplatedRowTable.RowData, FC extends Record<string, any>> = {\n [K in keyof FC & string]: HeadlessTemplatedRowTable.FilterPredicate<TData, K, NonNullable<FC[K]>>\n}\n\ntype ServerSortConfig<TData extends HeadlessTemplatedRowTable.RowData, SortKeys extends string> = Record<\n SortKeys,\n HeadlessTemplatedRowTable.SortComparator<TData>\n>\n\ntype ServerFeatures<\n TData extends HeadlessTemplatedRowTable.RowData,\n FC extends Record<string, any>,\n SC extends Record<string, any>\n> = [\n ReturnType<typeof HeadlessTemplatedRowTable.rowCount<TData>>,\n ReturnType<typeof HeadlessTemplatedRowTable.filtering<TData, ServerFilterConfig<TData, FC>>>,\n ReturnType<typeof HeadlessTemplatedRowTable.sort<TData, ServerSortConfig<TData, keyof SC & string>>>,\n ReturnType<typeof HeadlessTemplatedRowTable.search<TData>>,\n ReturnType<typeof HeadlessTemplatedRowTable.rowSelection<TData>>,\n ReturnType<typeof HeadlessTemplatedRowTable.pagination<TData>>\n]\n\nconst noopSearchCodec = SearchParams.codecs.string().default('')\nconst noopPaginationLoader = SearchParams.createLoader({\n limit: SearchParams.codecs.int().default(0),\n skip: SearchParams.codecs.int().default(0)\n})\n\nexport function useServerTable<\n TData extends HeadlessTemplatedRowTable.RowData,\n FC extends Record<string, any> = {},\n SC extends Record<string, any> = {}\n>(\n serverTable: ServerTable.ServerTable<TData, FC, SC>,\n _options: {\n getRowId: (row: TData) => string\n }\n): HeadlessTemplatedRowTable.ReactTable<TData, ServerFeatures<TData, FC, SC>> {\n type SortKey = keyof SC & string\n\n const filterCodec = React.useMemo(\n () => SearchParams.deserializeLoader(serverTable.filters),\n [serverTable.filters]\n ) as SearchParams.QueryStateLoader<FC>\n const sortCodec = React.useMemo(() => SearchParams.deserializeCodec(serverTable.sorts), [serverTable.sorts])\n const searchCodec = React.useMemo(\n () => (serverTable.search ? SearchParams.deserializeCodec(serverTable.search) : noopSearchCodec),\n [serverTable.search]\n )\n const paginationCodec = React.useMemo(\n () => (serverTable.pagination ? SearchParams.deserializeLoader(serverTable.pagination) : noopPaginationLoader),\n [serverTable.pagination]\n )\n\n const [filters] = SearchParams.useSearchParams(filterCodec)\n const [sort] = SearchParams.useState('sort', sortCodec)\n const sortValues = serverTable.sorts.values\n const [search] = SearchParams.useState('search', searchCodec)\n const [pagination, setPagination] = SearchParams.useSearchParams(paginationCodec)\n const [, setURLParams] = useSearchParamsRR()\n\n // Build noop configs for features (manual mode — predicates/comparators won't be called)\n const filterConfig = React.useMemo(\n () => Object.fromEntries(Object.keys(filterCodec ?? {}).map((key) => [key, () => true])),\n [filterCodec]\n ) as unknown as ServerFilterConfig<TData, FC>\n const sortConfig = React.useMemo(\n () => Object.fromEntries(sortValues.map((v) => [v, () => 0])),\n [sortValues]\n ) as unknown as ServerSortConfig<TData, SortKey>\n\n const state = React.useMemo(\n () => ({\n filters,\n sort: sort as SortKey | null,\n search: search ?? '',\n pagination: { limit: pagination.limit, skip: pagination.skip }\n }),\n [filters, sort, search, pagination]\n )\n\n const options = React.useMemo(\n () => ({\n manualFiltering: true as const,\n manualSearch: true as const,\n manualSort: true as const,\n manualPagination: true as const,\n onFiltersChange: (v: any) => {\n setURLParams(\n (prev) => {\n const next = new URLSearchParams(prev)\n const oldFilters = Object.fromEntries(\n Object.entries(filterCodec).map(([name, codec]: [string, any]) => {\n try {\n return [name, codec.decode(next.get(name)) ?? codec._def?.defaultValue ?? null]\n } catch {\n return [name, codec._def?.defaultValue ?? null]\n }\n })\n )\n const newFilters = typeof v === 'function' ? v(oldFilters) : v\n for (const [name, codec] of Object.entries(filterCodec) as [string, any][]) {\n const encoded = codec.encode(newFilters[name])\n if (\n encoded === null ||\n (codec._def?.defaultValue != null &&\n codec._def?.clearOnDefault &&\n encoded === codec.encode(codec._def?.defaultValue))\n ) {\n next.delete(name)\n } else {\n next.set(name, encoded)\n }\n }\n next.delete('skip')\n return next\n },\n { preventScrollReset: true, replace: true }\n )\n table.clearRowSelection?.()\n },\n onSortChange: (v: any) => {\n setURLParams(\n (prev) => {\n const next = new URLSearchParams(prev)\n const newSort = typeof v === 'function' ? v(sortCodec.decode(next.get('sort'))) : v\n const encoded = sortCodec.encode(newSort)\n if (encoded === null) next.delete('sort')\n else next.set('sort', encoded)\n next.delete('skip')\n return next\n },\n { preventScrollReset: true, replace: true }\n )\n table.clearRowSelection?.()\n },\n onSearchChange: (v: any) => {\n setURLParams(\n (prev) => {\n const next = new URLSearchParams(prev)\n const newSearch = typeof v === 'function' ? v(next.get('search') ?? '') : v\n if (newSearch) next.set('search', newSearch)\n else next.delete('search')\n next.delete('skip')\n return next\n },\n { preventScrollReset: true, replace: true }\n )\n table.clearRowSelection?.()\n },\n onPaginationChange: (v: any) => setPagination(v, { preventScrollReset: true, replace: true }),\n rowCount: serverTable.totalRows\n }),\n [filterCodec, sortCodec, setURLParams, setPagination, serverTable.totalRows]\n )\n\n const { current: table } = React.useRef(\n HeadlessTemplatedRowTable.createTable({\n features: [\n HeadlessTemplatedRowTable.rowCount(),\n HeadlessTemplatedRowTable.filtering<TData, ServerFilterConfig<TData, FC>>(filterConfig),\n HeadlessTemplatedRowTable.sort<TData, ServerSortConfig<TData, SortKey>>(sortConfig),\n HeadlessTemplatedRowTable.search<TData>({} as any),\n HeadlessTemplatedRowTable.rowSelection<TData>(_options.getRowId),\n HeadlessTemplatedRowTable.pagination<TData>(pagination.limit ?? 25)\n ],\n rows: serverTable.rows,\n initialState: state,\n ...options\n })\n )\n\n table.state = React.useSyncExternalStore(\n table.subscribe,\n () => table.state,\n () => table.state\n )\n\n table.setRows(serverTable.rows)\n\n React.useEffect(() => {\n table.setOptions({ state, ...options })\n }, [state, options])\n\n return table\n}\n"},{"name":"components/table/server-table.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { matchSorter, type MatchSorterOptions } from 'match-sorter'\nimport { SearchParams } from '../../utils/use-query-state'\nimport type { SearchParams as SearchParamsTypes } from '../../utils/use-query-state'\nimport type { HeadlessTemplatedRowTable } from './headless-templated-row-table'\n\nexport declare namespace ServerTable {\n type RowData = HeadlessTemplatedRowTable.RowData\n\n type ServerTableOptions<TData extends RowData, FC extends Record<string, any>, SC extends Record<string, any>> = {\n rows: TData[]\n url: string | URL\n filters?: FilterFieldMap<TData, FC>\n sorts?: HeadlessTemplatedRowTable.SortConfig<TData, SC>\n defaultSort?: HeadlessTemplatedRowTable.SortState<SC>\n search?: MatchSorterOptions<TData>\n pagination?: number\n }\n\n type ServerTable<TData extends RowData, FC extends Record<string, any>, SC extends Record<string, any>> = {\n rows: TData[]\n totalRows: number\n filters: SerializedFilterCodecs<FilterFieldMap<TData, FC>>\n sorts: SerializedSortCodec<SC>\n search: SearchParamsTypes.CodecDef<string | null> | null\n pagination: {\n limit: SearchParamsTypes.CodecDef<number | null>\n skip: SearchParamsTypes.CodecDef<number | null>\n } | null\n }\n\n // ---- Filtering ----\n\n type FilterField<TData extends RowData, Name extends string = string, Value = any> = {\n filter: HeadlessTemplatedRowTable.FilterPredicate<TData, Name, NonNullable<Value>>\n codec: SearchParamsTypes.Codec<SearchParamsTypes.CodecDef<Value>>\n }\n\n type FilterFieldMap<TData extends RowData, FC extends Record<string, any> = Record<string, any>> = {\n [K in keyof FC & string]: FilterField<TData, K, FC[K]>\n }\n\n type ExtractFilterValue<C extends FilterField<any, any, any>> = Parameters<C['filter']>[1]\n\n type FilterValues<TData extends RowData, Def extends FilterFieldMap<TData>> = {\n [key in keyof Def]: ExtractFilterValue<Def[key]> | null | undefined\n }\n\n type SerializedFilterCodecs<Def extends FilterFieldMap<any>> = {\n [K in keyof Def & string]: SearchParamsTypes.SerializedCodec<Def[K]['codec']>\n }\n\n // ---- Sorting ----\n\n type SortCodec<SC extends Record<string, any>> = SearchParamsTypes.Codec<\n SearchParamsTypes.CodecDef<(keyof SC & string) | null>\n >\n\n type SerializedSortCodec<SC extends Record<string, any>> = SearchParamsTypes.SerializedCodec<SortCodec<SC>> & {\n values: readonly (keyof SC & string)[]\n }\n}\n\nexport function createServerTable<\n TData extends ServerTable.RowData,\n FC extends Record<string, any> = {},\n SC extends Record<string, any> = {}\n>(config: ServerTable.ServerTableOptions<TData, FC, SC>): ServerTable.ServerTable<TData, FC, SC> {\n type Def = ServerTable.FilterFieldMap<TData, FC>\n let rows = config.rows\n\n // Build a unified loader from all feature codecs and read state from URL\n const filterDef = config.filters\n const filterKeys = filterDef ? (Object.keys(filterDef) as (keyof FC & string)[]) : []\n const sortKeys = config.sorts ? (Object.keys(config.sorts) as (keyof SC & string)[]) : []\n\n // Build codecs once\n const sortCodec = config.sorts\n ? SearchParams.codecs.enum(sortKeys).default(config.defaultSort ?? null)\n : SearchParams.codecs.enum([] as string[])\n const searchCodec = config.search ? SearchParams.codecs.string().default('') : null\n const limitCodec = config.pagination != null ? SearchParams.codecs.int().default(config.pagination) : null\n const skipCodec = config.pagination != null ? SearchParams.codecs.int().default(0) : null\n\n const loader = {\n ...Object.fromEntries(filterKeys.map((key) => [key, filterDef![key]!.codec])),\n ...(config.sorts ? { sort: sortCodec } : {}),\n ...(searchCodec ? { search: searchCodec } : {}),\n ...(limitCodec && skipCodec ? { limit: limitCodec, skip: skipCodec } : {})\n }\n\n const params = SearchParams.getSearchParamsState(config.url, loader)\n\n // Filtering\n const filterCodecs = filterDef\n ? (Object.fromEntries(\n filterKeys.map((key) => [key, SearchParams.serializeCodec(filterDef[key]!.codec)])\n ) as ServerTable.SerializedFilterCodecs<Def>)\n : ({} as ServerTable.SerializedFilterCodecs<Def>)\n\n rows = rows.filter((row) =>\n filterKeys.every((f) => {\n const value = (params as Record<string, any>)[f]\n if (value == null) return true\n return filterDef![f]!.filter(row, value, f)\n })\n )\n\n // Search\n if (config.search && searchCodec) {\n const query = (params as any).search as string\n if (query) {\n rows = query.split(' ').reduce((results, term) => matchSorter(results, term, config.search!), rows)\n }\n }\n\n // Sort\n if (config.sorts) {\n const sortKey = ((params as any).sort as string) || null\n if (sortKey && config.sorts[sortKey as keyof SC & string]) {\n rows = [...rows].sort((a, b) => config.sorts![sortKey as keyof SC & string]!(a, b, sortKey))\n }\n }\n\n // Pagination\n const totalRows = rows.length\n if (config.pagination != null) {\n const limit = ((params as any).limit as number) ?? config.pagination\n const skip = ((params as any).skip as number) ?? 0\n rows = rows.slice(skip, skip + limit)\n }\n\n return {\n rows,\n totalRows,\n filters: filterCodecs,\n sorts: SearchParams.serializeCodec(sortCodec) as ServerTable.SerializedSortCodec<SC>,\n search: searchCodec ? SearchParams.serializeCodec(searchCodec) : null,\n pagination:\n limitCodec && skipCodec\n ? { limit: SearchParams.serializeCodec(limitCodec), skip: SearchParams.serializeCodec(skipCodec) }\n : null\n }\n}\n"},{"name":"components/table/table-container.type-test.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport type { StandardTable, TableLocalization, TableFilterLocalization } from './table-types'\nimport type { Iso } from 'iso-fns'\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\ntype Expect<T extends true> = T\ntype Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false\n\n// =============================================================================\n// Setup — concrete filter/sort configs\n// =============================================================================\n\ntype StatusValue = 'active' | 'inactive'\ntype DateRangeValue = { startDate: Iso.Date; endDate: Iso.Date }\n\ntype FC = { status: StatusValue; dateRange: DateRangeValue }\ntype SC = { newest: null; oldest: null }\n\ntype MyTable = StandardTable<FC, SC>\ntype MyLocalization = TableLocalization<FC, SC>\n\n// =============================================================================\n// TableFilterLocalization discriminates on value type\n// =============================================================================\n\n// String union → DefaultTableFilter (has options array with typed values, no `type` discriminant)\nconst _selectFilter: TableFilterLocalization<StatusValue> = {\n label: 'Status',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Inactive', value: 'inactive' }\n ]\n}\n\n// Date range → DateRangeTableFilter (type: 'date-range', no value-typed options)\nconst _dateFilter: TableFilterLocalization<DateRangeValue> = {\n label: 'Date range',\n type: 'date-range'\n}\n\n// date-range value rejects DefaultTableFilter form\nconst _dateFilterRejectsSelect: TableFilterLocalization<DateRangeValue> = {\n label: 'Date range',\n type: 'date-range',\n // @ts-expect-error — DefaultTableFilter 'options' shape is not valid for date-range\n options: [{ label: 'Last 7 days', value: 'last7' }]\n}\n\n// string-union value rejects DateRangeTableFilter form\nconst _selectFilterRejectsDateRange: TableFilterLocalization<StatusValue> = {\n label: 'Status',\n // @ts-expect-error — type: 'date-range' is not valid for a select filter\n type: 'date-range'\n}\n\n// =============================================================================\n// TableLocalization requires the exact filter + sort keys\n// =============================================================================\n\ntype _FiltersHasKeys = Expect<Equal<keyof MyLocalization['filters'], 'status' | 'dateRange'>>\ntype _SortsHasKeys = Expect<Equal<keyof MyLocalization['sorts'], 'newest' | 'oldest'>>\ntype _SortShape = Expect<Equal<MyLocalization['sorts']['newest'], { label: string }>>\n\n// =============================================================================\n// Full valid localization object\n// =============================================================================\n\nconst _valid: MyLocalization = {\n filters: {\n status: {\n label: 'Status',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Inactive', value: 'inactive' }\n ]\n },\n dateRange: {\n label: 'Date range',\n type: 'date-range'\n }\n },\n sorts: {\n newest: { label: 'Newest' },\n oldest: { label: 'Oldest' }\n },\n searchPlaceholder: 'Search...'\n}\n\n// =============================================================================\n// Negative cases\n// =============================================================================\n\ndeclare function checkLocalization(l: MyLocalization): void\n\n// Missing a required filter key\ncheckLocalization({\n // @ts-expect-error — 'dateRange' is missing from filters\n filters: {\n status: { label: 'Status', options: [{ label: 'Active', value: 'active' }] }\n },\n sorts: { newest: { label: 'Newest' }, oldest: { label: 'Oldest' } }\n})\n\n// Missing a required sort key\ncheckLocalization({\n filters: {\n status: { label: 'Status', options: [{ label: 'Active', value: 'active' }] },\n dateRange: { label: 'Date range', type: 'date-range' }\n },\n // @ts-expect-error — 'oldest' is missing from sorts\n sorts: { newest: { label: 'Newest' } }\n})\n\n// Wrong option value type — status options must use StatusValue\ncheckLocalization({\n filters: {\n status: {\n label: 'Status',\n // @ts-expect-error — 'unknown' is not assignable to StatusValue\n options: [{ label: 'Unknown', value: 'unknown' }]\n },\n dateRange: { label: 'Date range', type: 'date-range' }\n },\n sorts: { newest: { label: 'Newest' }, oldest: { label: 'Oldest' } }\n})\n\n// Wrong filter form — dateRange requires DateRangeTableFilter, not DefaultTableFilter\ncheckLocalization({\n filters: {\n status: { label: 'Status', options: [{ label: 'Active', value: 'active' }] },\n dateRange: {\n label: 'Date range',\n // @ts-expect-error — select-style options array is not valid for date-range filter\n options: [{ label: 'Last 7 days', value: 'last7' }]\n }\n },\n sorts: { newest: { label: 'Newest' }, oldest: { label: 'Oldest' } }\n})\n\n// =============================================================================\n// TableContainer generic inference — localization is derived from table type\n// =============================================================================\n\ntype InferredLocalization<T extends StandardTable> = TableLocalization<\n ReturnType<T['getFilterConfig']>,\n ReturnType<T['getSortConfig']>\n>\n\n// Inferred localization from MyTable matches the manually specified one\ntype _Inferred = Expect<Equal<InferredLocalization<MyTable>, MyLocalization>>\n\n// A table with no filters/sorts infers empty objects\ntype EmptyTable = StandardTable<{}, {}>\ntype EmptyLocalization = InferredLocalization<EmptyTable>\ntype _EmptyFilters = Expect<Equal<EmptyLocalization['filters'], {}>>\ntype _EmptySorts = Expect<Equal<EmptyLocalization['sorts'], {}>>\n"},{"name":"components/table/use-client-table.type-test.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { useClientTable } from './use-client-table'\nimport type { TableLocalization } from './table-types'\nimport type { Iso } from 'iso-fns'\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\ntype Expect<T extends true> = T\ntype Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false\n\n// =============================================================================\n// Setup\n// =============================================================================\n\ntype Row = {\n id: string\n status: 'active' | 'inactive'\n name: string\n createdAt: string\n}\n\n// =============================================================================\n// Filter and sort config inference\n// =============================================================================\n\nfunction testFilterAndSortInference() {\n const table = useClientTable({\n rows: [] as Row[],\n getRowId: (row) => row.id,\n filters: {\n status: (row, value) => row.status === value\n },\n sorts: {\n newest: (a, b) => a.createdAt.localeCompare(b.createdAt),\n oldest: (a, b) => b.createdAt.localeCompare(a.createdAt)\n }\n })\n\n // getFilterConfig() keys match the provided filter names\n type _FilterKeys = Expect<Equal<keyof ReturnType<typeof table.getFilterConfig>, 'status'>>\n\n // getSortConfig() keys match the provided sort names\n type _SortKeys = Expect<Equal<keyof ReturnType<typeof table.getSortConfig>, 'newest' | 'oldest'>>\n\n // state.sort is the union of sort keys | null\n type _SortState = Expect<Equal<typeof table.state.sort, 'newest' | 'oldest' | null>>\n\n // state.filters keys match filter names\n type _FilterStateKeys = Expect<Equal<keyof typeof table.state.filters, 'status'>>\n}\n\n// =============================================================================\n// Localization compatibility — select filter\n// =============================================================================\n\nfunction testLocalizationWithSelectFilter() {\n const table = useClientTable({\n rows: [] as Row[],\n getRowId: (row) => row.id,\n filters: {\n status: (row, value) => row.status === value\n },\n sorts: {\n newest: (a, b) => 0,\n oldest: (a, b) => 0\n }\n })\n\n type Loc = TableLocalization<ReturnType<typeof table.getFilterConfig>, ReturnType<typeof table.getSortConfig>>\n\n type _LocFilterKeys = Expect<Equal<keyof Loc['filters'], 'status'>>\n type _LocSortKeys = Expect<Equal<keyof Loc['sorts'], 'newest' | 'oldest'>>\n\n // Valid localization with typed option values\n const _loc: Loc = {\n filters: {\n status: {\n label: 'Status',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Inactive', value: 'inactive' }\n ]\n }\n },\n sorts: {\n newest: { label: 'Newest' },\n oldest: { label: 'Oldest' }\n }\n }\n}\n\n// =============================================================================\n// Localization compatibility — date range filter\n// =============================================================================\n\nfunction testLocalizationWithDateRangeFilter() {\n type RowWithDate = {\n id: string\n dateRange: { startDate: Iso.Date; endDate: Iso.Date }\n }\n\n const table = useClientTable({\n rows: [] as RowWithDate[],\n getRowId: (row) => row.id,\n filters: {\n dateRange: (row, value) => true\n }\n })\n\n type Loc = TableLocalization<ReturnType<typeof table.getFilterConfig>, ReturnType<typeof table.getSortConfig>>\n\n // Valid: date-range filter accepts DateRangeTableFilter form\n const _loc: Loc = {\n filters: {\n dateRange: { label: 'Date Range', type: 'date-range' }\n },\n sorts: {}\n }\n\n // Invalid: date-range filter rejects select-style options\n const _badLoc: Loc = {\n filters: {\n dateRange: {\n label: 'Date Range',\n type: 'date-range',\n // @ts-expect-error — select-style options array is not valid for date-range\n options: [{ label: 'X', value: 'x' }]\n }\n },\n sorts: {}\n }\n}\n\n// =============================================================================\n// Filter predicate contextual typing — row and value are well typed\n// =============================================================================\n\nfunction testFilterPredicateContextualTyping() {\n useClientTable({\n rows: [] as Row[],\n getRowId: (row) => row.id,\n filters: {\n // @ts-expect-error — 'nonExistent' doesn't exist on Row\n status: (row, value) => row.nonExistent === value\n }\n })\n\n useClientTable({\n rows: [] as Row[],\n getRowId: (row) => row.id,\n filters: {\n // value is Row['status'], comparing to a number should error\n // @ts-expect-error — Row['status'] is not comparable to number\n status: (row, value) => value === 123\n }\n })\n}\n\n// =============================================================================\n// Sort comparator contextual typing — row is well typed\n// =============================================================================\n\nfunction testSortComparatorContextualTyping() {\n useClientTable({\n rows: [] as Row[],\n getRowId: (row) => row.id,\n sorts: {\n // @ts-expect-error — 'nonExistent' doesn't exist on Row\n byName: (a, b) => a.nonExistent.localeCompare(b.name)\n }\n })\n}\n\n// =============================================================================\n// Negative: wrong localization option value\n// =============================================================================\n\ndeclare function checkLocalization(l: TableLocalization<{ status: 'active' | 'inactive' }, {}>): void\n\nfunction testWrongLocalizationOptionValue() {\n checkLocalization({\n filters: {\n status: {\n label: 'Status',\n // @ts-expect-error — 'unknown' is not assignable to Row['status']\n options: [{ label: 'Unknown', value: 'unknown' }]\n }\n },\n sorts: {}\n })\n}\n\n// =============================================================================\n// No filters/sorts → empty localization\n// =============================================================================\n\nfunction testNoFiltersOrSorts() {\n const table = useClientTable({\n rows: [] as Row[],\n getRowId: (row) => row.id\n })\n\n type Loc = TableLocalization<ReturnType<typeof table.getFilterConfig>, ReturnType<typeof table.getSortConfig>>\n\n type _EmptyFilters = Expect<Equal<Loc['filters'], {}>>\n type _EmptySorts = Expect<Equal<Loc['sorts'], {}>>\n}\n"},{"name":"components/table/use-server-table.type-test.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { useServerTable } from './use-server-table'\nimport type { ServerTable } from './server-table'\nimport type { TableLocalization } from './table-types'\nimport type { Iso } from 'iso-fns'\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\ntype Expect<T extends true> = T\ntype Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false\n\n// =============================================================================\n// Setup\n// =============================================================================\n\ntype Row = {\n id: string\n status: 'active' | 'inactive'\n name: string\n createdAt: string\n}\n\n// =============================================================================\n// Filter and sort config inference\n// =============================================================================\n\nfunction testFilterAndSortInference() {\n const serverTable = null as unknown as ServerTable.ServerTable<\n Row,\n { status: 'active' | 'inactive' },\n { newest: true; oldest: true }\n >\n\n const table = useServerTable(serverTable, { getRowId: (row) => row.id })\n\n // getFilterConfig() keys match the provided filter names\n type _FilterKeys = Expect<Equal<keyof ReturnType<typeof table.getFilterConfig>, 'status'>>\n\n // getSortConfig() keys match the provided sort names\n type _SortKeys = Expect<Equal<keyof ReturnType<typeof table.getSortConfig>, 'newest' | 'oldest'>>\n\n // state.sort is the union of sort keys | null\n type _SortState = Expect<Equal<typeof table.state.sort, 'newest' | 'oldest' | null>>\n\n // state.filters keys match filter names\n type _FilterStateKeys = Expect<Equal<keyof typeof table.state.filters, 'status'>>\n}\n\n// =============================================================================\n// Localization compatibility — select filter\n// =============================================================================\n\nfunction testLocalizationWithSelectFilter() {\n const serverTable = null as unknown as ServerTable.ServerTable<\n Row,\n { status: 'active' | 'inactive' },\n { newest: true; oldest: true }\n >\n\n const table = useServerTable(serverTable, { getRowId: (row) => row.id })\n\n type Loc = TableLocalization<ReturnType<typeof table.getFilterConfig>, ReturnType<typeof table.getSortConfig>>\n\n type _LocFilterKeys = Expect<Equal<keyof Loc['filters'], 'status'>>\n type _LocSortKeys = Expect<Equal<keyof Loc['sorts'], 'newest' | 'oldest'>>\n\n // Valid localization with typed option values\n const _loc: Loc = {\n filters: {\n status: {\n label: 'Status',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Inactive', value: 'inactive' }\n ]\n }\n },\n sorts: {\n newest: { label: 'Newest' },\n oldest: { label: 'Oldest' }\n }\n }\n}\n\n// =============================================================================\n// Localization compatibility — date range filter\n// =============================================================================\n\nfunction testLocalizationWithDateRangeFilter() {\n type RowWithDate = {\n id: string\n dateRange: { startDate: Iso.Date; endDate: Iso.Date }\n }\n\n const serverTable = null as unknown as ServerTable.ServerTable<\n RowWithDate,\n { dateRange: { startDate: Iso.Date; endDate: Iso.Date } },\n {}\n >\n\n const table = useServerTable(serverTable, { getRowId: (row) => row.id })\n\n type Loc = TableLocalization<ReturnType<typeof table.getFilterConfig>, ReturnType<typeof table.getSortConfig>>\n\n // Valid: date-range filter accepts DateRangeTableFilter form\n const _loc: Loc = {\n filters: {\n dateRange: { label: 'Date Range', type: 'date-range' }\n },\n sorts: {}\n }\n\n // Invalid: date-range filter rejects select-style options\n const _badLoc: Loc = {\n filters: {\n dateRange: {\n label: 'Date Range',\n type: 'date-range',\n // @ts-expect-error — select-style options array is not valid for date-range\n options: [{ label: 'X', value: 'x' }]\n }\n },\n sorts: {}\n }\n}\n\n// =============================================================================\n// Negative: wrong localization option value\n// =============================================================================\n\ndeclare function checkLocalization(l: TableLocalization<{ status: 'active' | 'inactive' }, {}>): void\n\nfunction testWrongLocalizationOptionValue() {\n checkLocalization({\n filters: {\n status: {\n label: 'Status',\n // @ts-expect-error — 'unknown' is not assignable to 'active' | 'inactive'\n options: [{ label: 'Unknown', value: 'unknown' }]\n }\n },\n sorts: {}\n })\n}\n\n// =============================================================================\n// No filters/sorts → empty localization\n// =============================================================================\n\nfunction testNoFiltersOrSorts() {\n const serverTable = null as unknown as ServerTable.ServerTable<Row, {}, {}>\n\n const table = useServerTable(serverTable, { getRowId: (row) => row.id })\n\n type Loc = TableLocalization<ReturnType<typeof table.getFilterConfig>, ReturnType<typeof table.getSortConfig>>\n\n type _EmptyFilters = Expect<Equal<Loc['filters'], {}>>\n type _EmptySorts = Expect<Equal<Loc['sorts'], {}>>\n}\n\n// =============================================================================\n// State shape — search and pagination\n// =============================================================================\n\nfunction testStateShape() {\n const serverTable = null as unknown as ServerTable.ServerTable<Row, { status: 'active' | 'inactive' }, { newest: true }>\n\n const table = useServerTable(serverTable, { getRowId: (row) => row.id })\n\n type _Search = Expect<Equal<typeof table.state.search, string>>\n type _Pagination = Expect<Equal<typeof table.state.pagination, { limit: number; skip: number }>>\n}\n"},{"name":"components/table/table-filters/select-filter.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { twMerge } from 'tailwind-merge'\nimport { Icon } from '../../icon'\nimport type { ReactNode } from 'react'\nimport type { StandardTable } from '../table-types'\nimport { Button, ComboBox, Input, ListBox, ListBoxItem, Popover, Select } from 'react-aria-components'\nimport React from 'react'\n\ninterface TableSelectFilterProps {\n table: StandardTable\n label: string\n name: string\n options: { label: string; value: any; className?: string; icon?: ReactNode }[]\n value: any\n autoComplete?: boolean\n}\n\nexport function TableSelectFilter({ table, label, name, options, value, autoComplete }: TableSelectFilterProps) {\n const setFilter = (newValue: any) => {\n const toggledValue = newValue === value ? null : newValue\n table.setFilters((prev: Record<string, any>) => ({ ...prev, [name]: toggledValue }))\n }\n\n if (autoComplete) {\n return <AutoCompleteFilter label={label} options={options} value={value} onSelect={setFilter} />\n }\n\n return <DropdownFilter label={label} options={options} value={value} onSelect={setFilter} />\n}\n\nfunction DropdownFilter({\n label,\n options,\n value,\n onSelect\n}: {\n label: string\n options: TableSelectFilterProps['options']\n value: any\n onSelect: (value: any) => void\n}) {\n return (\n <Select\n aria-label={label}\n selectedKey={value != null ? String(value) : null}\n onSelectionChange={(key) => {\n if (key === '__clear__') return onSelect(null)\n const option = options.find((o) => String(o.value) === String(key))\n onSelect(option?.value ?? null)\n }}\n >\n <Button\n className={twMerge(\n 'text-sm px-2 h-full flex text-gray-500 focus:outline-none whitespace-pre',\n 'rounded-full ring-1 ring-black/10 data-[pressed]:scale-[.98]',\n 'my-1',\n value != null ? 'text-blue-600 font-medium' : ''\n )}\n >\n {label}\n <Icon name=\"chevron-down\" className=\"ml-1 h-5 w-5\" />\n </Button>\n <Popover offset={4} className=\"w-56 rounded-md shadow-lg ring-1 ring-black/10 bg-white z-20\">\n <ListBox className=\"outline-none max-h-60 overflow-y-auto py-1\">\n {value != null && (\n <ListBoxItem\n id=\"__clear__\"\n textValue=\"Clear\"\n className={({ isFocused }) =>\n twMerge(\n 'text-sm px-4 py-2 cursor-pointer outline-none border-b border-gray-100',\n isFocused ? 'text-white bg-primary-500' : 'text-gray-500'\n )\n }\n >\n Clear filter\n </ListBoxItem>\n )}\n {options.map((o) => (\n <FilterOption key={o.label} option={o} selected={value === o.value} />\n ))}\n </ListBox>\n </Popover>\n </Select>\n )\n}\n\nexport function FilterOption({\n option,\n selected\n}: {\n option: { label: string; value: any; className?: string; icon?: ReactNode }\n selected: boolean\n}) {\n return (\n <ListBoxItem\n id={String(option.value)}\n textValue={option.label}\n className={({ isFocused }) =>\n twMerge(\n 'group items-center text-sm px-4 py-2 relative flex justify-between w-full cursor-pointer outline-none',\n isFocused ? 'text-white bg-primary-500' : 'text-gray-900',\n option.className\n )\n }\n >\n {({ isFocused }) => (\n <>\n <div\n className={twMerge(\n selected ? 'font-semibold' : 'font-normal',\n option.icon ? 'flex gap-1 items-center' : 'block'\n )}\n >\n {option.icon}\n {option.label}\n </div>\n {selected ? (\n <div className={twMerge(isFocused ? 'text-white' : 'text-primary-500', 'flex items-center')}>\n <Icon name=\"check\" className=\"h-5 w-5\" aria-hidden=\"true\" />\n </div>\n ) : null}\n </>\n )}\n </ListBoxItem>\n )\n}\n\ninterface AutoCompleteFilterProps {\n label: string\n options: { label: string; value: any; className?: string; icon?: ReactNode }[]\n value: any\n onSelect: (value: any) => void\n}\n\nfunction AutoCompleteFilter({ label, options, value, onSelect }: AutoCompleteFilterProps) {\n const [query, setQuery] = React.useState('')\n\n const filteredOptions =\n query === ''\n ? options\n : options.filter((o) => {\n const q = query.toLowerCase()\n return o.label.toLowerCase().includes(q) || String(o.value).toLowerCase().includes(q)\n })\n\n return (\n <ComboBox\n aria-label={label}\n inputValue={query}\n onInputChange={setQuery}\n selectedKey={value != null ? String(value) : null}\n onSelectionChange={(key) => {\n if (key === null) return\n if (key === '__clear__') {\n onSelect(null)\n setQuery('')\n return\n }\n const option = options.find((o) => String(o.value) === String(key))\n onSelect(option?.value ?? null)\n setQuery('')\n }}\n menuTrigger=\"focus\"\n >\n <div className=\"relative inline-block text-left\">\n <Button\n className={twMerge(\n 'text-sm px-2 h-full flex text-gray-500 focus:outline-none whitespace-pre',\n 'rounded-full ring-1 ring-black/10 data-[pressed]:scale-[.98]',\n 'my-1',\n value != null ? 'text-blue-600 font-medium' : ''\n )}\n >\n {label}\n <Icon name=\"chevron-down\" className=\"ml-1 h-5 w-5\" />\n </Button>\n </div>\n <Popover offset={4} className=\"w-56 rounded-md shadow-lg ring-1 ring-black/10 bg-white z-20\">\n <div className=\"flex p-2\">\n <Input\n className={twMerge(\n 'text-sm block rounded-md focus:ring-0 border-0 focus:outline-none px-0 py-0 w-0 grow min-w-[30px]',\n 'py-1 px-1'\n )}\n placeholder=\"Filter...\"\n autoFocus\n />\n </div>\n <ListBox className=\"outline-none max-h-60 overflow-y-auto py-1\">\n {value != null && (\n <ListBoxItem\n id=\"__clear__\"\n textValue=\"Clear\"\n className={({ isFocused }) =>\n twMerge(\n 'text-sm px-4 py-2 cursor-pointer outline-none border-b border-gray-100',\n isFocused ? 'text-white bg-primary-500' : 'text-gray-500'\n )\n }\n >\n Clear filter\n </ListBoxItem>\n )}\n {filteredOptions.map((o) => (\n <FilterOption key={o.label} option={o} selected={value === o.value} />\n ))}\n </ListBox>\n </Popover>\n </ComboBox>\n )\n}\n"},{"name":"components/table/table-filters/sort.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { twMerge } from 'tailwind-merge'\nimport { Icon } from '../../icon'\nimport React from 'react'\nimport type { StandardTable } from '../table-types'\nimport { Button, ListBox, ListBoxItem, Popover, Select } from 'react-aria-components'\n\nexport interface TableSelectSortProps {\n table: StandardTable\n value: string | null\n options: { value: string; label: string }[]\n search: string | null\n}\n\nexport function TableSelectSort({ table, value, options, search }: TableSelectSortProps) {\n const optionsWithRelevant = React.useMemo(() => {\n return search ? [{ label: 'Most Relevant', value: 'mostRelevant' }, ...options] : options\n }, [options, search])\n\n return (\n <Select\n aria-label=\"Sort\"\n selectedKey={value ?? null}\n onSelectionChange={(key) => {\n table.setSort(key === '__clear__' ? null : key ? String(key) : null)\n }}\n >\n <Button\n className={twMerge(\n 'text-sm px-2 h-full flex text-gray-500',\n 'rounded-full ring-1 ring-black/10 data-[pressed]:scale-[.98]',\n 'my-1'\n )}\n >\n Sort\n <Icon name=\"chevron-down\" className=\"ml-1 h-5 w-5\" />\n </Button>\n <Popover offset={4} className=\"w-56 rounded-md shadow-lg ring-1 ring-black/10 bg-white z-20\">\n <ListBox className=\"outline-none max-h-60 overflow-y-auto py-1\">\n {value != null && (\n <ListBoxItem\n key=\"__clear__\"\n id=\"__clear__\"\n textValue=\"Clear\"\n className={({ isFocused }) =>\n twMerge(\n 'text-sm px-4 py-2 cursor-pointer outline-none border-b border-gray-100',\n isFocused ? 'text-white bg-primary-500' : 'text-gray-500'\n )\n }\n >\n Clear sort\n </ListBoxItem>\n )}\n {optionsWithRelevant.map((o) => {\n const selected = value === o.value\n return (\n <ListBoxItem\n key={o.value}\n id={o.value}\n textValue={o.label}\n className={({ isFocused }) =>\n twMerge(\n 'group items-center text-sm px-4 py-2 relative flex justify-between w-full cursor-pointer outline-none',\n isFocused ? 'text-white bg-primary-500' : 'text-gray-900'\n )\n }\n >\n {({ isFocused }) => (\n <>\n <span className={twMerge(selected ? 'font-semibold' : 'font-normal', 'block')}>{o.label}</span>\n {selected ? (\n <span className={twMerge(isFocused ? 'text-white' : 'text-primary-500', 'flex items-center')}>\n <Icon name=\"check\" className=\"h-5 w-5\" aria-hidden=\"true\" />\n </span>\n ) : null}\n </>\n )}\n </ListBoxItem>\n )\n })}\n </ListBox>\n </Popover>\n </Select>\n )\n}\n"},{"name":"components/table/table-filters/date-range-filter.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { twMerge } from 'tailwind-merge'\nimport React from 'react'\nimport { dateFns, type Iso } from 'iso-fns'\nimport { Button } from '../../button'\nimport type { StandardTable } from '../table-types'\nimport { Button as AriaButton, Dialog, DialogTrigger, Popover } from 'react-aria-components'\nimport { Icon } from '../../icon'\nimport { DateRangePicker, type DateRangeRootAdditionalProps } from '../../date-range'\n\nexport function TableDateRangeFilter({\n table,\n label,\n value,\n name,\n options\n}: {\n table: StandardTable\n label: string\n name: string\n value: null | { startDate: Iso.Date; endDate: Iso.Date }\n options?: Partial<\n Pick<DateRangeRootAdditionalProps, 'shouldDisableDate' | 'shouldDisableEndDate' | 'shouldDisableStartDate'>\n > & {\n quickPresets?: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]\n }\n}) {\n const [startDate, endDate] = value ? [value.startDate, value.endDate] : [null, null]\n\n return (\n <DialogTrigger>\n <AriaButton\n className={twMerge(\n 'text-sm px-2 h-full flex text-gray-500 focus:outline-none whitespace-pre',\n 'rounded-full border data-[pressed]:scale-[.98]',\n 'md:my-3',\n value ? 'text-primary-400 font-semibold' : ''\n )}\n >\n {label} {startDate && endDate ? getTableDateRangeFilterValue({ startDate, endDate }, options?.quickPresets) : ''}\n <Icon name=\"chevron-down\" className=\"ml-1 h-5 w-5\" />\n </AriaButton>\n <Popover offset={4} className=\"z-20 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 bg-white\">\n <Dialog className=\"outline-none\">\n {({ close }) => (\n <TableDateRangeFilterInner\n table={table}\n filterName={name}\n closePopover={close}\n defaultStart={startDate}\n defaultEnd={endDate}\n shouldDisableDate={options?.shouldDisableDate ?? (() => false)}\n shouldDisableEndDate={options?.shouldDisableEndDate ?? (() => false)}\n shouldDisableStartDate={options?.shouldDisableStartDate ?? (() => false)}\n quickPresets={options?.quickPresets}\n />\n )}\n </Dialog>\n </Popover>\n </DialogTrigger>\n )\n}\n\nfunction TableDateRangeFilterInner({\n table,\n filterName,\n closePopover,\n defaultStart,\n defaultEnd,\n shouldDisableDate,\n shouldDisableEndDate,\n shouldDisableStartDate,\n quickPresets\n}: {\n table: StandardTable\n filterName: string\n closePopover(): void\n quickPresets?: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]\n} & DateRangeRootAdditionalProps) {\n const [start, setStart] = React.useState<DateRangeRootAdditionalProps['defaultStart']>(defaultStart)\n const [end, setEnd] = React.useState<DateRangeRootAdditionalProps['defaultEnd']>(defaultEnd)\n const [currentField, setCurrentField] = React.useState<'start' | 'end' | null>(null)\n\n const isPresetAvailable = !!quickPresets?.length\n\n const applyFilter = (dateRange: { startDate: Iso.Date; endDate: Iso.Date }) => {\n table.setFilters((prev: Record<string, any>) => ({ ...prev, [filterName]: dateRange }))\n }\n\n const onQuickPresetClick = (presetValue: { startDate: Iso.Date; endDate: Iso.Date }) => {\n setCurrentField(null)\n closePopover()\n applyFilter(presetValue)\n }\n\n // Note: DateRangePicker and its useDateRangePicker hook need to be adapted\n // from headless-primitives. For now we pass through the same shape.\n const pickerControls = {\n start: start ?? null,\n end: end ?? null,\n onStartChange(v: Iso.Date | null, f: 'start' | 'end' | null) {\n setStart(v)\n setCurrentField(f)\n },\n onEndChange(v: Iso.Date | null, f: 'start' | 'end' | null) {\n setEnd(v)\n setCurrentField(f)\n },\n currentField,\n shouldDisableDate,\n shouldDisableEndDate,\n shouldDisableStartDate\n }\n\n return (\n <div className=\"flex bg-white overflow-auto rounded-md text-base sm:text-sm\">\n {isPresetAvailable ? (\n <QuickPresetPanel\n start={start}\n end={end}\n containerClassName=\"border-solid border-r border-gray-200 py-2 overflow-y-auto sm:min-w-[200px] hidden sm:block\"\n className={twMerge('px-4 py-2 w-full text-left')}\n quickPresets={quickPresets}\n onQuickPresetClick={onQuickPresetClick}\n />\n ) : null}\n <div>\n <div className=\"px-3 py-3\">\n <DateRangePicker {...pickerControls} />\n </div>\n {isPresetAvailable ? (\n <QuickPresetPanel\n start={start}\n end={end}\n containerClassName=\"border-solid border-y border-gray-200 p-2 flex gap-1 flex-wrap sm:hidden\"\n className={twMerge('px-3 py-1 rounded-full border')}\n quickPresets={quickPresets}\n onQuickPresetClick={onQuickPresetClick}\n />\n ) : null}\n <div className=\"p-2 float-right flex gap-2\">\n <Button variant=\"soft\" onClick={closePopover}>\n Cancel\n </Button>\n <Button\n onClick={() => {\n closePopover()\n if (start && end && start <= end) {\n applyFilter({ startDate: start as Iso.Date, endDate: end as Iso.Date })\n }\n }}\n >\n Apply\n </Button>\n </div>\n </div>\n </div>\n )\n}\n\nfunction QuickPresetPanel({\n start,\n end,\n quickPresets,\n onQuickPresetClick,\n className,\n containerClassName\n}: {\n start?: string | null\n end?: string | null\n quickPresets: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]\n onQuickPresetClick: (value: { startDate: Iso.Date; endDate: Iso.Date }) => void\n className?: string\n containerClassName?: string\n}) {\n return (\n <div className={containerClassName}>\n {quickPresets.map(({ label, value }) => {\n const [presetStartDate, presetEndDate] = [value.startDate, value.endDate]\n return (\n <button\n key={label}\n className={twMerge(\n 'text-sm cursor-pointer',\n className,\n 'text-gray-900 bg-white text-nowrap',\n 'hover:text-white hover:bg-primary-500',\n 'focus:text-white focus:bg-primary-500',\n presetStartDate === start && presetEndDate === end && 'bg-primary-500 text-white'\n )}\n onClick={() => onQuickPresetClick(value)}\n >\n {label}\n </button>\n )\n })}\n </div>\n )\n}\n\nexport function getTableDateRangeFilterValue(\n value: { startDate: Iso.Date; endDate: Iso.Date },\n quickPreset?: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]\n) {\n if (quickPreset) {\n const preset = quickPreset.find((p) => p.value.startDate === value.startDate && p.value.endDate === value.endDate)\n if (preset) {\n return preset.label\n }\n }\n if (dateFns.getYear(value.startDate) !== dateFns.getYear(value.endDate)) {\n return getFormattedDateRange(value, 'MMM dd yyyy')\n }\n if (\n dateFns.getYear(value.startDate) === dateFns.getYear(dateFns.now()) &&\n dateFns.getYear(value.endDate) === dateFns.getYear(dateFns.now())\n ) {\n return getFormattedDateRange(value, 'MMM dd')\n }\n return getFormattedDateRange(value, 'MMM dd yyyy')\n}\n\nfunction getFormattedDateRange(value: { startDate: Iso.Date; endDate: Iso.Date }, format: string) {\n if (value.startDate === value.endDate) {\n return dateFns.format(value.startDate, format)\n }\n return `${dateFns.format(value.startDate, format)} - ${dateFns.format(value.endDate, format)}`\n}\n"},{"name":"components/optional-link.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { type LinkProps, Link } from 'react-router'\nimport React from 'react'\n\nexport type OptionalLinkProps = Omit<LinkProps, 'to'> & { to?: LinkProps['to']; disabled?: boolean }\n\nexport const OptionalLink = React.forwardRef<HTMLAnchorElement, OptionalLinkProps>(\n ({ to, onClick, disabled, ...props }, ref) => {\n if (to && !disabled) {\n return <Link to={to} onClick={onClick} {...props} ref={ref} />\n } else {\n return <span {...props} ref={ref} />\n }\n }\n)\nOptionalLink.displayName = 'OptionalLink'\n"},{"name":"components/date-range.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport * as React from 'react'\nimport {\n RangeCalendar,\n CalendarGrid,\n CalendarCell,\n Heading,\n CalendarGridHeader,\n CalendarHeaderCell,\n CalendarGridBody,\n Button,\n I18nProvider\n} from 'react-aria-components'\nimport { twMerge } from 'tailwind-merge'\nimport { parseDate, type CalendarDate, type DateValue } from '@internationalized/date'\nimport type { Iso } from 'iso-fns'\nimport { Icon } from './icon'\n\nexport type DateRangeRootAdditionalProps = {\n defaultStart: Iso.Date | null\n defaultEnd: Iso.Date | null\n shouldDisableDate: (date: string) => boolean\n shouldDisableStartDate: (date: string) => boolean\n shouldDisableEndDate: (date: string) => boolean\n}\n\ntype DateRangePickerProps = {\n start: Iso.Date | null\n end: Iso.Date | null\n onStartChange: (value: Iso.Date | null, field: 'start' | 'end' | null) => void\n onEndChange: (value: Iso.Date | null, field: 'start' | 'end' | null) => void\n currentField: 'start' | 'end' | null\n shouldDisableDate?: (date: string) => boolean\n shouldDisableStartDate?: (date: string) => boolean\n shouldDisableEndDate?: (date: string) => boolean\n}\n\nexport function DateRangePicker(props: DateRangePickerProps) {\n const ariaValue = React.useMemo(() => {\n if (!props.start || !props.end) return null\n try {\n return { start: parseDate(props.start), end: parseDate(props.end) }\n } catch {\n return null\n }\n }, [props.start, props.end])\n\n const handleChange = (range: { start: CalendarDate; end: CalendarDate } | null) => {\n if (!range) {\n props.onStartChange(null, null)\n props.onEndChange(null, null)\n return\n }\n const start = range.start.toString() as Iso.Date\n const end = range.end.toString() as Iso.Date\n props.onStartChange(start, 'end')\n props.onEndChange(end, null)\n }\n\n const isDateUnavailable = React.useCallback(\n (date: DateValue) => {\n const dateStr = date.toString()\n return props.shouldDisableDate?.(dateStr) ?? false\n },\n [props.shouldDisableDate]\n )\n\n return (\n <I18nProvider locale=\"en\">\n <RangeCalendar\n value={ariaValue}\n onChange={handleChange}\n isDateUnavailable={isDateUnavailable}\n visibleDuration={{ months: 2 }}\n className=\"text-sm\"\n >\n <header className=\"flex items-center justify-between mb-2 px-1\">\n <Button slot=\"previous\" className=\"p-1 hover:bg-neutral-100 rounded cursor-pointer\">\n <Icon name=\"chevron-left\" />\n </Button>\n <Heading className=\"text-sm font-semibold text-neutral-900 flex-1 text-center\" />\n <Button slot=\"next\" className=\"p-1 hover:bg-neutral-100 rounded cursor-pointer\">\n <Icon name=\"chevron-right\" />\n </Button>\n </header>\n <div className=\"flex gap-4\">\n <CalendarGrid>\n <CalendarGridHeader>\n {(day) => (\n <CalendarHeaderCell className=\"text-xs text-neutral-500 font-medium w-8 h-8\">{day}</CalendarHeaderCell>\n )}\n </CalendarGridHeader>\n <CalendarGridBody>\n {(date) => (\n <CalendarCell\n date={date}\n className={({ isSelected, isSelectionStart, isSelectionEnd, isOutsideMonth, isDisabled, isUnavailable }) =>\n twMerge(\n 'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center',\n 'hover:bg-neutral-100',\n isOutsideMonth && 'text-neutral-300',\n (isDisabled || isUnavailable) && 'text-neutral-300 cursor-not-allowed',\n isSelected && !isSelectionStart && !isSelectionEnd && 'bg-primary-500/20',\n (isSelectionStart || isSelectionEnd) && 'bg-primary-500 text-white hover:bg-primary-500'\n )\n }\n />\n )}\n </CalendarGridBody>\n </CalendarGrid>\n <CalendarGrid offset={{ months: 1 }}>\n <CalendarGridHeader>\n {(day) => (\n <CalendarHeaderCell className=\"text-xs text-neutral-500 font-medium w-8 h-8\">{day}</CalendarHeaderCell>\n )}\n </CalendarGridHeader>\n <CalendarGridBody>\n {(date) => (\n <CalendarCell\n date={date}\n className={({ isSelected, isSelectionStart, isSelectionEnd, isOutsideMonth, isDisabled, isUnavailable }) =>\n twMerge(\n 'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center',\n 'hover:bg-neutral-100',\n isOutsideMonth && 'text-neutral-300',\n (isDisabled || isUnavailable) && 'text-neutral-300 cursor-not-allowed',\n isSelected && !isSelectionStart && !isSelectionEnd && 'bg-primary-500/20',\n (isSelectionStart || isSelectionEnd) && 'bg-primary-500 text-white hover:bg-primary-500'\n )\n }\n />\n )}\n </CalendarGridBody>\n </CalendarGrid>\n </div>\n </RangeCalendar>\n </I18nProvider>\n )\n}\n"},{"name":"utils/use-query-state.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { useLocation, useNavigation, useSearchParams as useSearchParamsRR } from 'react-router'\nimport { dateFns, dateTimeFns, durationFns, type Iso } from 'iso-fns'\nimport React from 'react'\nimport { useStableAccessor } from './use-stable-accessor'\n\n// ────────────────────────────────────────────────────────────────\n// Types\n// ────────────────────────────────────────────────────────────────\n\ninterface QueryStateNavigateOptions {\n replace?: boolean\n preventScrollReset?: boolean\n /** When true, updates the URL via `history.pushState`/`replaceState` without triggering a React Router navigation. */\n shallow?: boolean\n}\n\ninterface CodecDef<T> {\n name: string\n defaultValue?: T\n ['~type']: T\n /** When true, the param is removed from the URL when the value matches `defaultValue`. */\n clearOnDefault?: boolean\n}\n\ninterface Codec<Def extends CodecDef<any>> {\n _def: Def\n encode(value: Def['~type']): string | null\n decode(value: string | null): Def['~type']\n}\n\ninterface CodecConfigurator<Def extends CodecDef<any>> extends Codec<Def> {\n default(defaultValue: Def['~type']): CodecConfigurator<Def>\n clearOnDefault(clear: boolean): CodecConfigurator<Def>\n}\n\nfunction makeConfigurator<Def extends CodecDef<any>>(codec: Codec<Def>): CodecConfigurator<Def> {\n return {\n ...codec,\n default(defaultValue) {\n return makeConfigurator({ ...codec, _def: { ...codec._def, defaultValue } })\n },\n clearOnDefault(clear) {\n return makeConfigurator({ ...codec, _def: { ...codec._def, clearOnDefault: clear } })\n }\n }\n}\n\n// ────────────────────────────────────────────────────────────────\n// Codec defs\n// ────────────────────────────────────────────────────────────────\n\ninterface NoopDef extends CodecDef<string | null> {\n name: 'noop'\n ['~type']: string | null\n}\n\ninterface StringDef extends CodecDef<string | null> {\n name: 'string'\n ['~type']: string | null\n}\n\ninterface ValidatedDef<T extends string> extends CodecDef<T | null> {\n name: string\n ['~type']: T | null\n isValid: (v: string) => v is T\n}\n\ninterface RelativeDateTimeRangeDef extends CodecDef<Iso.Duration | { start: Iso.DateTime; end: Iso.DateTime } | null> {\n name: 'relativeDateTimeRange'\n ['~type']: Iso.Duration | { start: Iso.DateTime; end: Iso.DateTime } | null\n}\n\ninterface IntDef extends CodecDef<number | null> {\n name: 'int'\n ['~type']: number | null\n}\n\ninterface FloatDef extends CodecDef<number | null> {\n name: 'float'\n ['~type']: number | null\n}\n\ninterface HexDef extends CodecDef<number | null> {\n name: 'hex'\n ['~type']: number | null\n}\n\ninterface IndexDef extends CodecDef<number | null> {\n name: 'index'\n ['~type']: number | null\n}\n\ninterface BooleanDef extends CodecDef<boolean | null> {\n name: 'boolean'\n ['~type']: boolean | null\n}\n\ninterface EnumDef<T extends string> extends CodecDef<T | null> {\n name: 'enum'\n ['~type']: T | null\n values: readonly T[]\n}\n\ninterface EnumArrayDef<T extends string> extends CodecDef<T[]> {\n name: 'enumArray'\n ['~type']: T[]\n values: readonly T[]\n}\n\n// ────────────────────────────────────────────────────────────────\n// Codec factories\n// ────────────────────────────────────────────────────────────────\n\n/** Pass-through — no transformation, value is `string | null`. */\nfunction noop(def: Omit<NoopDef, '~type'>): Codec<NoopDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => value,\n decode: (value) => value\n }\n}\n\n/** String codec — pass-through with `string | null` type. */\nfunction string(def: Omit<StringDef, '~type'>): Codec<StringDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => value,\n decode: (value) => value\n }\n}\n\n/** Validates a string against a guard function. Used for date, dateTime, duration. */\nfunction validated<T extends string>(def: Omit<ValidatedDef<T>, '~type'>): Codec<ValidatedDef<T>> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => (value != null && def.isValid(value) ? value : null),\n decode: (value) => (value !== null && def.isValid(value) ? value : null)\n }\n}\n\n/**\n * Either an ISO duration (relative range) or a `start~end` pair of ISO\n * date-times (absolute range). Useful for \"last 7 days\" vs a custom window.\n */\nfunction relativeDateTimeRange(def: Omit<RelativeDateTimeRangeDef, '~type'>): Codec<RelativeDateTimeRangeDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => {\n if (value == null) return null\n if (durationFns.isValid(value)) return value\n if (typeof value === 'object' && 'start' in value && 'end' in value) {\n return dateTimeFns.isValid(value.start) && dateTimeFns.isValid(value.end) ? `${value.start}~${value.end}` : null\n }\n return null\n },\n decode: (value) => {\n if (value === null) return null\n if (durationFns.isValid(value)) return value\n const [start, end] = value.split('~')\n return dateTimeFns.isValid(start) && dateTimeFns.isValid(end) ? { start, end } : null\n }\n }\n}\n\n/** Integer (base-10). Non-integer or missing values decode to `null`. */\nfunction int(def: Omit<IntDef, '~type'>): Codec<IntDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => (value != null && Number.isInteger(value) ? value.toString() : null),\n decode: (value) => {\n if (value === null) return null\n const num = Number(value)\n return Number.isInteger(num) ? num : null\n }\n }\n}\n\n/** Finite floating-point number. `NaN` / `Infinity` decode to `null`. */\nfunction float(def: Omit<FloatDef, '~type'>): Codec<FloatDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => (value != null && Number.isFinite(value) ? value.toString() : null),\n decode: (value) => {\n if (value === null) return null\n const num = Number(value)\n return Number.isFinite(num) ? num : null\n }\n }\n}\n\n/** Hexadecimal integer (with or without `0x` prefix). Stored without prefix. */\nfunction hex(def: Omit<HexDef, '~type'>): Codec<HexDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => (typeof value === 'number' && Number.isFinite(value) ? value.toString(16) : null),\n decode: (value) => {\n if (value === null) return null\n const body = value.trim().toLowerCase().replace(/^0x/, '')\n if (body.length === 0 || /[^0-9a-f]/i.test(body)) return null\n const num = parseInt(body, 16)\n return Number.isFinite(num) ? num : null\n }\n }\n}\n\n/**\n * Zero-based index stored as a **1-based** number in the URL.\n * `?page=1` decodes to `0`, `0` encodes to `\"1\"`.\n */\nfunction index(def: Omit<IndexDef, '~type'>): Codec<IndexDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => (typeof value === 'number' && Number.isInteger(value) && value >= 0 ? (value + 1).toString() : null),\n decode: (value) => {\n if (value === null) return null\n const num = Number(value)\n return Number.isInteger(num) && num >= 1 ? num - 1 : null\n }\n }\n}\n\n/** Boolean accepting common truthy/falsy strings (`1`, `true`, `yes`, `on`, etc). Encodes as `\"1\"` / `\"0\"`. */\nfunction boolean(def: Omit<BooleanDef, '~type'>): Codec<BooleanDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => (value == null ? null : value ? '1' : '0'),\n decode: (value) => {\n if (value === null) return null\n const v = value.trim().toLowerCase()\n if (v === '1' || v === 'true' || v === 't' || v === 'yes' || v === 'y' || v === 'on') return true\n if (v === '0' || v === 'false' || v === 'f' || v === 'no' || v === 'n' || v === 'off') return false\n return null\n }\n }\n}\n\n/** Constrained to one of the provided `values`. Anything else decodes to `null`. */\nfunction enumCodec<T extends string>(def: Omit<EnumDef<T>, '~type'>): Codec<EnumDef<T>> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => value ?? null,\n decode: (value) => {\n if (value === null) return null\n return (def.values as readonly string[]).includes(value) ? (value as T) : null\n }\n }\n}\n\n/**\n * Comma-separated list constrained to the provided `values`.\n * Decodes to `T[]` (empty array when missing). Encodes in the canonical\n * order of `values` so the same selection always produces the same URL.\n */\nfunction enumArray<T extends string>(def: Omit<EnumArrayDef<T>, '~type'>): Codec<EnumArrayDef<T>> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => {\n if (!value.length) return null\n return value\n .sort((v1, v2) => def.values.indexOf(v1) - def.values.indexOf(v2))\n .map((v) => encodeURIComponent(v))\n .join(',')\n },\n decode: (value) => {\n if (value === null) return []\n return value\n .split(',')\n .map((v) => decodeURIComponent(v))\n .filter((v) => def.values.includes(v as T)) as T[]\n }\n }\n}\n\n// ────────────────────────────────────────────────────────────────\n// Serialize / Deserialize\n// ────────────────────────────────────────────────────────────────\n\n/** Extract the serializable def from a codec. */\nfunction serializeCodec<Def extends CodecDef<any>>(codec: Codec<Def>): Def {\n return codec._def\n}\n\ntype CodecFactory = (def: any) => Codec<any>\n\nconst defaultFactories: Record<string, CodecFactory> = {\n noop,\n string,\n date: validated,\n dateTime: validated,\n duration: validated,\n relativeDateTimeRange,\n int,\n float,\n hex,\n index,\n boolean,\n enum: enumCodec,\n enumArray\n}\n\n/** Reconstruct a codec from a serialized def. */\nfunction deserializeCodec<Def extends CodecDef<any>>(\n def: Def,\n factories: Record<string, CodecFactory> = defaultFactories\n): Codec<Def> {\n const factory = factories[def.name]\n if (!factory) throw new Error(`Unknown codec: ${def.name}`)\n return factory(def) as Codec<Def>\n}\n\ntype SerializedCodec<C extends Codec<CodecDef<any>>> = C['_def']\n\ntype SerializedLoader<L extends QueryStateLoader> = { [K in keyof L]: SerializedCodec<L[K]> }\n\n/** Serialize a loader into a plain object of defs, keyed by param name. */\nfunction serializeLoader<L extends QueryStateLoader>(loader: L): SerializedLoader<L> {\n return Object.fromEntries(\n Object.entries(loader).map(([name, codec]) => [name, serializeCodec(codec)])\n ) as SerializedLoader<L>\n}\n\n/** Reconstruct a loader from a serialized object of defs. */\nfunction deserializeLoader<L extends QueryStateLoader>(\n defs: SerializedLoader<L>,\n factories: Record<string, CodecFactory> = defaultFactories\n): L {\n return Object.fromEntries(\n Object.entries(defs).map(([name, def]) => [name, deserializeCodec(def as CodecDef<any>, factories)])\n ) as L\n}\n\n// ────────────────────────────────────────────────────────────────\n// Built-in codecs\n// ────────────────────────────────────────────────────────────────\n\n/**\n * Library of built-in codecs for common URL search-param types.\n *\n * Each codec converts between a URL string and a typed value, returning `null`\n * for missing or invalid params. Chain `.default(v)` to substitute a\n * fallback, or `.clearOnDefault(true)` to remove the param when it matches the default.\n */\nconst codecs = {\n /** Pass-through — no transformation, value is `string | null`. */\n noop: () => makeConfigurator(noop({ name: 'noop' })),\n\n /** String codec — pass-through defaulting to `''` (never null). */\n string: () => makeConfigurator(string({ name: 'string' })).default(''),\n\n /** ISO date (`YYYY-MM-DD`). Invalid strings decode to `null`. */\n date: () => makeConfigurator(validated({ name: 'date', isValid: dateFns.isValid })),\n\n /** ISO date-time (`YYYY-MM-DDTHH:mm:ss`). Invalid strings decode to `null`. */\n dateTime: () => makeConfigurator(validated({ name: 'dateTime', isValid: dateTimeFns.isValid })),\n\n /** ISO duration (`P1DT2H`, etc). Invalid strings decode to `null`. */\n duration: () => makeConfigurator(validated({ name: 'duration', isValid: durationFns.isValid })),\n\n /**\n * Either an ISO duration (relative range) or a `start~end` pair of ISO\n * date-times (absolute range). Useful for \"last 7 days\" vs a custom window.\n */\n relativeDateTimeRange: () => makeConfigurator(relativeDateTimeRange({ name: 'relativeDateTimeRange' })),\n\n /** Integer (base-10). Non-integer or missing values decode to `null`. */\n int: () => makeConfigurator(int({ name: 'int' })),\n\n /** Finite floating-point number. `NaN` / `Infinity` decode to `null`. */\n float: () => makeConfigurator(float({ name: 'float' })),\n\n /** Hexadecimal integer (with or without `0x` prefix). Stored without prefix. */\n hex: () => makeConfigurator(hex({ name: 'hex' })),\n\n /**\n * Zero-based index stored as a **1-based** number in the URL.\n * `?page=1` decodes to `0`, `0` encodes to `\"1\"`.\n */\n index: () => makeConfigurator(index({ name: 'index' })),\n\n /** Boolean accepting common truthy/falsy strings (`1`, `true`, `yes`, `on`, etc). Encodes as `\"1\"` / `\"0\"`. */\n boolean: () => makeConfigurator(boolean({ name: 'boolean' })),\n\n /** Constrained to one of the provided `values`. Anything else decodes to `null`. */\n enum: <T extends string>(values: readonly T[]) => makeConfigurator(enumCodec({ name: 'enum', values })),\n\n /**\n * Comma-separated list constrained to the provided `values`.\n * Decodes to `T[]` (empty array when missing). Encodes in the canonical\n * order of `values` so the same selection always produces the same URL.\n */\n enumArray: <T extends string>(values: readonly T[]) => makeConfigurator(enumArray({ name: 'enumArray', values }))\n}\n\n// ────────────────────────────────────────────────────────────────\n// Loader types\n// ────────────────────────────────────────────────────────────────\n\n/** A map of param names to codecs, defining the shape of search-param state. */\ntype QueryStateLoader<T extends { [key: string]: any } = { [key: string]: any }> = {\n [key in keyof T]: Codec<CodecDef<T[key]>>\n}\n\ntype ExtractTypeFromLoader<L extends QueryStateLoader> = {\n [key in keyof L]: L[key] extends Codec<infer S> ? S['~type'] : never\n}\n\n// ────────────────────────────────────────────────────────────────\n// Internal helpers\n// ────────────────────────────────────────────────────────────────\n\nfunction getQueryParam<T>(naturalValue: string | null, codec: Codec<CodecDef<T>>): T {\n try {\n const decoded = codec.decode(naturalValue)\n return (decoded ?? codec?._def?.defaultValue ?? null) as T\n } catch {\n return (codec?._def?.defaultValue ?? null) as T\n }\n}\n\nfunction setQueryParam<V>({\n searchParams,\n name,\n value,\n codec\n}: {\n searchParams: URLSearchParams\n name: string\n value: V\n codec: Codec<CodecDef<V>>\n}): void {\n const encoded = codec.encode(value)\n if (\n encoded === null ||\n (codec._def?.defaultValue != null && codec._def?.clearOnDefault && encoded === codec.encode(codec._def?.defaultValue))\n ) {\n searchParams.delete(name)\n } else {\n searchParams.set(name, encoded)\n }\n}\n\nfunction queryParamsToObject<L extends QueryStateLoader>(params: URLSearchParams, loader: L) {\n return Object.fromEntries(\n Object.entries(loader).map(([name, codec]) => [name, getQueryParam(params.get(name), codec)])\n ) as ExtractTypeFromLoader<L>\n}\n\n// ────────────────────────────────────────────────────────────────\n// Public API\n// ────────────────────────────────────────────────────────────────\n\n/**\n * Decode search params from various sources (URL string, `Request`, `URLSearchParams`, or\n * an object with a `.search` property) using a loader. Useful in loaders/actions on the server side.\n */\nfunction getSearchParamsState<L extends QueryStateLoader>(\n searchParams: URLSearchParams | string | Request | { search: string },\n loader: L\n) {\n if (typeof searchParams === 'string') {\n return queryParamsToObject(new URL(searchParams).searchParams, loader)\n } else if ('url' in searchParams) {\n return queryParamsToObject(new URL(searchParams.url).searchParams, loader)\n } else if ('search' in searchParams) {\n return queryParamsToObject(new URLSearchParams(searchParams.search), loader)\n } else {\n return queryParamsToObject(searchParams, loader)\n }\n}\n\n/**\n * React hook that binds a single search param to component state via a codec.\n * Returns `[value, setValue]` like `React.useState`. The setter accepts a value\n * or updater function and optional {@link QueryStateNavigateOptions}.\n *\n * Reads from the pending navigation location when one is in flight so the UI\n * reflects optimistic state during transitions.\n */\nfunction useState<S>(\n name: string,\n codec: Codec<CodecDef<S>>\n): [S, (val: S | ((prevState: S) => S), navigateOpts?: QueryStateNavigateOptions) => void] {\n const [searchParamsRaw, setSearchParams] = useSearchParamsRR()\n const navigation = useNavigation()\n\n const naturalValue = React.useMemo(\n () => (navigation.location?.search ? new URLSearchParams(navigation.location.search) : searchParamsRaw).get(name),\n [name, searchParamsRaw, navigation.location]\n )\n const value = React.useMemo(\n () => getQueryParam<S>(naturalValue, codec),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [naturalValue]\n )\n\n const setValue = React.useCallback(\n (val: S | ((prevState: S) => S), navigateOpts?: QueryStateNavigateOptions) => {\n const { replace = true, preventScrollReset = false, shallow = false } = { ...codec, ...navigateOpts }\n\n function updateParams(params: URLSearchParams) {\n const newParams = new URLSearchParams(params)\n const oldValue = getQueryParam(newParams.get(name), codec)\n const newValue = typeof val === 'function' ? (val as (prevState: S) => S)(oldValue) : val\n setQueryParam({ searchParams: newParams, name, value: newValue, codec })\n return newParams\n }\n if (shallow) {\n const params = new URLSearchParams(window.location.search)\n const newUrl = [\n [window.location.pathname, updateParams(params).toString()].filter(Boolean).join('?'),\n window.location.hash\n ].join('#')\n if (replace) {\n window.history.replaceState(null, '', newUrl)\n } else {\n window.history.pushState(null, '', newUrl)\n }\n } else {\n setSearchParams(updateParams, { replace, preventScrollReset })\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [name, setSearchParams]\n )\n\n return [value, setValue] as const\n}\n\n/**\n * React hook that decodes *all* search params defined by a loader into a typed object.\n * Read-only — for read/write access to individual params, use {@link useState}.\n *\n * Reads from the pending navigation location when one is in flight.\n */\nfunction useSearchParams<L extends QueryStateLoader>(\n loader: L\n): [\n state: ExtractTypeFromLoader<L>,\n setState: (\n val: ExtractTypeFromLoader<L> | ((prevState: ExtractTypeFromLoader<L>) => ExtractTypeFromLoader<L>),\n navigateOpts?: QueryStateNavigateOptions\n ) => void\n] {\n const location = useLocation()\n const { location: pendingLocation } = useNavigation()\n\n const search = pendingLocation?.search ?? location.search\n\n const getLoader = useStableAccessor(loader)\n const [_, setSearchParamsRaw] = useSearchParamsRR()\n\n const setSearchParam = React.useCallback(\n (\n val: ExtractTypeFromLoader<L> | ((prevState: ExtractTypeFromLoader<L>) => ExtractTypeFromLoader<L>),\n navigateOpts?: QueryStateNavigateOptions\n ) => {\n const loader = getLoader()\n const { replace = true, preventScrollReset = false, shallow = false } = { ...navigateOpts }\n\n function updateParams(params: URLSearchParams) {\n const newParams = new URLSearchParams(params)\n const oldValue = queryParamsToObject(new URLSearchParams(search), loader)\n const newValue = typeof val === 'function' ? val(oldValue) : val\n Object.entries(loader).forEach(([name, codec]) =>\n setQueryParam({ searchParams: newParams, name, value: newValue[name], codec })\n )\n return newParams\n }\n if (shallow) {\n const params = new URLSearchParams(window.location.search)\n const newUrl = [\n [window.location.pathname, updateParams(params).toString()].filter(Boolean).join('?'),\n window.location.hash\n ].join('#')\n if (replace) {\n window.history.replaceState(null, '', newUrl)\n } else {\n window.history.pushState(null, '', newUrl)\n }\n } else {\n setSearchParamsRaw(updateParams, { replace, preventScrollReset })\n }\n },\n [getLoader, setSearchParamsRaw]\n )\n\n const searchParams = React.useMemo(() => queryParamsToObject(new URLSearchParams(search), loader), [search, loader])\n\n return [searchParams, setSearchParam]\n}\n\n/**\n * Identity function that provides type inference for a loader definition.\n *\n * @example\n * ```ts\n * const loader = SearchParams.createLoader({\n * page: SearchParams.codecs.int.withDefaultValue(0),\n * sort: SearchParams.codecs.enum(['name', 'date'] as const)\n * })\n * ```\n */\nfunction createLoader<L extends QueryStateLoader>(loader: L): L {\n return loader\n}\n\nexport const SearchParams = {\n codecs,\n serializeCodec,\n deserializeCodec,\n serializeLoader,\n deserializeLoader,\n createLoader,\n getSearchParamsState,\n useState,\n useSearchParams\n}\n\nexport declare namespace SearchParams {\n export {\n Codec,\n CodecDef,\n QueryStateLoader,\n ExtractTypeFromLoader,\n QueryStateNavigateOptions,\n SerializedCodec,\n SerializedLoader\n }\n}\n"}]}]
1
+ [{"name":"tw","files":[{"name":"utils/tw.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add tw --directory app\n\nimport { twMerge } from 'tailwind-merge'\nimport { composeRenderProps } from 'react-aria-components'\n\nexport function tw(...inputs: unknown[]) {\n return twMerge(inputs.flat(Infinity).filter(Boolean).join(' '))\n}\n\nexport function composeTailwindRenderProps<T>(\n className: string | ((renderProps: T) => string) | undefined,\n tailwind: string\n): string | ((renderProps: T) => string) {\n return composeRenderProps(className, (prev) => tw(tailwind, prev))\n}\n"}]},{"name":"compose-refs","files":[{"name":"utils/compose-refs.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add compose-refs --directory app\n\nimport React from 'react'\n\ntype PossibleRef<T> = React.Ref<T> | undefined\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n if (typeof ref === 'function') {\n ref(value)\n } else if (ref !== null && ref !== undefined) {\n ;(ref as React.MutableRefObject<T>).current = value\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nexport function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n return (node: T) => refs.forEach((ref) => setRef(ref, node))\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nexport function useComposedRefs<T>(...refs: PossibleRef<T>[]) {\n // eslint-disable-next-line react-hooks/exhaustive-deps\n return React.useCallback(composeRefs(...refs), refs)\n}\n"}]},{"name":"use-render-props","files":[{"name":"utils/use-render-props.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-render-props --directory app\n\nimport React from 'react'\n\ntype Prettify<T> = { [K in keyof T]: T[K] } & {}\n\ntype DefaultRenderPropKeys<Props> = Extract<'children' | 'className' | 'style', keyof Props>\n\ntype MaybeRenderProp<Value, RenderProps> = Value | ((renderProps: RenderProps) => Value)\n\nexport type WithRenderProps<\n Props extends Record<string, any>,\n RenderProps,\n Keys extends keyof Props = DefaultRenderPropKeys<Props>\n> = Prettify<{\n [K in keyof Props]: K extends Keys ? MaybeRenderProp<Props[K], RenderProps> : Props[K]\n}>\n\nexport function useRenderProps<\n Props extends Record<string, any>,\n RenderProps,\n Keys extends keyof Props = DefaultRenderPropKeys<Props>\n>(props: Props, renderProps: RenderProps, keys: Keys[]) {\n const withRenderProps = React.useMemo(\n () =>\n Object.fromEntries(\n keys\n .map((key) => {\n if (key in props && typeof props[key] === 'function') {\n return [key, props[key](renderProps)]\n } else {\n return []\n }\n })\n .filter((k) => k.length)\n ),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [...keys, renderProps, props, ...keys.map((key) => props[key])]\n )\n return { ...props, ...withRenderProps }\n}\n"}]},{"name":"colors","files":[{"name":"utils/colors.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add colors --directory app\n\nexport const COLORS = [\n '#f43f5e',\n '#3b82f6',\n '#22c55e',\n '#EAB308',\n '#8b5cf6',\n '#191970',\n '#f97316',\n '#6b7280',\n '#ec4899',\n '#06b6d4',\n '#84cc16',\n '#64748b',\n '#ffff00',\n '#6366f1',\n '#ff6347',\n '#737373',\n '#14b8a6',\n '#f59e0b',\n '#0ea5e9',\n '#ef4444',\n '#78716c',\n '#00ffff',\n '#d946ef',\n '#10b981',\n '#b22222',\n '#40e0d0',\n '#71717a',\n '#a855f7',\n '#7fff00'\n] as const\n\nexport function stringToNumber(str: string) {\n let h = 0x811c9dc5 // FNV offset basis\n for (const b of new TextEncoder().encode(str)) {\n h ^= b\n h = Math.imul(h, 0x01000193) // 16777619\n }\n return h >>> 0 // unsigned 32-bit\n}\n\nexport function getColor(seed: number) {\n return COLORS[seed % COLORS.length]\n}\n"}]},{"name":"file-input-utils","files":[{"name":"utils/file-input.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add file-input-utils --directory app\n\n/**\n * Shared utilities for file-input components.\n */\n\nexport function formatFileSize(bytes: number) {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n\nexport function checkFileType(file: File, accept: string): string | null {\n const acceptedTypes = accept.split(',').map((type) => type.trim())\n const fileType = file.type\n const fileExtension = `.${file.name.split('.').pop()?.toLowerCase() || ''}`\n\n const isValid = acceptedTypes.some((acceptType) => {\n if (acceptType.includes('/*')) {\n const prefix = acceptType.split('/')[0]\n return fileType.startsWith(`${prefix}/`)\n }\n if (acceptType.startsWith('.')) {\n return acceptType.toLowerCase() === fileExtension\n }\n return fileType === acceptType\n })\n\n return isValid ? null : `Unsupported file type. Supported types: ${acceptedTypes.join(', ')}`\n}\n\nexport function checkFileMaxSize(file: File, maxSize: number): string | null {\n if (file.size > maxSize) {\n return `File exceeded max size of ${formatFileSize(maxSize)}`\n }\n return null\n}\n\nexport function validateFileMaxSize(value: string | File | null, maxSize?: number): string | null {\n if (value instanceof File && maxSize) {\n return checkFileMaxSize(value, maxSize)\n }\n return null\n}\n\nexport function validateFileType(value: string | File | null, accept?: string): string | null {\n if (value instanceof File && accept) {\n return checkFileType(value, accept)\n }\n return null\n}\n"}]},{"name":"use-spin-delay","files":[{"name":"utils/use-spin-delay.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-spin-delay --directory app\n\nimport React from 'react'\n\nexport function useSpinDelay(loading: boolean, { delay = 500, minDuration = 200 } = {}) {\n const [show, setShow] = React.useState(false)\n const delayTimer = React.useRef<ReturnType<typeof setTimeout>>(undefined)\n const minTimer = React.useRef<ReturnType<typeof setTimeout>>(undefined)\n\n React.useEffect(() => {\n if (loading) {\n delayTimer.current = setTimeout(() => setShow(true), delay)\n } else {\n clearTimeout(delayTimer.current)\n if (show) {\n minTimer.current = setTimeout(() => setShow(false), minDuration)\n }\n }\n return () => {\n clearTimeout(delayTimer.current)\n clearTimeout(minTimer.current)\n }\n }, [loading, delay, minDuration, show])\n\n return show\n}\n"}]},{"name":"use-number-input","files":[{"name":"utils/use-number-input.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-number-input --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\n\nexport type UseNumberInputOptions = {\n value?: number | null\n defaultValue?: number | null\n onChange?: ((value: number | null) => void) | null\n minValue?: number\n maxValue?: number\n step?: number\n formatOptions?: Intl.NumberFormatOptions\n locale?: string\n isDisabled?: boolean\n isReadOnly?: boolean\n}\n\nexport type UseNumberInputResult = {\n inputValue: string\n setInputValue: (value: string) => void\n numberValue: number | null\n setNumberValue: (value: number | null) => void\n minValue?: number\n maxValue?: number\n commit: (rawValue?: string) => void\n}\n\nfunction formatNumber(value: number | null | undefined, locale: string, formatOptions?: Intl.NumberFormatOptions): string {\n if (value == null) return ''\n try {\n return new Intl.NumberFormat(locale, formatOptions).format(value)\n } catch {\n return String(value)\n }\n}\n\nfunction parseNumber(text: string, locale: string, formatOptions?: Intl.NumberFormatOptions): number | null {\n if (text === '' || text == null) return null\n\n // Get the decimal and group separators for the locale\n const parts = new Intl.NumberFormat(locale, formatOptions).formatToParts(1234.5)\n const groupSeparator = parts.find((p) => p.type === 'group')?.value ?? ','\n const decimalSeparator = parts.find((p) => p.type === 'decimal')?.value ?? '.'\n\n // Strip everything except digits, decimal separator, and minus sign\n let cleaned = text\n // Remove currency symbols, percent signs, and other literals\n for (const part of parts) {\n if (part.type === 'currency' || part.type === 'percentSign' || part.type === 'literal') {\n cleaned = cleaned.replaceAll(part.value, '')\n }\n }\n // Remove group separators\n cleaned = cleaned.replaceAll(groupSeparator, '')\n // Normalize decimal separator to '.'\n if (decimalSeparator !== '.') {\n cleaned = cleaned.replaceAll(decimalSeparator, '.')\n }\n\n cleaned = cleaned.trim()\n if (cleaned === '' || cleaned === '-') return null\n\n const num = Number(cleaned)\n\n // For percent format, Intl.NumberFormat displays 0.5 as \"50%\",\n // so we need to divide by 100 when parsing\n if (formatOptions?.style === 'percent') {\n return isNaN(num) ? null : num / 100\n }\n\n return isNaN(num) ? null : num\n}\n\nfunction clamp(value: number, min?: number, max?: number): number {\n if (min != null && value < min) return min\n if (max != null && value > max) return max\n return value\n}\n\nexport function useNumberInput(options: UseNumberInputOptions): UseNumberInputResult {\n const { minValue, maxValue, step, formatOptions, locale = 'en-US', isDisabled = false, isReadOnly = false } = options\n\n const [numberValue, setNumberValue] = HeadlessForm.useControlledState<number | null>(\n options.value !== undefined ? (options.value ?? null) : undefined!,\n options.defaultValue !== undefined ? (options.defaultValue ?? null) : null,\n options.onChange ?? undefined\n )\n\n const [inputValue, setInputValueRaw] = React.useState(() => formatNumber(numberValue, locale, formatOptions))\n\n // Keep inputValue in sync when numberValue changes externally (controlled mode)\n const prevNumberValueRef = React.useRef(numberValue)\n React.useEffect(() => {\n if (prevNumberValueRef.current !== numberValue) {\n prevNumberValueRef.current = numberValue\n setInputValueRaw(formatNumber(numberValue, locale, formatOptions))\n }\n }, [numberValue, locale, formatOptions])\n\n const setInputValue = React.useCallback(\n (text: string) => {\n if (isDisabled || isReadOnly) return\n setInputValueRaw(text)\n },\n [isDisabled, isReadOnly]\n )\n\n const commit = React.useCallback(\n (rawValue?: string) => {\n if (isDisabled || isReadOnly) return\n\n const parsed = parseNumber(rawValue ?? inputValue, locale, formatOptions)\n if (parsed == null) {\n setNumberValue(null)\n setInputValueRaw('')\n return\n }\n\n let clamped = clamp(parsed, minValue, maxValue)\n\n // Snap to step if provided\n if (step != null && minValue != null) {\n const remainder = (clamped - minValue) % step\n if (remainder !== 0) {\n clamped = clamped - remainder\n }\n }\n\n setNumberValue(clamped)\n setInputValueRaw(formatNumber(clamped, locale, formatOptions))\n },\n [inputValue, locale, formatOptions, minValue, maxValue, step, isDisabled, isReadOnly, setNumberValue]\n )\n\n return {\n inputValue,\n setInputValue,\n numberValue,\n setNumberValue,\n minValue,\n maxValue,\n commit\n }\n}\n"}]},{"name":"use-prevent-default","dependencies":["use-stable-accessor"],"files":[{"name":"utils/use-prevent-default.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-prevent-default --directory app\n\nimport React from 'react'\nimport { useStableAccessor } from './use-stable-accessor'\n\nexport function usePreventDefault<E extends React.SyntheticEvent<any>>(\n eventHandler: React.EventHandler<E> | undefined,\n preventDefault: boolean\n): React.EventHandler<E> {\n const getShouldPreventDefault = useStableAccessor(preventDefault)\n const getEventHandler = useStableAccessor(eventHandler)\n\n const myEventHandler = React.useCallback(\n (event: E) => {\n if (getShouldPreventDefault()) {\n event.preventDefault()\n event.stopPropagation()\n return\n } else {\n const eventHandler = getEventHandler()\n if (eventHandler) {\n return eventHandler(event)\n }\n }\n },\n [getEventHandler, getShouldPreventDefault]\n )\n return myEventHandler\n}\n"}]},{"name":"use-stable-accessor","files":[{"name":"utils/use-stable-accessor.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-stable-accessor --directory app\n\nimport React from 'react'\n\nexport function useStableAccessor<T>(value: T) {\n const initialPersisterKey = React.useRef<{ value: T }>({ value })\n const getValue = React.useCallback(() => {\n return initialPersisterKey.current.value\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [initialPersisterKey.current])\n initialPersisterKey.current.value = value\n return getValue\n}\n"}]},{"name":"use-tab-indicator","files":[{"name":"utils/use-tab-indicator.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-tab-indicator --directory app\n\nimport * as React from 'react'\n\n// --- Shared Tab Styles ---\n\nexport const TAB_BASE_CLASSES = 'relative px-4 py-2 text-sm cursor-default transition-colors duration-200'\nexport const TAB_INACTIVE_CLASSES = 'text-neutral-600 font-medium'\nexport const TAB_DISABLED_CLASSES = 'opacity-50 cursor-not-allowed'\n\n// --- Tab Variant ---\n\nexport type TabVariant = 'underline' | 'oval'\n\n// --- Tab Color Map ---\n\nexport const tabColorMap = {\n primary: {\n text: 'text-primary-600',\n bg: 'bg-primary-600',\n outline: 'outline-primary-600'\n },\n neutral: {\n text: 'text-neutral-800',\n bg: 'bg-neutral-800',\n outline: 'outline-neutral-800'\n }\n} as const\n\nexport type TabColor = keyof typeof tabColorMap\n\n// --- Variant-Specific Styles ---\n\nexport const tabListContainerClasses: Record<TabVariant, string> = {\n underline: 'relative',\n oval: 'relative inline-flex bg-neutral-100 rounded-lg p-1'\n}\n\nexport const tabListInnerClasses: Record<TabVariant, string> = {\n underline: 'flex space-x-1 border-b border-neutral-200',\n oval: 'flex space-x-1'\n}\n\nexport function getIndicatorClasses(variant: TabVariant, color: TabColor): string {\n if (variant === 'underline') {\n return `absolute bottom-0 h-0.5 rounded-full ${tabColorMap[color].bg}`\n }\n return 'absolute inset-y-1 rounded-md bg-white shadow-sm'\n}\n\nexport const TAB_OVAL_BASE_CLASSES = 'relative z-10 px-4 py-1.5 text-sm cursor-default transition-colors duration-200 rounded-md'\nexport const TAB_OVAL_SELECTED_CLASSES = 'text-neutral-900 font-semibold'\n\n// --- Animated Indicator Hook ---\n\nexport function useAnimatedIndicator(\n containerRef: React.RefObject<HTMLDivElement | null>,\n activeSelector: string,\n observedAttribute: string\n) {\n const indicatorRef = React.useRef<HTMLDivElement>(null)\n const hasAnimated = React.useRef(false)\n\n const updateIndicator = React.useCallback(() => {\n const container = containerRef.current\n const indicator = indicatorRef.current\n if (!container || !indicator) return\n\n const activeTab = container.querySelector(activeSelector) as HTMLElement | null\n if (activeTab) {\n const containerRect = container.getBoundingClientRect()\n const tabRect = activeTab.getBoundingClientRect()\n\n if (!hasAnimated.current) {\n indicator.style.transition = 'none'\n hasAnimated.current = true\n } else {\n indicator.style.transition = 'left 200ms ease, width 200ms ease'\n }\n\n indicator.style.left = `${tabRect.left - containerRect.left}px`\n indicator.style.width = `${tabRect.width}px`\n indicator.style.opacity = '1'\n } else {\n indicator.style.opacity = '0'\n }\n }, [containerRef, activeSelector])\n\n React.useEffect(() => {\n updateIndicator()\n\n const container = containerRef.current\n if (!container) return\n\n const observer = new MutationObserver(updateIndicator)\n observer.observe(container, { attributes: true, subtree: true, attributeFilter: [observedAttribute] })\n\n const resizeObserver = new ResizeObserver(updateIndicator)\n resizeObserver.observe(container)\n\n return () => {\n observer.disconnect()\n resizeObserver.disconnect()\n }\n }, [containerRef, updateIndicator, observedAttribute])\n\n return indicatorRef\n}\n"}]},{"name":"form-field","dependencies":["tw"],"files":[{"name":"components/helpers/form-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add form-field --directory app\n\nimport * as React from 'react'\nimport {\n Button,\n Collection,\n Group,\n Header,\n Input,\n ListBox,\n ListBoxItem,\n ListBoxSection,\n type InputProps\n} from 'react-aria-components'\nimport { composeTailwindRenderProps, tw } from '../../utils/tw'\nimport { Icon } from '../icon'\n\nconst colorClasses = { neutral: 'text-neutral-500', negative: 'text-negative-600', notice: 'text-notice-600' } as const\n\ntype LabelProps = {\n as?: React.ElementType<React.ComponentPropsWithRef<'label'>>\n} & React.ComponentPropsWithRef<'label'>\n\nconst Label = React.forwardRef<HTMLLabelElement, LabelProps>(({ as = 'label', ...props }, ref) => {\n return React.createElement(as, {\n ...props,\n ref,\n className: tw('block text-sm font-medium text-neutral-700', props.className)\n })\n})\nLabel.displayName = 'Label'\n\nfunction FormFieldHelpText(props: {\n id: string\n isInvalid: boolean\n validationMessage: React.ReactNode\n hasWarning: boolean\n warningMessage?: React.ReactNode\n helpText?: React.ReactNode\n UNSAFE_className?: string\n}) {\n const text = props.isInvalid ? props.validationMessage : props.hasWarning ? props.warningMessage : props.helpText\n const color = props.isInvalid ? 'negative' : props.hasWarning ? 'notice' : 'neutral'\n return text === null ? null : (\n <div\n id={props.id}\n className={tw('mt-1 text-sm', !text && 'h-5', colorClasses[color], 'text-wrap', props.UNSAFE_className)}\n >\n {text ?? ''}\n </div>\n )\n}\n\nfunction LabelRow(\n props: { label?: React.ReactNode; cornerHint?: React.ReactNode } & ({ labelId: string } | { htmlFor: string })\n): React.JSX.Element | null {\n if (!props.label) return null\n return (\n <div className=\"flex justify-between items-center mb-1\">\n {'labelId' in props ? (\n <Label id={props.labelId}>{props.label}</Label>\n ) : (\n <Label htmlFor={props.htmlFor}>{props.label}</Label>\n )}\n {props.cornerHint && <div className=\"text-sm text-neutral-500\">{props.cornerHint}</div>}\n </div>\n )\n}\n\ntype FieldGroupProps = {\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n hasWarning?: boolean\n shape?: 'rectangle' | 'oval'\n className?: string\n children: React.ReactNode\n onClick?: React.MouseEventHandler<HTMLDivElement>\n}\n\nconst FieldGroup = React.forwardRef<HTMLDivElement, FieldGroupProps>(\n ({ children, className, onClick, ...opts }, ref) => {\n const disabled = opts.isDisabled || opts.isReadonly\n const shape = opts.shape === 'oval' ? 'rounded-full' : 'rounded'\n\n return (\n <Group\n ref={ref}\n className={tw(\n 'relative overflow-hidden ring-1 shadow-sm transition-all duration-200 max-w-96',\n 'focus-within:outline-2 focus-within:-outline-offset-1',\n shape,\n disabled && 'ring-neutral-200 bg-neutral-50 text-neutral-400 shadow-none cursor-not-allowed',\n opts.isInvalid && 'ring-negative-300 bg-negative-50 focus-within:outline-negative-500',\n opts.hasWarning && 'ring-notice-300 bg-notice-50 focus-within:outline-notice-500',\n !opts.isInvalid &&\n !opts.hasWarning &&\n !disabled &&\n 'ring-black/10 hover:ring-black/20 focus-within:outline-primary-500',\n className\n )}\n onClick={onClick}\n >\n {children}\n </Group>\n )\n }\n)\n\nconst FieldInput = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {\n return (\n <Input\n {...props}\n ref={ref}\n className={composeTailwindRenderProps(\n props.className,\n tw(\n 'flex-1 bg-transparent px-3 py-2 border-0 focus:ring-0 focus:outline-none text-sm max-sm:text-base text-neutral-900 placeholder:text-neutral-400',\n (props.disabled || props.readOnly) && 'cursor-not-allowed'\n )\n )}\n />\n )\n})\n\nfunction DropdownListBox<T extends object>(props: React.ComponentProps<typeof ListBox<T>>) {\n return (\n <ListBox<T>\n {...props}\n className={composeTailwindRenderProps(\n props.className,\n 'overflow-hidden rounded bg-white border border-neutral-200 shadow-lg text-sm w-full focus:outline-none max-h-60 overflow-y-auto'\n )}\n />\n )\n}\n\nfunction DropdownItem(props: {\n id: string\n textValue: string\n label: React.ReactNode\n secondary?: React.ReactNode\n isDisabled?: boolean\n color?: string | null\n showCheckIcon?: boolean\n customValuePrefix?: string\n className?: string\n}) {\n const isCustom = Boolean(props.customValuePrefix)\n const showCheck = props.showCheckIcon !== false && !isCustom\n\n return (\n <ListBoxItem\n id={props.id}\n textValue={props.textValue}\n isDisabled={props.isDisabled}\n className={({ isFocused, isSelected }) =>\n tw(\n isFocused ? 'bg-neutral-50 text-neutral-900' : 'text-neutral-900 bg-white',\n 'relative cursor-pointer text-sm transition-colors duration-150',\n 'pr-2 pl-3 py-1.5 outline-none',\n props.isDisabled && 'opacity-50 cursor-not-allowed',\n isSelected ? (isFocused ? 'bg-primary-100' : 'bg-primary-50') : '',\n isCustom && 'italic',\n props.className\n )\n }\n >\n {({ isSelected }) => (\n <>\n {props.color && !isCustom ? (\n <div className=\"absolute left-0 top-0 w-[3px] h-full\" style={{ backgroundColor: props.color }} />\n ) : null}\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex-1 min-w-0\">\n <div className={tw(showCheck && isSelected ? 'font-medium' : 'font-normal', 'truncate')}>\n {isCustom ? `${props.customValuePrefix} \"${props.label}\"` : props.label}\n </div>\n {props.secondary && !isCustom && (\n <div className=\"text-xs mt-px truncate text-neutral-500\">{props.secondary}</div>\n )}\n </div>\n {showCheck && isSelected && (\n <div className=\"ml-2 w-fit shrink-0 flex items-center text-primary-500\">\n <Icon name=\"check\" className=\"h-4 w-4\" />\n </div>\n )}\n </div>\n </>\n )}\n </ListBoxItem>\n )\n}\n\nfunction DropdownSection<T extends object>(props: {\n title?: string\n items?: Iterable<T>\n children: React.ReactNode | ((item: T) => React.ReactNode)\n}) {\n return (\n <ListBoxSection>\n {props.title && (\n <Header className=\"sticky top-0 z-10 bg-white px-3 py-1.5 text-xs font-semibold text-neutral-500 border-b border-neutral-100\">\n {props.title}\n </Header>\n )}\n <Collection items={props.items}>{props.children}</Collection>\n </ListBoxSection>\n )\n}\n\nfunction ChevronButton(props?: { className?: string }) {\n return (\n <Button className={tw('flex items-center px-2 py-2', props?.className)}>\n <Icon name=\"chevron-up-down\" className=\"h-4 w-4 text-neutral-400 transition-colors\" />\n </Button>\n )\n}\n\nexport const FormFieldComponents = {\n Label,\n FormFieldHelpText,\n LabelRow,\n FieldGroup,\n FieldInput,\n DropdownListBox,\n DropdownSection,\n DropdownItem,\n ChevronButton\n}\n"}]},{"name":"animated-popover","dependencies":["tw"],"files":[{"name":"components/helpers/animated-popover.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add animated-popover --directory app\n\nimport { Popover, type PopoverProps } from 'react-aria-components'\nimport { tw } from '../../utils/tw'\n\nconst animationClasses = tw(\n 'transition-all duration-150 ease-out',\n // resting state: keep transform properties defined so transitions have endpoints\n 'translate-x-0 translate-y-0',\n // entering: fade + slide from trigger direction\n 'data-[entering]:opacity-0',\n 'data-[entering]:data-[placement=bottom]:-translate-y-1',\n 'data-[entering]:data-[placement=top]:translate-y-1',\n 'data-[entering]:data-[placement=left]:translate-x-1',\n 'data-[entering]:data-[placement=right]:-translate-x-1',\n // exiting: fade + slide toward trigger direction\n 'data-[exiting]:opacity-0',\n 'data-[exiting]:data-[placement=bottom]:-translate-y-1',\n 'data-[exiting]:data-[placement=top]:translate-y-1',\n 'data-[exiting]:data-[placement=left]:translate-x-1',\n 'data-[exiting]:data-[placement=right]:-translate-x-1'\n)\n\nexport function AnimatedPopover({ className, ...props }: PopoverProps) {\n return <Popover {...props} className={tw(animationClasses, className)} />\n}\n"}]},{"name":"headless-button","dependencies":["compose-refs","use-render-props"],"files":[{"name":"components/helpers/headless-button.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add headless-button --directory app\n\nimport { composeRefs } from '../../utils/compose-refs'\nimport { type WithRenderProps, useRenderProps } from '../../utils/use-render-props'\n\nimport React from 'react'\nimport { usePress } from '@react-aria/interactions'\nimport { mergeProps } from '@react-aria/utils'\n\nexport const HeadlessButton = React.forwardRef<\n HTMLButtonElement,\n WithRenderProps<React.ComponentProps<'button'>, { isPressed: boolean }>\n>((rawProps, externalRef) => {\n const myRef = React.useRef<HTMLButtonElement>(null)\n const { isPressed, pressProps } = usePress({ ref: myRef, isDisabled: rawProps.disabled })\n const renderProps = React.useMemo(\n () => ({\n isPressed\n }),\n [isPressed]\n )\n\n const props = useRenderProps(mergeProps(pressProps, rawProps), renderProps, ['children', 'style', 'className'])\n const ref = React.useMemo(() => composeRefs(myRef, externalRef), [myRef, externalRef])\n\n return (\n <button\n type=\"button\"\n {...props}\n {...(props.disabled || props['data-disabled'] ? { ['data-disabled']: true } : {})}\n {...((isPressed && !props.disabled) || props['data-pressed'] ? { ['data-pressed']: true } : {})}\n aria-pressed={props['aria-pressed'] ?? (isPressed && !props.disabled)}\n ref={ref}\n />\n )\n})\n\nHeadlessButton.displayName = 'HeadlessButton'\n"}]},{"name":"get-button-classes","dependencies":["tw"],"files":[{"name":"components/helpers/get-button-classes.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add get-button-classes --directory app\n\nimport { tw } from '../../utils/tw'\n\nexport type ButtonVariant = 'contained' | 'outlined' | 'soft' | 'plain'\nexport type ButtonColor = 'primary' | 'negative' | 'positive' | 'notice' | 'neutral'\nexport type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\nexport type ButtonShape = 'rectangle' | 'circle' | 'oval'\n\nconst base =\n 'relative inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow,transform,opacity] duration-150 cursor-default select-none [&_svg]:pointer-events-none [&_svg:not([class*=size-])]:size-4 [&_svg]:shrink-0 data-[disabled]:cursor-not-allowed'\n\nconst sizeClasses: Record<ButtonSize, string> = {\n xs: 'h-7 min-w-14 px-2 text-xs',\n sm: 'h-8 min-w-16 px-2.5 text-sm',\n md: 'h-9 min-w-20 px-3 text-sm',\n lg: 'h-10 min-w-24 px-4 text-base',\n xl: 'h-12 min-w-28 px-5 text-base'\n}\n\nconst shapeClasses: Record<ButtonShape, string> = {\n rectangle: 'rounded',\n circle: 'rounded-full aspect-square px-0 min-w-0',\n oval: 'rounded-full'\n}\n\nconst variantColorClasses: Record<ButtonVariant, Record<ButtonColor, string>> = {\n contained: {\n primary:\n 'bg-primary-600 text-white ring-1 ring-inset ring-primary-700 data-[hovered]:bg-primary-700 data-[hovered]:ring-primary-800 data-[pressed]:bg-primary-800 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none',\n negative:\n 'bg-negative-600 text-white ring-1 ring-inset ring-negative-700 data-[hovered]:bg-negative-700 data-[hovered]:ring-negative-800 data-[pressed]:bg-negative-800 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none',\n positive:\n 'bg-positive-600 text-white ring-1 ring-inset ring-positive-700 data-[hovered]:bg-positive-700 data-[hovered]:ring-positive-800 data-[pressed]:bg-positive-800 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none',\n notice:\n 'bg-notice-600 text-white ring-1 ring-inset ring-notice-700 data-[hovered]:bg-notice-700 data-[hovered]:ring-notice-800 data-[pressed]:bg-notice-800 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none',\n neutral:\n 'bg-neutral-800 text-white ring-1 ring-inset ring-neutral-900 data-[hovered]:bg-neutral-900 data-[pressed]:bg-neutral-950 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none'\n },\n outlined: {\n primary:\n 'bg-white ring-1 ring-inset ring-primary-300 text-primary-700 data-[hovered]:bg-primary-50 data-[hovered]:ring-primary-400 data-[pressed]:bg-primary-100 data-[pressed]:ring-primary-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent',\n negative:\n 'bg-white ring-1 ring-inset ring-negative-300 text-negative-700 data-[hovered]:bg-negative-50 data-[hovered]:ring-negative-400 data-[pressed]:bg-negative-100 data-[pressed]:ring-negative-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent',\n positive:\n 'bg-white ring-1 ring-inset ring-positive-300 text-positive-700 data-[hovered]:bg-positive-50 data-[hovered]:ring-positive-400 data-[pressed]:bg-positive-100 data-[pressed]:ring-positive-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent',\n notice:\n 'bg-white ring-1 ring-inset ring-notice-300 text-notice-700 data-[hovered]:bg-notice-50 data-[hovered]:ring-notice-400 data-[pressed]:bg-notice-100 data-[pressed]:ring-notice-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent',\n neutral:\n 'bg-white ring-1 ring-inset ring-neutral-300 text-neutral-700 data-[hovered]:bg-neutral-50 data-[hovered]:ring-neutral-400 data-[pressed]:bg-neutral-100 data-[pressed]:ring-neutral-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent'\n },\n soft: {\n primary:\n 'bg-primary-100 text-primary-800 data-[hovered]:bg-primary-200 data-[pressed]:bg-primary-300 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n negative:\n 'bg-negative-100 text-negative-800 data-[hovered]:bg-negative-200 data-[pressed]:bg-negative-300 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n positive:\n 'bg-positive-100 text-positive-800 data-[hovered]:bg-positive-200 data-[pressed]:bg-positive-300 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n notice:\n 'bg-notice-100 text-notice-800 data-[hovered]:bg-notice-200 data-[pressed]:bg-notice-300 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n neutral:\n 'bg-neutral-200 text-neutral-800 data-[hovered]:bg-neutral-300 data-[pressed]:bg-neutral-400 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400'\n },\n plain: {\n primary:\n 'bg-transparent text-primary-700 data-[hovered]:bg-primary-50 data-[pressed]:bg-primary-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n negative:\n 'bg-transparent text-negative-700 data-[hovered]:bg-negative-50 data-[pressed]:bg-negative-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n positive:\n 'bg-transparent text-positive-700 data-[hovered]:bg-positive-50 data-[pressed]:bg-positive-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n notice:\n 'bg-transparent text-notice-700 data-[hovered]:bg-notice-50 data-[pressed]:bg-notice-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',\n neutral:\n 'bg-transparent text-neutral-700 data-[hovered]:bg-neutral-50 data-[pressed]:bg-neutral-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400'\n }\n}\n\nconst toggledVariantColorClasses: Record<ButtonVariant, Record<ButtonColor, string>> = {\n contained: {\n primary: 'bg-primary-800 ring-primary-900',\n negative: 'bg-negative-800 ring-negative-900',\n positive: 'bg-positive-800 ring-positive-900',\n notice: 'bg-notice-800 ring-notice-900',\n neutral: 'bg-neutral-950'\n },\n outlined: {\n primary: 'bg-primary-100 ring-primary-500',\n negative: 'bg-negative-100 ring-negative-500',\n positive: 'bg-positive-100 ring-positive-500',\n notice: 'bg-notice-100 ring-notice-500',\n neutral: 'bg-neutral-100 ring-neutral-500'\n },\n soft: {\n primary: 'bg-primary-200',\n negative: 'bg-negative-200',\n positive: 'bg-positive-200',\n notice: 'bg-notice-200',\n neutral: 'bg-neutral-300'\n },\n plain: {\n primary: 'bg-primary-100',\n negative: 'bg-negative-100',\n positive: 'bg-positive-100',\n notice: 'bg-notice-100',\n neutral: 'bg-neutral-100'\n }\n}\n\nconst focusClasses =\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 data-[focus-visible]:outline-2 data-[focus-visible]:outline-offset-2 data-[focus-visible]:outline-primary-600'\n\nexport function getButtonClasses({\n variant = 'contained',\n color = 'primary',\n size = 'md',\n shape = 'oval',\n showSpinner,\n isToggled,\n className\n}: {\n variant?: ButtonVariant\n color?: ButtonColor\n size?: ButtonSize\n shape?: ButtonShape\n showSpinner?: boolean\n isToggled?: boolean\n className?: string\n} = {}) {\n return tw(\n base,\n 'data-[pressed]:scale-[.98]',\n showSpinner ? '[&>:not([data-loader])]:opacity-0 !text-transparent' : '',\n sizeClasses[size],\n shapeClasses[shape],\n variantColorClasses[variant][color],\n isToggled && toggledVariantColorClasses[variant][color],\n focusClasses,\n className\n )\n}\n"}]},{"name":"spinner","dependencies":["tw"],"files":[{"name":"components/spinner.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add spinner --directory app\n\nimport { ProgressBar } from 'react-aria-components'\nimport { Icon } from './icon'\nimport { tw } from '../utils/tw'\n\ninterface SpinnerProps {\n className?: string\n 'aria-label'?: string\n}\n\nexport function Spinner({ className, 'aria-label': ariaLabel = 'Loading…' }: SpinnerProps) {\n return (\n <ProgressBar isIndeterminate aria-label={ariaLabel}>\n <Icon name=\"arrow-path\" className={tw('animate-spin', className)} />\n </ProgressBar>\n )\n}\n"}]},{"name":"button-context","dependencies":["get-button-classes"],"files":[{"name":"components/helpers/button-context.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add button-context --directory app\n\nimport React from 'react'\nimport type { ButtonVariant, ButtonColor, ButtonSize, ButtonShape } from './get-button-classes'\n\nexport const ButtonContext = React.createContext<{\n variant?: ButtonVariant\n color?: ButtonColor\n size?: ButtonSize\n shape?: ButtonShape\n fullWidth?: boolean\n}>({})\n"}]},{"name":"calendar-month-year-picker","dependencies":["tw"],"files":[{"name":"components/helpers/calendar-month-year-picker.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add calendar-month-year-picker --directory app\n\nimport * as React from 'react'\nimport {\n Button,\n Calendar,\n CalendarGrid,\n CalendarCell,\n Heading,\n CalendarGridHeader,\n CalendarHeaderCell,\n CalendarGridBody\n} from 'react-aria-components'\nimport { CalendarDate, CalendarDateTime } from '@internationalized/date'\nimport { tw } from '../../utils/tw'\nimport { Icon } from '../icon'\n\ntype MonthYearPickerProps = {\n currentDate: CalendarDate | CalendarDateTime\n onSelect: (year: number, month: number) => void\n onCancel: () => void\n}\n\nexport function MonthYearPicker({ currentDate, onSelect, onCancel }: MonthYearPickerProps) {\n const [selectedYear, setSelectedYear] = React.useState(currentDate.year)\n const [viewingYear, setViewingYear] = React.useState(currentDate.year)\n\n const months = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December'\n ]\n\n // Generate year range (current year ± 50 years)\n const startYear = currentDate.year - 50\n const endYear = currentDate.year + 50\n const years = Array.from({ length: endYear - startYear + 1 }, (_, i) => startYear + i)\n\n // Find years to display (show 12 years at a time, 3x4 grid)\n const yearIndex = years.indexOf(viewingYear)\n const displayYears = years.slice(Math.max(0, yearIndex - 5), Math.min(years.length, yearIndex + 7))\n\n const handlePreviousYears = () => {\n setViewingYear((prev) => prev - 12)\n }\n\n const handleNextYears = () => {\n setViewingYear((prev) => prev + 12)\n }\n\n return (\n <div className=\"bg-white border border-neutral-200 rounded-md shadow-lg p-4 w-80\">\n {/* Year selection header */}\n <div className=\"mb-4\">\n <header className=\"flex items-center justify-between mb-3\">\n <Button className=\"p-1 hover:bg-neutral-100 rounded\" onPress={handlePreviousYears}>\n <Icon name=\"chevron-left\" className=\"h-4 w-4\" />\n </Button>\n <div className=\"text-sm font-semibold text-neutral-900\">\n {displayYears[0]} - {displayYears[displayYears.length - 1]}\n </div>\n <Button className=\"p-1 hover:bg-neutral-100 rounded\" onPress={handleNextYears}>\n <Icon name=\"chevron-right\" className=\"h-4 w-4\" />\n </Button>\n </header>\n\n {/* Year grid */}\n <div className=\"grid grid-cols-3 gap-2\">\n {displayYears.map((year) => (\n <button\n key={year}\n type=\"button\"\n onClick={() => setSelectedYear(year)}\n className={tw(\n 'py-2 px-3 text-sm rounded cursor-pointer transition-colors',\n 'hover:bg-neutral-100',\n selectedYear === year && 'bg-primary-500 text-white hover:bg-primary-500',\n year === currentDate.year && selectedYear !== year && 'font-semibold'\n )}\n >\n {year}\n </button>\n ))}\n </div>\n </div>\n\n {/* Month selection */}\n <div>\n <div className=\"text-sm font-semibold text-neutral-900 mb-2\">{selectedYear}</div>\n <div className=\"grid grid-cols-3 gap-2\">\n {months.map((month, index) => {\n const monthNum = index + 1\n const isCurrentMonth = selectedYear === currentDate.year && monthNum === currentDate.month\n\n return (\n <button\n key={month}\n type=\"button\"\n onClick={() => onSelect(selectedYear, monthNum)}\n className={tw(\n 'py-2 px-2 text-sm rounded cursor-pointer transition-colors',\n 'hover:bg-neutral-100',\n isCurrentMonth && 'font-semibold border border-primary-500'\n )}\n >\n {month.slice(0, 3)}\n </button>\n )\n })}\n </div>\n </div>\n\n {/* Cancel button */}\n <div className=\"mt-4 flex justify-end\">\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"py-1 px-3 text-sm text-neutral-600 hover:text-neutral-900 hover:bg-neutral-100 rounded transition-colors\"\n >\n Cancel\n </button>\n </div>\n </div>\n )\n}\n\ntype CustomCalendarPropsBase = {\n isDateUnavailable?: (date: CalendarDate | CalendarDateTime) => boolean\n}\n\ntype CustomCalendarPropsDate = CustomCalendarPropsBase & {\n value: CalendarDate | null\n onChange: (date: CalendarDate | null) => void\n minValue?: CalendarDate\n maxValue?: CalendarDate\n}\n\ntype CustomCalendarPropsDateTime = CustomCalendarPropsBase & {\n value: CalendarDateTime | null\n onChange: (date: CalendarDateTime | null) => void\n minValue?: CalendarDateTime\n maxValue?: CalendarDateTime\n}\n\ntype CustomCalendarProps = CustomCalendarPropsDate | CustomCalendarPropsDateTime\n\nexport function CustomCalendar(props: CustomCalendarProps) {\n const { value, onChange, minValue, maxValue, isDateUnavailable } = props\n const [showMonthYearPicker, setShowMonthYearPicker] = React.useState(false)\n\n // Create a default focused date\n const createDefaultDate = () => {\n const now = new Date()\n const year = now.getFullYear()\n const month = now.getMonth() + 1\n const day = now.getDate()\n\n // Check if we're working with CalendarDateTime\n if (value && 'hour' in value) {\n return new CalendarDateTime(year, month, day, 0, 0, 0)\n }\n return new CalendarDate(year, month, day)\n }\n\n const [focusedDate, setFocusedDate] = React.useState(value || createDefaultDate())\n\n const handleMonthYearSelect = (year: number, month: number) => {\n if ('hour' in focusedDate) {\n // CalendarDateTime\n const newDate = focusedDate.set({ year, month })\n setFocusedDate(newDate)\n } else {\n // CalendarDate - use safe day value\n const newDate = new CalendarDate(year, month, Math.min(focusedDate.day, 28))\n setFocusedDate(newDate)\n }\n setShowMonthYearPicker(false)\n }\n\n const handleFocusChange = (date: CalendarDate) => {\n if ('hour' in focusedDate) {\n // Convert CalendarDate to CalendarDateTime by preserving time from focusedDate\n const newDateTime = focusedDate.set({\n year: date.year,\n month: date.month,\n day: date.day\n })\n setFocusedDate(newDateTime)\n } else {\n setFocusedDate(date)\n }\n }\n\n if (showMonthYearPicker) {\n return (\n <MonthYearPicker\n currentDate={focusedDate}\n onSelect={handleMonthYearSelect}\n onCancel={() => setShowMonthYearPicker(false)}\n />\n )\n }\n\n const handleCalendarChange = (date: CalendarDate | CalendarDateTime | null) => {\n if (typeof onChange === 'function') {\n // Type assertion needed due to union type constraints\n ;(onChange as (date: CalendarDate | CalendarDateTime | null) => void)(date)\n }\n }\n\n return (\n <Calendar\n value={value ?? undefined}\n onChange={handleCalendarChange}\n focusedValue={focusedDate}\n onFocusChange={handleFocusChange}\n minValue={minValue !== undefined && minValue !== null ? minValue : undefined}\n maxValue={maxValue !== undefined && maxValue !== null ? maxValue : undefined}\n isDateUnavailable={\n isDateUnavailable\n ? (date) => {\n // Convert DateValue to CalendarDate for the callback\n if ('hour' in date) {\n // It's a CalendarDateTime, convert to CalendarDate\n const calendarDate = new CalendarDate(date.year, date.month, date.day)\n return isDateUnavailable(calendarDate)\n }\n return isDateUnavailable(date as CalendarDate)\n }\n : undefined\n }\n className=\"bg-white border border-neutral-200 rounded-md shadow-lg p-3\"\n >\n <header className=\"flex items-center justify-between mb-2\">\n <Button slot=\"previous\" className=\"p-1 hover:bg-neutral-100 rounded\">\n <Icon name=\"chevron-left\" className=\"h-4 w-4\" />\n </Button>\n <button\n type=\"button\"\n onClick={() => setShowMonthYearPicker(true)}\n className=\"text-sm font-semibold text-neutral-900 hover:bg-neutral-100 px-2 py-1 rounded transition-colors\"\n >\n <Heading />\n </button>\n <Button slot=\"next\" className=\"p-1 hover:bg-neutral-100 rounded\">\n <Icon name=\"chevron-right\" className=\"h-4 w-4\" />\n </Button>\n </header>\n <CalendarGrid>\n <CalendarGridHeader>\n {(day) => <CalendarHeaderCell className=\"text-xs text-neutral-500 font-medium w-8 h-8\">{day}</CalendarHeaderCell>}\n </CalendarGridHeader>\n <CalendarGridBody>\n {(date) => (\n <CalendarCell\n date={date}\n className={({ isSelected, isOutsideMonth, isDisabled, isUnavailable, isFocusVisible }) =>\n tw(\n 'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center',\n 'hover:bg-neutral-100',\n isOutsideMonth && 'text-neutral-300',\n (isDisabled || isUnavailable) && 'text-neutral-300 cursor-not-allowed',\n isSelected && 'bg-primary-500 text-white hover:bg-primary-500',\n isFocusVisible && 'ring-2 ring-primary-500 ring-offset-1'\n )\n }\n />\n )}\n </CalendarGridBody>\n </CalendarGrid>\n </Calendar>\n )\n}\n"}]},{"name":"pdf-dist","files":[{"name":"components/helpers/pdf-dist.client.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add pdf-dist --directory app\n\n// @ts-ignore\nimport pdfjs from '@bundled-es-modules/pdfjs-dist'\n\npdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`\n\nexport default pdfjs\n"}]},{"name":"button","dependencies":["tw","spinner","button-context","get-button-classes","headless-button","compose-refs","use-prevent-default","use-render-props","use-spin-delay","use-stable-accessor","icon"],"files":[{"name":"components/button.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add button --directory app\n\nimport * as React from 'react'\nimport { Spinner } from './spinner'\nimport { flushSync } from 'react-dom'\nimport { composeRenderProps } from 'react-aria-components'\nimport {\n getButtonClasses,\n type ButtonColor,\n type ButtonShape,\n type ButtonSize,\n type ButtonVariant\n} from './helpers/get-button-classes'\nimport { HeadlessButton } from './helpers/headless-button'\nimport { useSpinDelay } from '../utils/use-spin-delay'\nimport { usePreventDefault } from '../utils/use-prevent-default'\nimport { composeTailwindRenderProps, tw } from '../utils/tw'\nimport { useStableAccessor } from '../utils/use-stable-accessor'\nimport { mergeProps } from '@react-aria/utils'\nimport { ButtonContext } from './helpers/button-context'\n\n// --- Public types ---\n\nexport type ButtonProps = {\n variant?: ButtonVariant\n color?: ButtonColor\n size?: ButtonSize\n shape?: ButtonShape\n isToggled?: boolean\n fullWidth?: boolean\n onActionSucceeded?: () => void\n onActionCompleted?: () => void\n disabled?: boolean\n onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void\n className?: string\n children?: React.ReactNode\n value?: string | undefined\n isPending?: boolean\n} & Partial<Pick<React.ComponentProps<'button'>, 'type' | 'role' | 'name' | 'form' | 'id' | 'title' | 'aria-label'>>\n\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ isToggled, onActionSucceeded, onActionCompleted, isPending: isPendingProp, ...propsRaw }, ref) => {\n const {\n variant = 'contained',\n color = 'primary',\n size = 'md',\n shape = 'oval',\n fullWidth,\n ...props\n } = mergeProps(propsRaw, React.useContext(ButtonContext))\n\n const { isClicking, onClick: isClickingHandler } = useIsClicking(props.onClick)\n\n const isPending = isPendingProp || isClicking\n const isPendingSmoothed = useSpinDelay(isPending, { delay: 0, minDuration: 50 })\n const showSpinner = useSpinDelay(isPending, { delay: 500, minDuration: 200 })\n\n const getOnActionCompleted = useStableAccessor(onActionCompleted)\n const getOnActionSucceeded = useStableAccessor(onActionSucceeded)\n const wasPending = React.useRef(false)\n React.useEffect(() => {\n if (isPendingSmoothed !== wasPending.current) {\n wasPending.current = isPendingSmoothed\n if (!isPendingSmoothed) {\n getOnActionCompleted()?.()\n getOnActionSucceeded()?.()\n }\n }\n }, [isPendingSmoothed, getOnActionCompleted, getOnActionSucceeded])\n\n const onClick = usePreventDefault(isClickingHandler, showSpinner || isClicking)\n\n return (\n <HeadlessButton\n {...props}\n ref={ref}\n data-pending={isPending}\n onClick={onClick}\n {...(isToggled != null ? { 'aria-pressed': isToggled } : {})}\n className={composeTailwindRenderProps(\n props.className,\n tw(getButtonClasses({ variant, color, size, shape, isToggled }), fullWidth && 'w-full')\n )}\n >\n {composeRenderProps(props.children, (children) => (\n <>\n {(size === 'xs' || size === 'sm' || shape === 'circle') && (\n <span className=\"absolute left-1/2 top-1/2 size-12 -translate-x-1/2 -translate-y-1/2\" aria-hidden=\"true\" />\n )}\n <span className={tw('inline-flex items-center gap-2', showSpinner && 'opacity-0')}>{children}</span>\n <span\n aria-hidden={!showSpinner ? true : undefined}\n className={tw('absolute inset-0 flex items-center justify-center', !showSpinner && 'opacity-0')}\n >\n <Spinner className=\"size-4\" />\n </span>\n </>\n ))}\n </HeadlessButton>\n )\n }\n)\n\nButton.displayName = 'Button'\n\nfunction useIsClicking<C extends ((event: React.MouseEvent<any, MouseEvent>) => unknown) | undefined>(\n onClickRaw: C\n): { onClick: C; isClicking: boolean } {\n type Event = C extends (event: infer E) => unknown ? E : never\n\n const [isClicking, setIsClicking] = React.useState<boolean>(false)\n\n const onClickRawStabilized = useStableAccessor(onClickRaw)\n\n const onClickDebounced = React.useCallback(\n (event: Event) => {\n try {\n const onClickInner = onClickRawStabilized()\n flushSync(() => setIsClicking(true))\n const result = onClickInner ? onClickInner(event) : null\n Promise.resolve(result).finally(() => {\n setIsClicking(false)\n })\n return result\n } catch (e) {\n setIsClicking(false)\n throw e\n }\n },\n [onClickRawStabilized, setIsClicking]\n )\n\n if (!onClickRaw) {\n return { onClick: undefined as C, isClicking }\n } else {\n return { onClick: onClickDebounced as C, isClicking }\n }\n}\n"}]},{"name":"icon","files":[{"name":"components/icon.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add icon --directory app\n\nimport * as React from 'react'\n// @ts-ignore\nimport href from '../icons/sprite.svg'\nimport type { IconName } from '~/icons/types'\n\n// ---------------------------------------------------------------------------\n// Props\n// ---------------------------------------------------------------------------\n\nexport interface IconProps extends React.ComponentProps<'svg'> {\n name: IconName\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport const Icon = React.forwardRef<SVGSVGElement, IconProps>(({ className, name, ...props }, ref) => {\n return (\n <svg ref={ref} viewBox=\"0 0 24 24\" className={className} {...props}>\n <use href={`${href}#${name}`} />\n </svg>\n )\n})\n\nIcon.displayName = 'Icon'\n"}]},{"name":"headless-file-input","dependencies":["compose-refs"],"files":[{"name":"components/headless-file-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add headless-file-input --directory app\n\nimport React from 'react'\nimport { useComposedRefs } from '../utils/compose-refs'\n\nexport type HeadlessFileInputProps = {\n children?: React.ReactNode\n} & (\n | {\n multiple: true\n value?: (string | File)[]\n defaultValue?: (string | File)[]\n onChange?(value: (string | File)[]): unknown\n }\n | {\n multiple?: false\n value?: string | File | null\n defaultValue?: string | File | null\n onChange?(value: string | File | null): unknown\n }\n)\n\ntype FileInputContext =\n | { multiple?: false; value: File | string | null; onChange(value: File | string | null): unknown }\n | { multiple: true; value: (File | string)[]; onChange(value: (File | string)[]): unknown }\n\nconst fileInputContext = React.createContext<FileInputContext>({\n multiple: false,\n value: null,\n onChange: () => null\n})\n\nfunction FileInputContainer({ multiple, value, defaultValue, onChange, children }: HeadlessFileInputProps) {\n const controlled = useControlled({ multiple, value, defaultValue, onChange }) as FileInputContext\n return <fileInputContext.Provider value={controlled}>{children}</fileInputContext.Provider>\n}\n\nFileInputContainer.displayName = 'HeadlessFileInput'\n\nconst FileInputDropZone = React.forwardRef<\n HTMLLabelElement,\n {\n name?: string\n children: ((props: { dragOver: boolean }) => React.ReactElement) | React.ReactElement\n inputId?: string\n accept?: string | undefined\n onDragChange?(value: boolean): unknown\n capture?: React.InputHTMLAttributes<'file'>['capture']\n disabled?: React.InputHTMLAttributes<'file'>['disabled']\n processFile?: (file: File) => Promise<File>\n inputRef?: React.Ref<HTMLInputElement>\n } & Omit<React.ComponentProps<'label'>, 'children'>\n>(({ children, inputId, inputRef, onDragChange, name, accept, capture, disabled, processFile, ...props }, ref) => {\n const { onChange, value, multiple } = React.useContext(fileInputContext)\n const [dragOver, setDragOver] = React.useState(false)\n const startDrag = React.useCallback(\n (e: React.DragEvent<HTMLInputElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setDragOver(true)\n onDragChange && onDragChange(true)\n },\n [setDragOver, onDragChange]\n )\n const stopDrag = React.useCallback(\n (e: React.DragEvent<HTMLInputElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setDragOver(false)\n onDragChange && onDragChange(false)\n },\n [setDragOver, onDragChange]\n )\n\n const childrenResult = typeof children === 'function' ? children({ dragOver }) : children\n const childProps = childrenResult.props as Record<string, unknown>\n const cloned = React.cloneElement(\n childrenResult,\n {\n ...childProps\n },\n [\n ...React.Children.toArray(childProps.children as React.ReactNode),\n <FileInputInput\n key=\"file-input-hidden\"\n id={inputId}\n name={name}\n accept={accept}\n ref={inputRef}\n style={{ width: '0.1px', height: '0.1px', opacity: 0, overflow: 'hidden', position: 'absolute', zIndex: -10 }}\n capture={capture}\n disabled={disabled}\n processFile={processFile}\n />\n ]\n )\n\n return (\n <label {...props} ref={ref}>\n <div\n onDrag={cancel}\n onDragStart={cancel}\n onDragOver={cancel}\n onDragEnter={startDrag}\n onDragLeave={stopDrag}\n onDrop={(e) => {\n e.preventDefault()\n e.stopPropagation()\n setDragOver(false)\n onDragChange && onDragChange(false)\n if (e.dataTransfer.files.length) {\n if (multiple) {\n onChange([...value, ...Array.from(e.dataTransfer.files)])\n } else {\n onChange(e.dataTransfer.files.item(0) || null)\n }\n } else {\n const uri = e.dataTransfer.getData('text/uri-list')\n if (uri) {\n fetchFileFromUri(uri).then((file) => {\n if (multiple) {\n onChange([...value, file])\n } else {\n onChange(file)\n }\n })\n }\n }\n }}\n >\n {cloned}\n </div>\n </label>\n )\n})\nFileInputDropZone.displayName = 'FileInputDropZone'\n\nconst FileInputInput = React.forwardRef<\n HTMLInputElement,\n React.ComponentPropsWithoutRef<'input'> & { processFile?: (file: File) => Promise<File> }\n>(({ name, processFile, form, ...props }, refProp) => {\n const { multiple, value, onChange } = React.useContext(fileInputContext)\n const localRef = React.useRef<HTMLInputElement>(null)\n const ref = useComposedRefs(localRef, refProp)\n\n React.useEffect(() => {\n if (localRef.current) {\n const dt = new DataTransfer()\n if (Array.isArray(value)) {\n value.forEach(async (v) => v instanceof File && dt.items.add(v))\n } else if (value && value instanceof File) {\n dt.items.add(value)\n }\n localRef.current.files = dt.files\n }\n }, [value, processFile])\n\n const urls = React.useMemo(() => {\n if (Array.isArray(value)) {\n return value.map((v, i) => (typeof v === 'string' ? { name: `${name}.${i}`, value: v } : null)).filter(Boolean) as {\n name: string\n value: string\n }[]\n } else if (value && typeof value === 'string') {\n return [{ name: name, value: value }]\n } else {\n return []\n }\n }, [value, name])\n\n const hasFiles =\n typeof value !== 'string' &&\n !!value &&\n !(Array.isArray(value) && !value.filter((v) => v && typeof v !== 'string').length)\n const hasUrls = !!urls.length\n\n return (\n <>\n <input\n {...props}\n type=\"file\"\n form={form}\n ref={hasFiles ? ref : undefined}\n name={hasFiles ? name : undefined}\n onChange={async (e) => {\n const newValue = (\n multiple\n ? e.currentTarget.files\n ? [\n ...value,\n ...(await Promise.all(\n Array.from(e.currentTarget.files).map(async (file) => (processFile ? processFile(file) : file))\n ))\n ]\n : []\n : processFile && e.currentTarget.files?.item(0)\n ? await processFile(e.currentTarget.files?.item(0) as File)\n : e.currentTarget.files?.item(0)\n ) as any\n\n onChange(newValue)\n }}\n multiple={multiple}\n />\n {urls.map((url, i) => (\n <input\n type=\"hidden\"\n ref={i === 0 && !hasFiles ? ref : undefined}\n form={form}\n value={url.value}\n name={url.name}\n key={url.name}\n />\n ))}\n {!hasUrls && !hasFiles ? <input type=\"hidden\" ref={ref} form={form} name={name} value=\"\" /> : null}\n </>\n )\n})\nFileInputInput.displayName = 'FileInputInput'\n\nfunction FilePreview({\n children\n}: {\n children(\n props: {\n file: File | null\n fileUrl: string\n remove(): unknown\n replace(newFile: File | string): unknown\n }[]\n ): React.ReactNode\n}) {\n const { value, onChange } = React.useContext(fileInputContext)\n const urlMap = useFileUrls(value)\n const removeSingle = React.useCallback(() => onChange(null as any), [onChange])\n const replaceSingle = React.useCallback((file: File | string) => onChange(file as any), [onChange])\n\n const removeMultiple = React.useCallback(\n (index: number) => onChange((value as File[]).filter((f, i) => i !== index) as any),\n [value, onChange]\n )\n const replaceMultiple = React.useCallback(\n (index: number, file: File | string) =>\n onChange((value as (File | string)[]).map((f, i) => (i === index ? file : f)) as any),\n [value, onChange]\n )\n\n if (!value) {\n return <>{children([])}</>\n } else if (Array.isArray(value)) {\n return (\n <>\n {children(\n value.map((v, i) => ({\n file: typeof v === 'string' ? null : v,\n fileUrl: urlMap.get(v) ?? (typeof v === 'string' ? v : ''),\n remove: () => removeMultiple(i),\n replace: (f) => replaceMultiple(i, f)\n }))\n )}\n </>\n )\n } else {\n return (\n <>\n {children([\n {\n file: typeof value === 'string' ? null : value,\n fileUrl: urlMap.get(value) ?? (typeof value === 'string' ? value : ''),\n remove: removeSingle,\n replace: replaceSingle\n }\n ])}\n </>\n )\n }\n}\n\nexport const HeadlessFileInput = Object.assign(FileInputContainer, {\n Input: FileInputInput,\n Preview: FilePreview,\n DropZone: FileInputDropZone\n})\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction cancel(e: React.SyntheticEvent) {\n e.stopPropagation()\n e.preventDefault()\n}\n\nfunction useControlled<T extends string | File | null | (string | File)[]>({\n defaultValue,\n value,\n onChange: onChangeRaw,\n multiple\n}: {\n value?: T\n defaultValue?: T\n onChange?(value: null | string | File | (string | File)[]): unknown\n multiple?: boolean\n}) {\n const [state, setState] = React.useState(defaultValue)\n\n const onChange = React.useCallback(\n (v: T) => {\n setState(v)\n onChangeRaw && onChangeRaw(v)\n },\n [onChangeRaw, setState]\n )\n\n const proposed = value === undefined ? state : value\n\n return { value: !proposed && multiple ? [] : proposed || null, onChange, multiple: !!multiple }\n}\n\nfunction useFileUrls(value: (File | string)[] | File | string | null): Map<File | string, string> {\n const urlMapRef = React.useRef(new Map<File | string, string>())\n\n const items = value === null ? [] : Array.isArray(value) ? value : [value]\n\n // Create blob URLs for new items only\n for (const item of items) {\n if (!urlMapRef.current.has(item)) {\n urlMapRef.current.set(item, typeof item === 'string' ? item : URL.createObjectURL(item))\n }\n }\n\n // Prune stale entries and revoke their blob URLs\n const currentKeys = new Set(items)\n React.useEffect(() => {\n for (const [key, url] of urlMapRef.current) {\n if (!currentKeys.has(key)) {\n if (typeof key !== 'string') URL.revokeObjectURL(url)\n urlMapRef.current.delete(key)\n }\n }\n })\n\n // Revoke all blob URLs on unmount\n React.useEffect(() => {\n const map = urlMapRef.current\n return () => {\n for (const [key, url] of map) {\n if (typeof key !== 'string') URL.revokeObjectURL(url)\n }\n map.clear()\n }\n }, [])\n\n return urlMapRef.current\n}\n\nasync function fetchFileFromUri(uri: string): Promise<File> {\n const res = await fetch(uri)\n const blob = await res.blob()\n const contentType = res.headers.get('content-type') || blob.type || ''\n const contentDisposition = res.headers.get('content-disposition')\n let filename = new URL(uri).pathname.split('/').pop() || 'file'\n const match = contentDisposition?.match(/filename\\*?=(?:UTF-8'')?[\"']?([^\"';\\n]+)/)\n if (match) filename = decodeURIComponent(match[1]!)\n return new File([blob], filename, { type: contentType })\n}\n"}]},{"name":"button-link","dependencies":["tw","spinner","button-context","get-button-classes","use-spin-delay","icon"],"files":[{"name":"components/button-link.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add button-link --directory app\n\nimport * as React from 'react'\nimport { Link } from 'react-router'\nimport { usePress } from '@react-aria/interactions'\nimport { mergeProps } from '@react-aria/utils'\nimport { Spinner } from './spinner'\nimport {\n getButtonClasses,\n type ButtonColor,\n type ButtonShape,\n type ButtonSize,\n type ButtonVariant\n} from './helpers/get-button-classes'\nimport { useSpinDelay } from '../utils/use-spin-delay'\nimport { tw } from '../utils/tw'\nimport { ButtonContext } from './helpers/button-context'\n\nexport type ButtonLinkProps = React.ComponentPropsWithoutRef<typeof Link> & {\n size?: ButtonSize\n variant?: ButtonVariant\n color?: ButtonColor\n shape?: ButtonShape\n disabled?: boolean\n fullWidth?: boolean\n isPending?: boolean\n}\n\nexport const ButtonLink = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(\n ({ disabled, fullWidth, isPending: isPendingProp, children, onClick, className, ...propsRaw }, ref) => {\n const {\n size = 'md',\n variant = 'contained',\n color = 'primary',\n shape = 'oval',\n ...props\n } = mergeProps(propsRaw, React.useContext(ButtonContext))\n const { isPressed, pressProps } = usePress({ isDisabled: disabled })\n\n const isPending = isPendingProp ?? false\n const showSpinner = useSpinDelay(isPending, { delay: 500, minDuration: 200 })\n\n const handleClick = disabled || isPending ? (e: React.MouseEvent<HTMLAnchorElement>) => e.preventDefault() : onClick\n\n return (\n <Link\n {...mergeProps(props, pressProps, { onClick: handleClick })}\n ref={ref}\n data-pending={isPending || undefined}\n data-disabled={disabled || undefined}\n data-pressed={(isPressed && !disabled) || undefined}\n aria-disabled={disabled || undefined}\n className={getButtonClasses({\n size,\n variant,\n color,\n shape,\n className: tw(fullWidth && 'w-full', className)\n })}\n >\n {(size === 'xs' || size === 'sm' || shape === 'circle') && (\n <span className=\"absolute left-1/2 top-1/2 size-12 -translate-x-1/2 -translate-y-1/2\" aria-hidden=\"true\" />\n )}\n <span className={tw('inline-flex items-center gap-2', showSpinner && 'opacity-0')}>{children}</span>\n <span\n aria-hidden={!showSpinner ? true : undefined}\n className={tw('absolute inset-0 flex items-center justify-center', !showSpinner && 'opacity-0')}\n >\n <Spinner className=\"size-4\" />\n </span>\n </Link>\n )\n }\n)\n\nButtonLink.displayName = 'ButtonLink'\n"}]},{"name":"button-group","dependencies":["tw"],"files":[{"name":"components/button-group.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add button-group --directory app\n\nimport * as React from 'react'\nimport { mergeProps } from '@react-aria/utils'\nimport { tw } from '../utils/tw'\n\nexport type ButtonGroupOrientation = 'horizontal' | 'vertical'\nexport type ButtonGroupAlignment = 'start' | 'center' | 'end'\nexport type ButtonGroupSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n\nexport type ButtonGroupProps = {\n orientation?: ButtonGroupOrientation\n alignment?: ButtonGroupAlignment\n size?: ButtonGroupSize\n className?: string\n children?: React.ReactNode\n 'aria-label'?: string\n 'aria-labelledby'?: string\n 'aria-describedby'?: string\n fullWidth?: boolean\n}\n\nexport const ButtonGroupContext = React.createContext<{\n orientation?: ButtonGroupOrientation\n alignment?: ButtonGroupAlignment\n size?: ButtonGroupSize\n fullWidth?: boolean\n className?: string\n}>({})\n\nconst sizeGapClasses: Record<ButtonGroupSize, string> = {\n xl: 'gap-4',\n lg: 'gap-3',\n md: 'gap-2',\n sm: 'gap-1',\n xs: 'gap-0.5'\n}\n\nconst alignmentClasses: Record<ButtonGroupAlignment, string> = {\n start: 'justify-start',\n center: 'justify-center',\n end: 'justify-end'\n}\n\nexport function ButtonGroup(inputProps: ButtonGroupProps) {\n const context = React.useContext(ButtonGroupContext)\n const {\n orientation = 'horizontal',\n alignment = 'end',\n size = 'md',\n className,\n children,\n fullWidth,\n ...ariaProps\n } = mergeProps(React.useContext(ButtonGroupContext), inputProps)\n\n return (\n <div\n role=\"group\"\n aria-label={ariaProps['aria-label']}\n aria-labelledby={ariaProps['aria-labelledby']}\n aria-describedby={ariaProps['aria-describedby']}\n className={tw(\n 'inline-flex w-fit',\n sizeGapClasses[size],\n orientation === 'horizontal' ? 'flex-row items-center flex-wrap' : 'flex-col',\n alignmentClasses[alignment],\n fullWidth && 'w-full',\n className\n )}\n >\n {children}\n </div>\n )\n}\n\nButtonGroup.displayName = 'ButtonGroup'\n"}]},{"name":"toggle-button-group","dependencies":["tw","get-button-classes"],"files":[{"name":"components/toggle-button-group.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add toggle-button-group --directory app\n\nimport * as React from 'react'\nimport { RadioGroup, Radio } from 'react-aria-components'\nimport { tw } from '../utils/tw'\nimport type { ButtonSize } from './helpers/get-button-classes'\n\n// --- Types ---\n\nexport type ToggleButtonGroupSize = ButtonSize\n\nexport type ToggleButtonGroupProps = {\n value?: string | null\n defaultValue?: string | null\n onChange?: (value: string) => void\n orientation?: 'horizontal' | 'vertical'\n className?: string\n isDisabled?: boolean\n name?: string\n children: React.ReactNode\n 'aria-label'?: string\n 'aria-labelledby'?: string\n}\n\n// --- ToggleButtonGroup ---\n\nexport function ToggleButtonGroup({ orientation = 'horizontal', className, children, ...props }: ToggleButtonGroupProps) {\n return (\n <RadioGroup\n {...props}\n orientation={orientation}\n className={tw(\n 'inline-flex',\n orientation === 'horizontal'\n ? ['flex-row', '[&>*:first-child]:rounded-l [&>*:last-child]:rounded-r', '[&>*:not(:first-child)]:-ml-px']\n : ['flex-col', '[&>*:first-child]:rounded-t [&>*:last-child]:rounded-b', '[&>*:not(:first-child)]:-mt-px'],\n className\n )}\n >\n {children}\n </RadioGroup>\n )\n}\n\nToggleButtonGroup.displayName = 'ToggleButtonGroup'\n\n// --- ToggleButton ---\n\nexport type ToggleButtonProps = {\n value: string\n children: React.ReactNode\n isDisabled?: boolean\n className?: string\n 'aria-label'?: string\n size?: ToggleButtonGroupSize\n}\n\nexport function ToggleButton({ children, className, size = 'md', ...props }: ToggleButtonProps) {\n return (\n <Radio {...props} className={({ isSelected }) => tw(getToggleButtonClasses({ size, isSelected }), className)}>\n {children}\n </Radio>\n )\n}\n\nToggleButton.displayName = 'ToggleButton'\n\n// --- Styling ---\n\nfunction getToggleButtonClasses({ size = 'md', isSelected }: { size?: ButtonSize; isSelected?: boolean }) {\n return tw(\n 'relative rounded-none cursor-default select-none',\n 'font-medium transition-all',\n 'data-[focus-visible]:outline data-[focus-visible]:outline-2 data-[focus-visible]:outline-offset-2',\n 'data-[disabled]:bg-neutral-50 data-[disabled]:ring-neutral-50 data-[disabled]:cursor-not-allowed data-[disabled]:text-neutral-400',\n\n // Size\n size === 'xs' && 'px-3.5 py-1.5 text-xs gap-x-1 min-w-6',\n size === 'sm' && 'px-4 py-1.5 text-sm gap-x-1.5 min-w-7',\n size === 'md' && 'px-5 py-2 text-sm gap-x-2 min-w-8',\n size === 'lg' && 'px-6 py-2.5 text-base gap-x-2 min-w-9',\n size === 'xl' && 'px-8 py-3 text-base gap-x-2.5 min-w-10',\n\n // Unselected\n !isSelected && 'bg-white ring-1 ring-inset ring-neutral-300',\n !isSelected && 'data-[hovered]:bg-neutral-50 data-[hovered]:ring-neutral-400',\n !isSelected && 'text-neutral-700 data-[focus-visible]:outline-primary-600',\n\n // Selected\n isSelected && 'bg-primary-500 text-white ring-1 ring-inset ring-primary-700',\n isSelected && 'data-[hovered]:bg-primary-600 data-[hovered]:ring-primary-700',\n isSelected && 'data-[focus-visible]:outline-primary-600',\n\n 'inline-flex justify-center items-center gap-x-2'\n )\n}\n"}]},{"name":"date-picker","dependencies":["tw","animated-popover","calendar-month-year-picker","form-field","icon"],"files":[{"name":"components/date-picker.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add date-picker --directory app\n\nimport * as React from 'react'\nimport { DatePicker as AriaDatePicker, DateInput, DateSegment, Button, Dialog, I18nProvider } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport { CalendarDate, parseDate } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { CustomCalendar } from './helpers/calendar-month-year-picker'\nimport { dateFns, type Iso } from 'iso-fns'\n\nexport type DatePickerProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<Iso.Date | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (ISO date string YYYY-MM-DD)\n value?: Iso.Date | null\n defaultValue?: Iso.Date | null\n onChange?: (value: Iso.Date | null) => void\n\n // Date specific (ISO date strings)\n minValue?: Iso.Date\n maxValue?: Iso.Date\n isDateUnavailable?: (date: Iso.Date) => boolean\n\n // Calendar display\n locale?: string\n\n // Input shape\n shape?: 'rectangle' | 'oval'\n}\n\nfunction DatePicker_(props: DatePickerProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Internal state for CalendarDate\n const [value, _setValue] = React.useState(() => {\n try {\n const startingDate = props.value === undefined ? (props.defaultValue ?? null) : props.value\n return startingDate ? parseDate(startingDate) : null\n } catch {\n return null\n }\n })\n\n function setValue(d: CalendarDate | null) {\n _setValue(d)\n if (!d || dateFns.isValid(d.toString())) {\n props.onChange && props.onChange(d ? (d.toString() as Iso.Date) : null)\n }\n commitValidation()\n }\n\n // Sync controlled value\n if (props.value !== undefined) {\n const currentValueAsIso = value && dateFns.isValid(value.toString()) ? value.toString() : null\n const propValue = props.value && dateFns.isValid(props.value) ? props.value : null\n if ((!value || dateFns.isValid(value.toString())) && propValue !== currentValueAsIso) {\n try {\n _setValue(propValue ? parseDate(propValue) : null)\n } catch {\n _setValue(null)\n }\n }\n }\n\n // Convert min/max dates\n const minDate = React.useMemo(() => {\n if (!props.minValue) return undefined\n try {\n return parseDate(props.minValue)\n } catch {\n return undefined\n }\n }, [props.minValue])\n\n const maxDate = React.useMemo(() => {\n if (!props.maxValue) return undefined\n try {\n return parseDate(props.maxValue)\n } catch {\n return undefined\n }\n }, [props.maxValue])\n\n // Form validation\n const {\n validationMessage: validationMessageRaw,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: value && dateFns.isValid(value.toString()) ? (value.toString() as Iso.Date) : null,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by AriaDatePicker\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const lessThanMin =\n props.minValue && value && dateFns.isValid(value.toString()) ? value.toString() < props.minValue : false\n const greaterThanMax =\n props.maxValue && value && dateFns.isValid(value.toString()) ? value.toString() > props.maxValue : false\n\n const validationMessage =\n validationMessageRaw ||\n (lessThanMin ? `Cannot be less than ${dateFns.format(props.minValue!, 'MM/dd/yyyy')}` : '') ||\n (greaterThanMax ? `Cannot be greater than ${dateFns.format(props.maxValue!, 'MM/dd/yyyy')}` : '')\n\n const isInvalid = props.isInvalid || validationInvalid || lessThanMin || greaterThanMax\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value?.toString() ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaDatePicker\n value={value}\n aria-labelledby={labelId}\n onChange={setValue}\n minValue={minDate}\n maxValue={maxDate}\n isDateUnavailable={\n props.isDateUnavailable ? (date) => props.isDateUnavailable!(date.toString() as Iso.Date) : undefined\n }\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <DateInput className=\"flex-1 grow flex px-3 py-2 text-sm\">\n {(segment) => (\n <DateSegment\n segment={segment}\n className={tw('px-0.5 tabular-nums outline-none rounded-sm', 'focus:bg-primary-500 focus:text-white')}\n />\n )}\n </DateInput>\n <Button className=\"flex items-center px-2 py-2\">\n <Icon name=\"calendar\" className=\"h-4 w-4 text-neutral-400\" />\n </Button>\n </FormFieldComponents.FieldGroup>\n\n <AnimatedPopover>\n <Dialog>\n <CustomCalendar\n value={value}\n onChange={setValue}\n minValue={minDate}\n maxValue={maxDate}\n isDateUnavailable={\n props.isDateUnavailable ? (date) => props.isDateUnavailable!(date.toString() as Iso.Date) : undefined\n }\n />\n </Dialog>\n </AnimatedPopover>\n </AriaDatePicker>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n\nconst today = dateFns.now()\nexport const DatePicker = Object.assign(DatePicker_, {\n isDateInPast: (date: Iso.Date) => dateFns.isBefore(date, today)\n})\n"}]},{"name":"text-field","dependencies":["tw","form-field","headless-button","compose-refs","use-render-props"],"files":[{"name":"components/text-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add text-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessButton } from './helpers/headless-button'\n\nexport type TextFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string>\n\n // Value management\n value?: string\n defaultValue?: string\n onChange?: (value: string) => void\n onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>\n\n // Input type\n type?: 'text' | 'email' | 'password' | 'search' | 'tel' | 'url'\n shape?: 'rectangle' | 'oval'\n\n // Additional input props\n autoComplete?: string\n autoFocus?: boolean\n maxLength?: number\n minLength?: number\n pattern?: string\n inputMode?: React.HTMLAttributes<HTMLInputElement>['inputMode']\n\n trailingInlineAddon?: React.ReactNode\n}\n\nexport function TextField(props: TextFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? '', props.onChange)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n setValue(e.target.value)\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={inputRef} name={props.name} value={value} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n {/* Input wrapper with state-based styling */}\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <FormFieldComponents.FieldInput\n id={inputId}\n type={props.type ?? 'text'}\n value={value}\n onChange={handleChange}\n onKeyDown={props.onKeyDown}\n onBlur={commitValidation}\n placeholder={props.placeholder}\n disabled={props.isDisabled}\n readOnly={props.isReadonly}\n autoComplete={props.autoComplete ?? 'off'}\n autoFocus={props.autoFocus}\n maxLength={props.maxLength}\n minLength={props.minLength}\n pattern={props.pattern}\n inputMode={props.inputMode}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n />\n {props.trailingInlineAddon}\n </FormFieldComponents.FieldGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nexport function TrailingInlineAddonButton({ className, ...props }: React.ComponentProps<typeof HeadlessButton>) {\n return (\n <HeadlessButton\n {...props}\n className={(a) =>\n tw(\n 'inline-flex items-center inset-y-0 right-0 px-3 text-neutral-500 hover:bg-neutral-50 rounded-l sm:text-sm',\n typeof className === 'function' ? className(a) : className\n )\n }\n />\n )\n}\n"}]},{"name":"text-area","dependencies":["tw","form-field"],"files":[{"name":"components/text-area.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add text-area --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\n\nexport type TextAreaProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string>\n\n // Value management\n value?: string\n defaultValue?: string\n onChange?: (value: string) => void\n\n // TextArea specific\n rows?: number\n minRows?: number\n maxRows?: number\n autoResize?: boolean\n maxLength?: number\n resize?: 'none' | 'both' | 'horizontal' | 'vertical'\n shape?: 'rectangle' | 'oval'\n}\n\nexport function TextArea(props: TextAreaProps) {\n const autoResize = props.autoResize ?? true\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const textAreaRef = React.useRef<HTMLTextAreaElement>(null)\n const helpTextId = React.useId()\n const textAreaId = React.useId()\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? '', props.onChange)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n textAreaRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setValue(e.target.value)\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={textAreaId} />\n\n {/* TextArea wrapper with state-based styling */}\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className={autoResize ? 'grid' : undefined}\n >\n {/* Auto-expand helper element */}\n {autoResize && (\n <p\n className={tw(\n 'whitespace-pre-wrap overflow-hidden col-span-full row-span-full px-3 py-2 text-sm text-transparent pointer-events-none',\n 'border-0 m-0'\n )}\n style={{\n WebkitLineClamp: props.maxRows || undefined,\n WebkitBoxOrient: props.maxRows ? 'vertical' : undefined,\n display: props.maxRows ? '-webkit-box' : undefined,\n minHeight: props.minRows ? `${props.minRows * 1.5}rem` : undefined\n }}\n aria-hidden=\"true\"\n >\n {value ? value + ' ' : ' '}\n </p>\n )}\n\n <textarea\n ref={textAreaRef}\n id={textAreaId}\n className={tw(\n 'w-full bg-transparent px-3 py-2 border-0 focus:ring-0 focus:outline-none text-sm max-sm:text-base text-neutral-900 placeholder:text-neutral-400',\n autoResize && 'resize-none overflow-hidden col-span-full row-span-full',\n (props.isDisabled || props.isReadonly) && 'cursor-not-allowed',\n !autoResize && props.resize === 'none' && 'resize-none',\n !autoResize && props.resize === 'both' && 'resize',\n !autoResize && props.resize === 'horizontal' && 'resize-x',\n !autoResize && props.resize === 'vertical' && 'resize-y',\n !autoResize && !props.resize && 'resize-y'\n )}\n value={value}\n onChange={handleChange}\n onBlur={commitValidation}\n placeholder={props.placeholder}\n disabled={props.isDisabled}\n readOnly={props.isReadonly}\n maxLength={props.maxLength}\n rows={!autoResize ? props.rows || props.minRows || 3 : props.minRows || 3}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n />\n </FormFieldComponents.FieldGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n"}]},{"name":"number-field","dependencies":["tw","form-field","use-number-input"],"files":[{"name":"components/number-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add number-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { useNumberInput } from '../utils/use-number-input'\n\nexport type NumberFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<number>\n errorMessage?: React.ReactNode | ((v: import('@maestro-js/form').ValidationResult) => React.ReactNode)\n\n // Value management\n value?: number | null\n defaultValue?: number | null\n onChange?: (value: number | null) => void\n\n // Number specific\n min?: number | null\n max?: number | null\n step?: number | null\n formatOptions?: Intl.NumberFormatOptions\n autoComplete?: string | null\n autoFocus?: boolean\n shape?: 'rectangle' | 'oval'\n}\n\nexport function NumberField(props: NumberFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n const numberInput = useNumberInput({\n value: props.value ?? undefined,\n defaultValue: props.defaultValue ?? undefined,\n onChange: props.onChange,\n minValue: props.min ?? undefined,\n maxValue: props.max ?? undefined,\n step: props.step ?? undefined,\n formatOptions: props.formatOptions,\n locale: 'en-US',\n isDisabled: props.isDisabled,\n isReadOnly: props.isReadonly ?? false\n })\n\n HeadlessForm.useReset(inputRef, numberInput.numberValue, numberInput.setNumberValue)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n value: numberInput.numberValue as number,\n validate: props.validate,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n const handleBlur = () => {\n numberInput.commit(inputRef.current?.value)\n commitValidation()\n }\n\n return (\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n {/* Hidden input for form submission */}\n {props.isDisabled ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n form={props.form}\n name={props.name}\n value={numberInput.numberValue ?? ''}\n type=\"number\"\n min={numberInput.minValue}\n max={numberInput.maxValue}\n />\n )}\n\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <FormFieldComponents.FieldInput\n ref={inputRef}\n id={inputId}\n type=\"text\"\n value={numberInput.inputValue}\n onChange={(e) => numberInput.setInputValue(e.currentTarget.value)}\n onBlur={handleBlur}\n placeholder={props.placeholder}\n disabled={props.isDisabled}\n readOnly={props.isReadonly}\n autoComplete={props.autoComplete ?? undefined}\n autoFocus={props.autoFocus}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n />\n </FormFieldComponents.FieldGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n"}]},{"name":"currency-field","dependencies":["tw","form-field","use-number-input"],"files":[{"name":"components/currency-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add currency-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { useNumberInput } from '../utils/use-number-input'\n\nexport type CurrencyFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<number>\n errorMessage?: React.ReactNode | ((v: import('@maestro-js/form').ValidationResult) => React.ReactNode)\n\n // Value management (in the unit specified by `units`)\n value?: number | null\n defaultValue?: number | null\n onChange?: (value: number | null) => void\n\n // Number specific\n min?: number | null\n max?: number | null\n autoComplete?: string | null\n autoFocus?: boolean\n\n // Currency specific\n units: 'dollars' | 'cents'\n shape?: 'rectangle' | 'oval'\n}\n\nconst currencyFormatOptions: Intl.NumberFormatOptions = {\n style: 'currency',\n currency: 'USD',\n minimumFractionDigits: 2,\n maximumFractionDigits: 2\n}\n\nexport function CurrencyField(props: CurrencyFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n const isCents = props.units === 'cents'\n\n // Stable ref for onChange to avoid unnecessary re-renders in useNumberInput\n const onChangeRef = React.useRef(props.onChange)\n onChangeRef.current = props.onChange\n\n const handleChange = React.useCallback(\n (newValue: number | null) => {\n onChangeRef.current?.(isCents && newValue !== null ? dollarsToCents(newValue) : newValue)\n },\n [isCents]\n )\n\n const displayValue = isCents && props.value != null ? centsToDollars(props.value) : props.value\n const displayDefaultValue = isCents && props.defaultValue != null ? centsToDollars(props.defaultValue) : props.defaultValue\n const displayMin = isCents && props.min != null ? centsToDollars(props.min) : props.min\n const displayMax = isCents && props.max != null ? centsToDollars(props.max) : props.max\n\n const numberInput = useNumberInput({\n value: displayValue ?? undefined,\n defaultValue: displayDefaultValue ?? undefined,\n onChange: handleChange,\n minValue: displayMin ?? undefined,\n maxValue: displayMax ?? undefined,\n step: 0.01,\n formatOptions: currencyFormatOptions,\n locale: 'en-US',\n isDisabled: props.isDisabled,\n isReadOnly: props.isReadonly ?? false\n })\n\n HeadlessForm.useReset(inputRef, numberInput.numberValue, numberInput.setNumberValue)\n\n // Validation value should be in the original unit\n const validationValue = isCents ? dollarsToCents(numberInput.numberValue) : numberInput.numberValue\n\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: validationValue!,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Hidden input value in the original unit\n const hiddenValue = isCents ? dollarsToCents(numberInput.numberValue) : numberInput.numberValue\n\n const handleBlur = () => {\n numberInput.commit(inputRef.current?.value)\n commitValidation()\n }\n\n return (\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n {/* Hidden input for form submission */}\n {props.isDisabled ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n form={props.form}\n name={props.name}\n value={hiddenValue ?? ''}\n type=\"number\"\n min={numberInput.minValue}\n max={numberInput.maxValue}\n />\n )}\n\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <FormFieldComponents.FieldInput\n ref={inputRef}\n id={inputId}\n type=\"text\"\n value={numberInput.inputValue}\n onChange={(e) => numberInput.setInputValue(e.currentTarget.value)}\n onBlur={handleBlur}\n placeholder={props.placeholder}\n disabled={props.isDisabled}\n readOnly={props.isReadonly}\n autoComplete={props.autoComplete ?? undefined}\n autoFocus={props.autoFocus}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n />\n </FormFieldComponents.FieldGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nfunction centsToDollars(cents: number): number {\n return cents / 100\n}\n\nfunction dollarsToCents(dollars: number | null): number | null {\n if (dollars == null) return null\n return Math.round(dollars * 100)\n}\n"}]},{"name":"percentage-field","dependencies":["tw","form-field","use-number-input"],"files":[{"name":"components/percentage-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add percentage-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { useNumberInput } from '../utils/use-number-input'\n\nexport type PercentageFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<number>\n errorMessage?: React.ReactNode | ((v: import('@maestro-js/form').ValidationResult) => React.ReactNode)\n\n // Value management (in the unit specified by `units`)\n value?: number | null\n defaultValue?: number | null\n onChange?: (value: number | null) => void\n\n // Number specific\n min?: number | null\n max?: number | null\n autoComplete?: string | null\n autoFocus?: boolean\n\n // Percentage specific\n units: 'integer' | 'decimal'\n minimumFractionDigits?: number\n maximumFractionDigits?: number\n shape?: 'rectangle' | 'oval'\n}\n\nexport function PercentageField(props: PercentageFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n const isInteger = props.units === 'integer'\n\n // Stable ref for onChange to avoid unnecessary re-renders in useNumberInput\n const onChangeRef = React.useRef(props.onChange)\n onChangeRef.current = props.onChange\n\n const handleChange = React.useCallback(\n (newValue: number | null) => {\n onChangeRef.current?.(isInteger && newValue !== null ? decimalToInteger(newValue) : newValue)\n },\n [isInteger]\n )\n\n const formatOptions = React.useMemo<Intl.NumberFormatOptions>(\n () => ({\n style: 'percent',\n minimumFractionDigits: props.minimumFractionDigits,\n maximumFractionDigits: props.maximumFractionDigits\n }),\n [props.minimumFractionDigits, props.maximumFractionDigits]\n )\n\n const displayValue = isInteger && props.value != null ? integerToDecimal(props.value) : props.value\n const displayDefaultValue =\n isInteger && props.defaultValue != null ? integerToDecimal(props.defaultValue) : props.defaultValue\n const displayMin = isInteger && props.min != null ? integerToDecimal(props.min) : props.min\n const displayMax = isInteger && props.max != null ? integerToDecimal(props.max) : props.max\n\n const numberInput = useNumberInput({\n value: displayValue ?? undefined,\n defaultValue: displayDefaultValue ?? undefined,\n onChange: handleChange,\n minValue: displayMin ?? undefined,\n maxValue: displayMax ?? undefined,\n formatOptions,\n locale: 'en-US',\n isDisabled: props.isDisabled,\n isReadOnly: props.isReadonly ?? false\n })\n\n HeadlessForm.useReset(inputRef, numberInput.numberValue, numberInput.setNumberValue)\n\n // Validation value should be in the original unit\n const validationValue = isInteger ? decimalToInteger(numberInput.numberValue) : numberInput.numberValue\n\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: validationValue as number,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Hidden input value in the original unit\n const hiddenValue = isInteger ? decimalToInteger(numberInput.numberValue) : numberInput.numberValue\n\n const handleBlur = () => {\n numberInput.commit(inputRef.current?.value)\n commitValidation()\n }\n\n return (\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n {/* Hidden input for form submission */}\n {props.isDisabled ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n form={props.form}\n name={props.name}\n value={hiddenValue ?? ''}\n type=\"number\"\n min={numberInput.minValue}\n max={numberInput.maxValue}\n />\n )}\n\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <FormFieldComponents.FieldInput\n ref={inputRef}\n id={inputId}\n type=\"text\"\n value={numberInput.inputValue}\n onChange={(e) => numberInput.setInputValue(e.currentTarget.value)}\n onBlur={handleBlur}\n placeholder={props.placeholder}\n disabled={props.isDisabled}\n readOnly={props.isReadonly}\n autoComplete={props.autoComplete ?? undefined}\n autoFocus={props.autoFocus}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n />\n </FormFieldComponents.FieldGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nfunction integerToDecimal(value: number): number {\n return value / 100\n}\n\nfunction decimalToInteger(value: number | null): number | null {\n if (value == null) return null\n return Math.round(value * 100)\n}\n"}]},{"name":"phone-number-field","dependencies":["tw","form-field"],"files":[{"name":"components/phone-number-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add phone-number-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { FormFieldComponents } from './helpers/form-field'\n\nexport type PhoneNumberFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string>\n\n // Value management\n value?: string\n defaultValue?: string\n onChange?: (value: string) => void\n\n // Phone specific\n country?: 'US' | 'CA' | 'international'\n autoFormat?: boolean\n autoComplete?: string\n autoFocus?: boolean\n shape?: 'rectangle' | 'oval'\n}\n\nexport function PhoneNumberField(props: PhoneNumberFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? '', props.onChange)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Track whether field is focused for formatting toggle\n const [isFocused, setIsFocused] = React.useState(false)\n\n // Display value - formatted when not focused, raw when focused\n const displayValue = React.useMemo(() => {\n if (props.autoFormat === false) return value\n if (isFocused) return value\n return formatPhoneNumber(value, props.country)\n }, [value, props.autoFormat, props.country, isFocused])\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (props.autoFormat !== false) {\n setValue(getDigits(e.target.value))\n } else {\n setValue(e.target.value)\n }\n }\n\n const handleFocus = () => {\n setIsFocused(true)\n }\n\n const handleBlur = () => {\n setIsFocused(false)\n commitValidation()\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={inputRef} name={props.name} value={value} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <FormFieldComponents.FieldInput\n id={inputId}\n type=\"tel\"\n value={displayValue}\n onChange={handleChange}\n onFocus={handleFocus}\n onBlur={handleBlur}\n placeholder={props.placeholder ?? (props.country === 'international' ? '+1234567890' : '(555) 555-5555')}\n disabled={props.isDisabled}\n readOnly={props.isReadonly}\n autoComplete={props.autoComplete ?? 'tel'}\n autoFocus={props.autoFocus}\n inputMode=\"tel\"\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n />\n </FormFieldComponents.FieldGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\n// Format phone number for display\nfunction formatPhoneNumber(value: string, country: 'US' | 'CA' | 'international' = 'US'): string {\n const digits = value.replace(/\\D/g, '')\n\n if (country === 'US' || country === 'CA') {\n if (digits.length === 0) return ''\n if (digits.length <= 3) return `(${digits}`\n if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`\n if (digits.length <= 10) return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`\n // Handle country code\n if (digits.length === 11 && digits[0] === '1') {\n return `+1 (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7)}`\n }\n return value\n }\n\n // International - just add + prefix for readability\n if (digits.length > 0 && !value.startsWith('+')) {\n return '+' + digits\n }\n return value\n}\n\n// Get raw digits from formatted phone\nfunction getDigits(value: string): string {\n return value.replace(/\\D/g, '')\n}\n"}]},{"name":"tag-field","dependencies":["tw","form-field","icon"],"files":[{"name":"components/tag-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add tag-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\n\nexport type TagFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string[]>\n\n // Value management\n value?: string[]\n defaultValue?: string[]\n onChange?: (value: string[]) => void\n\n // Tag specific\n maxTags?: number\n hideTagCount?: boolean\n allowDuplicates?: boolean\n separator?: string | RegExp | (string | RegExp)[]\n validateTag?: (tag: string) => boolean\n transformTag?: (tag: string) => string\n\n // Custom rendering\n renderTag?: (tag: string, index: number, onRemove: () => void, isDisabled?: boolean) => React.ReactNode\n shape?: 'rectangle' | 'oval'\n}\n\nexport function TagField(props: TagFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n const [query, setQuery] = React.useState('')\n\n // Controlled state management\n const [tags, setTags] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? [], props.onChange)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: tags,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n const separators = React.useMemo(() => {\n const sep = props.separator ?? [',', '\\n']\n return Array.isArray(sep) ? sep : [sep]\n }, [props.separator])\n\n const addTag = React.useCallback(\n (tag: string) => {\n const trimmed = tag.trim()\n if (!trimmed) return\n\n const final = props.transformTag ? props.transformTag(trimmed) : trimmed\n\n if (props.validateTag && !props.validateTag(final)) return\n if (!props.allowDuplicates && tags.includes(final)) return\n if (props.maxTags !== undefined && tags.length >= props.maxTags) return\n\n setTags([...tags, final])\n setQuery('')\n commitValidation()\n },\n [tags, setTags, commitValidation, props.transformTag, props.validateTag, props.allowDuplicates, props.maxTags]\n )\n\n const removeTag = React.useCallback(\n (index: number) => {\n setTags(tags.filter((_, i) => i !== index))\n commitValidation()\n },\n [tags, setTags, commitValidation]\n )\n\n const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const value = e.target.value\n\n for (const sep of separators) {\n const matches = typeof sep === 'string' ? value.includes(sep) : sep.test(value)\n if (matches) {\n const parts = typeof sep === 'string' ? value.split(sep) : value.split(sep)\n for (const part of parts) {\n if (part.trim()) addTag(part)\n }\n setQuery('')\n return\n }\n }\n\n setQuery(value)\n }\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n if (query) addTag(query)\n } else if (e.key === 'Backspace' && !query && tags.length > 0) {\n removeTag(tags.length - 1)\n }\n }\n\n const atMax = props.maxTags !== undefined && tags.length >= props.maxTags\n const isDisabled = props.isDisabled || atMax\n\n return (\n <div className=\"relative\">\n {/* Hidden inputs for form submission */}\n {tags.map((tag, i) => (\n <HeadlessForm.HiddenInput\n key={i}\n ref={i === 0 ? hiddenInputRef : undefined}\n name={props.name}\n value={tag}\n form={props.form}\n />\n ))}\n {tags.length === 0 && <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value=\"\" form={props.form} />}\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <FormFieldComponents.FieldGroup isDisabled={props.isDisabled} isInvalid={isInvalid} hasWarning={hasWarning} shape={props.shape} onClick={() => inputRef.current?.focus()}>\n <div className=\"flex flex-wrap items-center gap-1.5 px-3 py-1.5 min-h-[38px] w-full\">\n {/* Display existing tags */}\n {tags.map((tag, index) =>\n props.renderTag ? (\n <React.Fragment key={index}>\n {props.renderTag(tag, index, () => removeTag(index), props.isDisabled)}\n </React.Fragment>\n ) : (\n <Tag\n key={index}\n onRemove={!props.isDisabled ? () => removeTag(index) : undefined}\n isDisabled={props.isDisabled}\n >\n {tag}\n </Tag>\n )\n )}\n\n {/* Text input for adding tags */}\n <input\n id={inputId}\n ref={inputRef}\n className={tw(\n 'flex-1 min-w-[120px] bg-transparent p-0 border-0 focus:ring-0 focus:outline-none text-sm text-neutral-900 placeholder:text-neutral-400',\n props.isDisabled && 'cursor-not-allowed'\n )}\n value={query}\n onChange={handleInputChange}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n if (query) addTag(query)\n commitValidation()\n }}\n placeholder={\n tags.length === 0 ? (props.placeholder ?? 'Add tags...') : atMax ? `Maximum ${props.maxTags} tags` : ''\n }\n disabled={isDisabled}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n autoComplete=\"off\"\n />\n </div>\n </FormFieldComponents.FieldGroup>\n\n {/* Tag count */}\n {!props.hideTagCount && props.maxTags !== undefined && (\n <div className=\"mt-1 text-xs text-neutral-500\">\n {tags.length} / {props.maxTags} tags\n </div>\n )}\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nfunction Tag({\n children,\n onRemove,\n isDisabled\n}: {\n children: React.ReactNode\n onRemove?: () => void\n isDisabled?: boolean\n}) {\n return (\n <span\n className={tw(\n 'inline-flex items-center rounded-full pl-2 pr-1.5 py-0.5 text-xs font-normal bg-primary-500/10 text-primary-500',\n isDisabled && 'opacity-50'\n )}\n >\n <span className=\"truncate\">{children}</span>\n {onRemove && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n onRemove()\n }}\n className=\"inline-flex items-center justify-center size-4 -m-0.5 p-0.5 rounded-full translate-x-1 hover:bg-neutral-950/10\"\n aria-label=\"Remove\"\n tabIndex={-1}\n >\n <Icon name=\"x-mark\" className=\"size-2\" aria-hidden=\"true\" />\n </button>\n )}\n </span>\n )\n}\n"}]},{"name":"numeric-tag-field","dependencies":["tw","form-field","icon"],"files":[{"name":"components/numeric-tag-field.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add numeric-tag-field --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\n\nexport type NumericTagFieldProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<number[]>\n\n // Value management\n value?: number[]\n defaultValue?: number[]\n onChange?: (value: number[]) => void\n\n // Tag specific\n maxTags?: number\n hideTagCount?: boolean\n allowDuplicates?: boolean\n separator?: string | RegExp | (string | RegExp)[]\n validateTag?: (tag: number) => boolean\n transformTag?: (tag: number) => number\n min?: number\n max?: number\n shape?: 'rectangle' | 'oval'\n}\n\nexport function NumericTagField(props: NumericTagFieldProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n const [query, setQuery] = React.useState('')\n\n // Controlled state management\n const [tags, setTags] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? [], props.onChange)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: tags,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n const separators = React.useMemo(() => {\n const sep = props.separator ?? [',', '\\n']\n return Array.isArray(sep) ? sep : [sep]\n }, [props.separator])\n\n const addTag = React.useCallback(\n (raw: string) => {\n const trimmed = raw.trim()\n if (!trimmed || Number.isNaN(Number(trimmed))) return\n\n let num = Number(trimmed)\n\n // Transform tag if needed\n if (props.transformTag) num = props.transformTag(num)\n\n // Check min/max bounds\n if (props.min !== undefined && num < props.min) return\n if (props.max !== undefined && num > props.max) return\n\n // Validate tag\n if (props.validateTag && !props.validateTag(num)) return\n\n // Check for duplicates\n if (!props.allowDuplicates && tags.includes(num)) return\n\n // Check max tags\n if (props.maxTags !== undefined && tags.length >= props.maxTags) return\n\n setTags([...tags, num])\n setQuery('')\n commitValidation()\n },\n [\n tags,\n setTags,\n commitValidation,\n props.transformTag,\n props.validateTag,\n props.allowDuplicates,\n props.maxTags,\n props.min,\n props.max\n ]\n )\n\n const removeTag = React.useCallback(\n (index: number) => {\n setTags(tags.filter((_, i) => i !== index))\n commitValidation()\n },\n [tags, setTags, commitValidation]\n )\n\n const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const value = e.target.value\n\n for (const sep of separators) {\n const matches = typeof sep === 'string' ? value.includes(sep) : sep.test(value)\n if (matches) {\n const parts = typeof sep === 'string' ? value.split(sep) : value.split(sep)\n for (const part of parts) {\n if (part.trim()) addTag(part)\n }\n setQuery('')\n return\n }\n }\n\n setQuery(value)\n }\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n if (query) addTag(query)\n } else if (e.key === 'Backspace' && !query && tags.length > 0) {\n removeTag(tags.length - 1)\n }\n }\n\n const atMax = props.maxTags !== undefined && tags.length >= props.maxTags\n const isDisabled = props.isDisabled || atMax\n\n const defaultPlaceholder =\n props.min !== undefined && props.max !== undefined\n ? `Add numbers (${props.min}–${props.max})...`\n : props.min !== undefined\n ? `Add numbers (min: ${props.min})...`\n : props.max !== undefined\n ? `Add numbers (max: ${props.max})...`\n : 'Add numbers...'\n\n return (\n <div className=\"relative\">\n {/* Hidden inputs for form submission */}\n {tags.map((tag, i) => (\n <HeadlessForm.HiddenInput\n key={i}\n ref={i === 0 ? hiddenInputRef : undefined}\n name={props.name}\n value={tag}\n form={props.form}\n />\n ))}\n {tags.length === 0 && <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value=\"\" form={props.form} />}\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <FormFieldComponents.FieldGroup isDisabled={props.isDisabled} isInvalid={isInvalid} hasWarning={hasWarning} shape={props.shape} onClick={() => inputRef.current?.focus()}>\n <div className=\"flex flex-wrap items-center gap-1.5 px-3 py-1.5 min-h-[38px] w-full\">\n {/* Display existing tags */}\n {tags.map((tag, index) => (\n <Tag key={index} onRemove={!props.isDisabled ? () => removeTag(index) : undefined} isDisabled={props.isDisabled}>\n {tag}\n </Tag>\n ))}\n\n {/* Text input for adding tags */}\n <input\n id={inputId}\n ref={inputRef}\n className={tw(\n 'flex-1 min-w-[120px] bg-transparent p-0 border-0 focus:ring-0 focus:outline-none text-sm text-neutral-900 placeholder:text-neutral-400',\n props.isDisabled && 'cursor-not-allowed'\n )}\n value={query}\n onChange={handleInputChange}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n if (query) addTag(query)\n commitValidation()\n }}\n placeholder={\n tags.length === 0 ? (props.placeholder ?? defaultPlaceholder) : atMax ? `Maximum ${props.maxTags} tags` : ''\n }\n disabled={isDisabled}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n autoComplete=\"off\"\n inputMode=\"numeric\"\n />\n </div>\n </FormFieldComponents.FieldGroup>\n\n {/* Tag count */}\n {!props.hideTagCount && props.maxTags !== undefined && (\n <div className=\"mt-1 text-xs text-neutral-500\">\n {tags.length} / {props.maxTags} tags\n </div>\n )}\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nfunction Tag({\n children,\n onRemove,\n isDisabled\n}: {\n children: React.ReactNode\n onRemove?: () => void\n isDisabled?: boolean\n}) {\n return (\n <span\n className={tw(\n 'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-normal bg-primary-500/10 text-primary-500',\n isDisabled && 'opacity-50'\n )}\n >\n <span className=\"truncate\">{children}</span>\n {onRemove && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n onRemove()\n }}\n className=\"inline-flex items-center justify-center h-4 w-4 -m-0.5 p-0.5 rounded-full translate-x-1 hover:bg-black/10\"\n aria-label=\"Remove\"\n tabIndex={-1}\n >\n <Icon name=\"x-mark\" className=\"h-2 w-2\" aria-hidden=\"true\" />\n </button>\n )}\n </span>\n )\n}\n"}]},{"name":"date-input","dependencies":["tw","form-field"],"files":[{"name":"components/date-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add date-input --directory app\n\nimport * as React from 'react'\nimport { DateField, DateInput as AriaDateInput, DateSegment, I18nProvider } from 'react-aria-components'\nimport { CalendarDate, parseDate } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { dateFns, type Iso } from 'iso-fns'\n\nexport type DateInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<Iso.Date | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (ISO date string YYYY-MM-DD)\n value?: Iso.Date | null\n defaultValue?: Iso.Date | null\n onChange?: (value: Iso.Date | null) => void\n\n // Date specific (ISO date strings)\n minValue?: Iso.Date\n maxValue?: Iso.Date\n isDateUnavailable?: (date: Iso.Date) => boolean\n\n // Locale\n locale?: string\n shape?: 'rectangle' | 'oval'\n}\n\nexport function DateInput(props: DateInputProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Internal state for CalendarDate\n const [value, _setValue] = React.useState(() => {\n try {\n const startingDate = props.value === undefined ? (props.defaultValue ?? null) : props.value\n return startingDate ? parseDate(startingDate) : null\n } catch {\n return null\n }\n })\n\n function setValue(d: CalendarDate | null) {\n _setValue(d)\n if (!d || dateFns.isValid(d.toString())) {\n props.onChange && props.onChange(d ? (d.toString() as Iso.Date) : null)\n }\n commitValidation()\n }\n\n // Sync controlled value\n if (props.value !== undefined) {\n const currentValueAsIso = value && dateFns.isValid(value.toString()) ? value.toString() : null\n const propValue = props.value && dateFns.isValid(props.value) ? props.value : null\n if ((!value || dateFns.isValid(value.toString())) && propValue !== currentValueAsIso) {\n try {\n _setValue(propValue ? parseDate(propValue) : null)\n } catch {\n _setValue(null)\n }\n }\n }\n\n // Convert min/max dates\n const minDate = React.useMemo(() => {\n if (!props.minValue) return undefined\n try {\n return parseDate(props.minValue)\n } catch {\n return undefined\n }\n }, [props.minValue])\n\n const maxDate = React.useMemo(() => {\n if (!props.maxValue) return undefined\n try {\n return parseDate(props.maxValue)\n } catch {\n return undefined\n }\n }, [props.maxValue])\n\n // Form validation\n const {\n validationMessage: validationMessageRaw,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: value && dateFns.isValid(value.toString()) ? (value.toString() as Iso.Date) : null,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by DateField\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const lessThanMin =\n props.minValue && value && dateFns.isValid(value.toString()) ? value.toString() < props.minValue : false\n const greaterThanMax =\n props.maxValue && value && dateFns.isValid(value.toString()) ? value.toString() > props.maxValue : false\n\n const validationMessage =\n validationMessageRaw ||\n (lessThanMin ? `Cannot be less than ${dateFns.format(props.minValue!, 'MM/dd/yyyy')}` : '') ||\n (greaterThanMax ? `Cannot be greater than ${dateFns.format(props.maxValue!, 'MM/dd/yyyy')}` : '')\n\n const isInvalid = props.isInvalid || validationInvalid || lessThanMin || greaterThanMax\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value?.toString() ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n {/* Date input */}\n <DateField\n aria-labelledby={labelId}\n aria-describedby={helpTextId}\n value={value}\n onChange={setValue}\n minValue={minDate}\n maxValue={maxDate}\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n isDateUnavailable={\n props.isDateUnavailable ? (date) => props.isDateUnavailable!(date.toString() as Iso.Date) : undefined\n }\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n >\n <AriaDateInput className=\"flex px-3 py-2 text-sm\">\n {(segment) => (\n <DateSegment\n segment={segment}\n className={tw(\n 'px-0.5 tabular-nums outline-none rounded-sm',\n 'focus:bg-primary-500 focus:text-white',\n segment.isPlaceholder && 'text-neutral-400'\n )}\n />\n )}\n </AriaDateInput>\n </FormFieldComponents.FieldGroup>\n </DateField>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n"}]},{"name":"date-range-picker","dependencies":["tw","animated-popover","form-field","icon"],"files":[{"name":"components/date-range-picker.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add date-range-picker --directory app\n\nimport * as React from 'react'\nimport {\n DateRangePicker as AriaDateRangePicker,\n DateInput,\n DateSegment,\n Button,\n Dialog,\n RangeCalendar,\n CalendarGrid,\n CalendarCell,\n Heading,\n CalendarGridHeader,\n CalendarHeaderCell,\n CalendarGridBody,\n I18nProvider\n} from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport { CalendarDate, parseDate } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\n\nexport type DateRange = {\n start: string | null\n end: string | null\n}\n\nexport type DateRangePickerProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<DateRange>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: DateRange\n defaultValue?: DateRange\n onChange?: (value: DateRange) => void\n\n // Date specific (ISO date strings)\n minValue?: string\n maxValue?: string\n presetRanges?: Array<{\n label: string\n start: string\n end: string\n }>\n\n // Calendar display\n locale?: string\n shape?: 'rectangle' | 'oval'\n}\n\nexport function DateRangePicker(props: DateRangePickerProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState<DateRange>(\n props.value,\n props.defaultValue ?? { start: null, end: null },\n props.onChange\n )\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by AriaDateRangePicker\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Convert ISO strings to CalendarDate for aria components\n const ariaValue = React.useMemo(() => {\n if (!value.start || !value.end) return null\n try {\n return {\n start: parseDate(value.start),\n end: parseDate(value.end)\n }\n } catch {\n return null\n }\n }, [value])\n\n const minDate = React.useMemo(() => {\n if (!props.minValue) return undefined\n try {\n return parseDate(props.minValue)\n } catch {\n return undefined\n }\n }, [props.minValue])\n\n const maxDate = React.useMemo(() => {\n if (!props.maxValue) return undefined\n try {\n return parseDate(props.maxValue)\n } catch {\n return undefined\n }\n }, [props.maxValue])\n\n const handleRangeChange = (range: { start: CalendarDate; end: CalendarDate } | null) => {\n if (!range) {\n setValue({ start: null, end: null })\n } else {\n setValue({\n start: range.start.toString(),\n end: range.end.toString()\n })\n }\n commitValidation()\n }\n\n const handlePresetClick = (preset: { start: string; end: string }) => {\n setValue({ start: preset.start, end: preset.end })\n commitValidation()\n }\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden inputs for form submission */}\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name ? `${props.name}.start` : undefined}\n value={value.start || ''}\n form={props.form}\n />\n <HeadlessForm.HiddenInput\n name={props.name ? `${props.name}.end` : undefined}\n value={value.end || ''}\n form={props.form}\n />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n {/* Preset ranges */}\n {props.presetRanges && props.presetRanges.length > 0 && (\n <div className=\"flex flex-wrap gap-2 mb-2\">\n {props.presetRanges.map((preset, i) => (\n <button\n key={i}\n type=\"button\"\n onClick={() => handlePresetClick(preset)}\n disabled={props.isDisabled}\n className=\"px-2 py-1 text-xs rounded border border-neutral-200 hover:bg-neutral-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {preset.label}\n </button>\n ))}\n </div>\n )}\n\n {/* Date range picker */}\n <AriaDateRangePicker\n aria-labelledby={labelId}\n value={ariaValue}\n onChange={handleRangeChange}\n minValue={minDate}\n maxValue={maxDate}\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <div className=\"flex items-center justify-start flex-1 px-3 py-2 text-sm\">\n <DateInput slot=\"start\" className=\"flex\">\n {(segment) => (\n <DateSegment\n segment={segment}\n className={tw('px-0.5 tabular-nums outline-none rounded-sm', 'focus:bg-primary-500 focus:text-white')}\n />\n )}\n </DateInput>\n <span className=\"mx-2 text-neutral-400\">&ndash;</span>\n <DateInput slot=\"end\" className=\"flex\">\n {(segment) => (\n <DateSegment\n segment={segment}\n className={tw('px-0.5 tabular-nums outline-none rounded-sm', 'focus:bg-primary-500 focus:text-white')}\n />\n )}\n </DateInput>\n </div>\n <Button className=\"flex items-center px-2 py-2\">\n <Icon name=\"calendar\" className=\"h-4 w-4 text-neutral-400\" />\n </Button>\n </FormFieldComponents.FieldGroup>\n\n <AnimatedPopover>\n <Dialog>\n <RangeCalendar className=\"bg-white border border-neutral-200 rounded-md shadow-lg p-3\">\n <header className=\"flex items-center justify-between mb-2\">\n <Button slot=\"previous\" className=\"p-1 hover:bg-neutral-100 rounded\">\n <Icon name=\"chevron-left\" className=\"h-4 w-4\" />\n </Button>\n <Heading className=\"text-sm font-semibold text-neutral-900 flex-1 text-center\" />\n <Button slot=\"next\" className=\"p-1 hover:bg-neutral-100 rounded\">\n <Icon name=\"chevron-right\" className=\"h-4 w-4\" />\n </Button>\n </header>\n <CalendarGrid>\n <CalendarGridHeader>\n {(day) => (\n <CalendarHeaderCell className=\"text-xs text-neutral-500 font-medium w-8 h-8\">{day}</CalendarHeaderCell>\n )}\n </CalendarGridHeader>\n <CalendarGridBody>\n {(date) => (\n <CalendarCell\n date={date}\n className={({\n isSelected,\n isSelectionStart,\n isSelectionEnd,\n isOutsideMonth,\n isDisabled,\n isUnavailable,\n isFocusVisible\n }) =>\n tw(\n 'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center',\n 'hover:bg-neutral-100',\n isOutsideMonth && 'text-neutral-300',\n (isDisabled || isUnavailable) && 'text-neutral-300 cursor-not-allowed',\n isSelected && !isSelectionStart && !isSelectionEnd && 'bg-primary-500/20',\n (isSelectionStart || isSelectionEnd) && 'bg-primary-500 text-white hover:bg-primary-500',\n isFocusVisible && 'ring-2 ring-primary-500 ring-offset-1'\n )\n }\n />\n )}\n </CalendarGridBody>\n </CalendarGrid>\n </RangeCalendar>\n </Dialog>\n </AnimatedPopover>\n </AriaDateRangePicker>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n"}]},{"name":"date-time-picker","dependencies":["tw","animated-popover","calendar-month-year-picker","form-field","icon"],"files":[{"name":"components/date-time-picker.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add date-time-picker --directory app\n\nimport * as React from 'react'\nimport { DatePicker as AriaDatePicker, DateInput, DateSegment, Button, Dialog, I18nProvider } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport { CalendarDateTime, parseDateTime } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { CustomCalendar } from './helpers/calendar-month-year-picker'\nimport { dateFns, dateTimeFns, type Iso } from 'iso-fns'\n\nexport type DateTimePickerProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<Iso.DateTime | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (ISO datetime string e.g. 2024-01-15T09:30:00)\n value?: Iso.DateTime | null\n defaultValue?: Iso.DateTime | null\n onChange?: (value: Iso.DateTime | null) => void\n\n // Date/Time specific\n minValue?: Iso.DateTime\n maxValue?: Iso.DateTime\n use24Hour?: boolean\n minuteStep?: number\n locale?: string\n shape?: 'rectangle' | 'oval'\n}\n\nexport function DateTimePicker(props: DateTimePickerProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Internal state for CalendarDateTime\n const [value, _setValue] = React.useState(() => {\n try {\n const startingValue = props.value === undefined ? (props.defaultValue ?? null) : props.value\n return startingValue ? parseDateTime(startingValue) : null\n } catch {\n return null\n }\n })\n\n function setValue(dt: CalendarDateTime | null) {\n _setValue(dt)\n if (!dt || dateTimeFns.isValid(dt.toString())) {\n props.onChange && props.onChange(dt ? (dt.toString() as Iso.DateTime) : null)\n }\n commitValidation()\n }\n\n // Sync controlled value\n if (props.value !== undefined) {\n const currentValueAsIso = value && dateTimeFns.isValid(value.toString()) ? value.toString() : null\n const propValue = props.value && dateTimeFns.isValid(props.value) ? props.value : null\n if ((!value || dateTimeFns.isValid(value.toString())) && propValue !== currentValueAsIso) {\n try {\n _setValue(propValue ? parseDateTime(propValue) : null)\n } catch {\n _setValue(null)\n }\n }\n }\n\n // Convert min/max values\n const minDateTime = React.useMemo(() => {\n if (!props.minValue) return undefined\n try {\n return parseDateTime(props.minValue)\n } catch {\n return undefined\n }\n }, [props.minValue])\n\n const maxDateTime = React.useMemo(() => {\n if (!props.maxValue) return undefined\n try {\n return parseDateTime(props.maxValue)\n } catch {\n return undefined\n }\n }, [props.maxValue])\n\n // Form validation\n const {\n validationMessage: validationMessageRaw,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: value && dateTimeFns.isValid(value.toString()) ? (value.toString() as Iso.DateTime) : null,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by AriaDatePicker\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const lessThanMin =\n props.minValue && value && dateTimeFns.isValid(value.toString()) ? value.toString() < props.minValue : false\n const greaterThanMax =\n props.maxValue && value && dateTimeFns.isValid(value.toString()) ? value.toString() > props.maxValue : false\n\n const validationMessage =\n validationMessageRaw ||\n (lessThanMin ? `Cannot be before ${dateTimeFns.format(props.minValue!, 'MM/dd/yyyy h:mm a')}` : '') ||\n (greaterThanMax ? `Cannot be after ${dateTimeFns.format(props.maxValue!, 'MM/dd/yyyy h:mm a')}` : '')\n\n const isInvalid = props.isInvalid || validationInvalid || lessThanMin || greaterThanMax\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value?.toString() ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaDatePicker\n value={value}\n aria-labelledby={labelId}\n aria-describedby={helpTextId}\n onChange={setValue}\n minValue={minDateTime}\n maxValue={maxDateTime}\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n granularity=\"minute\"\n hideTimeZone\n hourCycle={props.use24Hour ? 24 : 12}\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <DateInput className=\"flex-1 grow flex px-3 py-2 text-sm\">\n {(segment) => (\n <DateSegment\n segment={segment}\n className={tw('px-0.5 tabular-nums outline-none rounded-sm', 'focus:bg-primary-500 focus:text-white')}\n />\n )}\n </DateInput>\n <Button className=\"flex items-center px-2 py-2\">\n <Icon name=\"calendar\" className=\"h-4 w-4 text-neutral-400\" />\n </Button>\n </FormFieldComponents.FieldGroup>\n\n <AnimatedPopover>\n <Dialog>\n <CustomCalendar value={value} onChange={setValue} minValue={minDateTime} maxValue={maxDateTime} />\n </Dialog>\n </AnimatedPopover>\n </AriaDatePicker>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n"}]},{"name":"time-input","dependencies":["tw","form-field"],"files":[{"name":"components/time-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add time-input --directory app\n\nimport * as React from 'react'\nimport { TimeField, DateInput, DateSegment, I18nProvider } from 'react-aria-components'\nimport { Time, parseTime } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { timeFns, type Iso } from 'iso-fns'\n\nexport type TimeInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<Iso.Time | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (IsoTimeString format HH:mm:ss)\n value?: Iso.Time | null\n defaultValue?: Iso.Time | null\n onChange?: (value: Iso.Time | null) => void\n\n // Time specific\n use24Hour?: boolean\n minuteStep?: number\n minTime?: Iso.Time\n maxTime?: Iso.Time\n\n // Locale\n locale?: string\n shape?: 'rectangle' | 'oval'\n}\n\nexport function TimeInput(props: TimeInputProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Internal state for Time object\n const [value, _setValue] = React.useState<Time | null>(() => {\n try {\n const startingTime = props.value === undefined ? (props.defaultValue ?? null) : props.value\n return startingTime ? parseTime(startingTime) : null\n } catch {\n return null\n }\n })\n\n function setValue(t: Time | null) {\n _setValue(t)\n const isoTime = t ? (t.toString() as Iso.Time) : null\n props.onChange?.(isoTime)\n commitValidation()\n }\n\n // Sync controlled value\n if (props.value !== undefined) {\n const currentValueAsIso = value && isValidTimeString(value.toString()) ? value.toString() : null\n const propValue = props.value && isValidTimeString(props.value) ? props.value : null\n if ((!value || isValidTimeString(value.toString())) && propValue !== currentValueAsIso) {\n try {\n _setValue(propValue ? parseTime(propValue) : null)\n } catch {\n _setValue(null)\n }\n }\n }\n\n // Convert min/max times\n const minTime = React.useMemo(() => {\n if (!props.minTime) return undefined\n try {\n return parseTime(props.minTime)\n } catch {\n return undefined\n }\n }, [props.minTime])\n\n const maxTime = React.useMemo(() => {\n if (!props.maxTime) return undefined\n try {\n return parseTime(props.maxTime)\n } catch {\n return undefined\n }\n }, [props.maxTime])\n\n // Form validation\n const {\n validationMessage: validationMessageRaw,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: value && isValidTimeString(value.toString()) ? (value.toString() as Iso.Time) : null,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by TimeField\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const lessThanMin =\n props.minTime && value && isValidTimeString(value.toString()) ? value.toString() < props.minTime : false\n const greaterThanMax =\n props.maxTime && value && isValidTimeString(value.toString()) ? value.toString() > props.maxTime : false\n\n const validationMessage =\n validationMessageRaw ||\n (lessThanMin ? `Cannot be before ${formatTimeString(props.minTime!, 'h:mm a')}` : '') ||\n (greaterThanMax ? `Cannot be after ${formatTimeString(props.maxTime!, 'h:mm a')}` : '')\n\n const isInvalid = props.isInvalid || validationInvalid || lessThanMin || greaterThanMax\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value?.toString() ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n {/* Time field */}\n <TimeField\n aria-labelledby={labelId}\n aria-describedby={helpTextId}\n value={value}\n onChange={setValue}\n isRequired={props.isRequired}\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n minValue={minTime}\n maxValue={maxTime}\n hourCycle={props.use24Hour ? 24 : 12}\n granularity=\"minute\"\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n >\n <DateInput className=\"flex w-full bg-transparent px-3 py-2 text-sm\">\n {(segment) => (\n <DateSegment\n segment={segment}\n className={tw(\n 'px-0.5 tabular-nums outline-none rounded-sm',\n 'focus:bg-primary-500 focus:text-white',\n segment.isPlaceholder && 'text-neutral-400'\n )}\n />\n )}\n </DateInput>\n </FormFieldComponents.FieldGroup>\n </TimeField>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n\n/** Check if a string is a valid HH:mm:ss time string */\nfunction isValidTimeString(timeStr: string): boolean {\n return timeFns.isValid(timeStr)\n}\n\n/** Format an HH:mm:ss time string using a date-fns format pattern */\nfunction formatTimeString(timeStr: Iso.Time, fmt: string): string {\n return timeFns.format(timeStr, fmt)\n}\n"}]},{"name":"month-day-input","dependencies":["tw","form-field"],"files":[{"name":"components/month-day-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add month-day-input --directory app\n\nimport * as React from 'react'\nimport { DateField, DateInput as AriaDateInput, DateSegment, I18nProvider } from 'react-aria-components'\nimport { CalendarDate } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { monthDayFns, type Iso } from 'iso-fns'\n\nexport type MonthDayInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<Iso.MonthDay | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (ISO month-day string --MM-DD)\n value?: Iso.MonthDay | null\n defaultValue?: Iso.MonthDay | null\n onChange?: (value: Iso.MonthDay | null) => void\n\n // Locale\n locale?: string\n shape?: 'rectangle' | 'oval'\n}\n\nexport function MonthDayInput(props: MonthDayInputProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Internal state for CalendarDate (using placeholder year 1972)\n const [value, _setValue] = React.useState(() => {\n try {\n const startingDate = props.value === undefined ? (props.defaultValue ?? null) : props.value\n return startingDate ? parseMonthDay(startingDate) : null\n } catch {\n return null\n }\n })\n\n function setValue(d: CalendarDate | null) {\n _setValue(d)\n const monthDay = formatMonthDay(d)\n props.onChange?.(monthDay)\n commitValidation()\n }\n\n // Sync controlled value\n if (props.value !== undefined) {\n const currentMonthDay = formatMonthDay(value)\n if (props.value !== currentMonthDay) {\n try {\n _setValue(props.value ? parseMonthDay(props.value) : null)\n } catch {\n _setValue(null)\n }\n }\n }\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: formatMonthDay(value),\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by DateField\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n value={formatMonthDay(value) ?? ''}\n form={props.form}\n />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n {/* Month/Day input using DateField */}\n <DateField\n aria-labelledby={labelId}\n aria-describedby={helpTextId}\n value={value}\n onChange={setValue}\n placeholderValue={new CalendarDate(1972, 1, 1)}\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n granularity=\"day\"\n hideTimeZone\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n >\n <AriaDateInput className=\"flex px-3 py-2 text-sm [&>[data-literal]:not(:has(~[data-literal]))]:hidden\">\n {(segment) => {\n const isHidden = segment.type === 'year'\n\n return (\n <DateSegment\n segment={segment}\n data-literal={segment.type === 'literal' ? true : undefined}\n className={tw(\n 'px-0.5 tabular-nums outline-none rounded-sm',\n 'focus:bg-primary-500 focus:text-white',\n segment.isPlaceholder && 'text-neutral-400',\n 'last-of-type:hidden',\n isHidden && 'hidden'\n )}\n />\n )\n }}\n </AriaDateInput>\n </FormFieldComponents.FieldGroup>\n </DateField>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n\nfunction parseMonthDay(monthDay: Iso.MonthDay): CalendarDate | null {\n if (!monthDayFns.isValid(monthDay)) return null\n // Use a leap-year placeholder so Feb 29 is valid\n const { month, day } = monthDayFns.getFields(monthDay)\n return new CalendarDate(1972, month, day)\n}\n\nfunction formatMonthDay(date: CalendarDate | null): Iso.MonthDay | null {\n if (!date) return null\n const month = date.month.toString().padStart(2, '0')\n const day = date.day.toString().padStart(2, '0')\n return `--${month}-${day}` as Iso.MonthDay\n}\n"}]},{"name":"year-month-input","dependencies":["tw","form-field"],"files":[{"name":"components/year-month-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add year-month-input --directory app\n\nimport * as React from 'react'\nimport { DateField, DateInput as AriaDateInput, DateSegment, I18nProvider } from 'react-aria-components'\nimport { CalendarDate } from '@internationalized/date'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { yearMonthFns } from 'iso-fns'\n\n/** ISO year-month string in YYYY-MM format */\nexport type YearMonth = string\n\nexport type YearMonthInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<YearMonth | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (ISO year-month string YYYY-MM)\n value?: YearMonth | null\n defaultValue?: YearMonth | null\n onChange?: (value: YearMonth | null) => void\n\n // Locale\n locale?: string\n shape?: 'rectangle' | 'oval'\n}\n\nexport function YearMonthInput(props: YearMonthInputProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Internal state for CalendarDate\n const [value, _setValue] = React.useState(() => {\n try {\n const startingDate = props.value === undefined ? (props.defaultValue ?? null) : props.value\n return startingDate ? parseYearMonth(startingDate) : null\n } catch {\n return null\n }\n })\n\n function setValue(d: CalendarDate | null) {\n _setValue(d)\n const yearMonth = formatYearMonth(d)\n props.onChange?.(yearMonth)\n commitValidation()\n }\n\n // Sync controlled value\n if (props.value !== undefined) {\n const currentYearMonth = formatYearMonth(value)\n if (props.value !== currentYearMonth) {\n try {\n _setValue(props.value ? parseYearMonth(props.value) : null)\n } catch {\n _setValue(null)\n }\n }\n }\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: formatYearMonth(value),\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus handled by DateField\n }\n },\n hiddenInputRef as React.RefObject<HTMLInputElement>\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <I18nProvider locale={props.locale ?? 'en'}>\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n value={formatYearMonth(value) ?? ''}\n form={props.form}\n />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n {/* Year/Month input using DateField */}\n <DateField\n aria-labelledby={labelId}\n aria-describedby={helpTextId}\n value={value}\n onChange={setValue}\n isDisabled={props.isDisabled}\n isReadOnly={props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n granularity=\"day\"\n hideTimeZone\n >\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n >\n <AriaDateInput className=\"flex px-3 py-2 text-sm [&>[data-literal]:not(:has(~[data-literal]))]:hidden\">\n {(segment) => {\n // Hide day segment\n const isHidden = segment.type === 'day'\n\n return (\n <DateSegment\n segment={segment}\n data-literal={segment.type === 'literal' ? true : undefined}\n className={tw(\n 'px-0.5 tabular-nums outline-none rounded-sm',\n 'focus:bg-primary-500 focus:text-white',\n segment.isPlaceholder && 'text-neutral-400',\n isHidden && 'hidden'\n )}\n />\n )\n }}\n </AriaDateInput>\n </FormFieldComponents.FieldGroup>\n </DateField>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </I18nProvider>\n )\n}\n\nfunction parseYearMonth(yearMonth: YearMonth): CalendarDate | null {\n const match = yearMonth.match(/^(\\d{4})-(\\d{2})$/)\n if (!match || match.length < 3) return null\n const year = parseInt(match[1]!, 10)\n const month = parseInt(match[2]!, 10)\n // Use day 1 as placeholder for the CalendarDate\n return new CalendarDate(year, month, 1)\n}\n\nfunction formatYearMonth(date: CalendarDate | null): YearMonth | null {\n if (!date) return null\n const year = date.year.toString().padStart(4, '0')\n const month = date.month.toString().padStart(2, '0')\n const yearMonth = `${year}-${month}`\n return isValidYearMonth(yearMonth) ? yearMonth : null\n}\n\n/** Check if a string is a valid YYYY-MM year-month string */\nfunction isValidYearMonth(yearMonth: string): boolean {\n return yearMonthFns.isValid(yearMonth)\n}\n"}]},{"name":"select","dependencies":["tw","animated-popover","form-field","colors","icon"],"files":[{"name":"components/select.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add select --directory app\n\nimport * as React from 'react'\nimport { Select as AriaSelect, Button, ListBoxItem } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\nimport { getColor, stringToNumber } from '../utils/colors'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type SelectOption = {\n value: string | number | null\n label: string\n secondary?: string\n isDisabled?: boolean\n}\n\nexport type SelectProps<T extends SelectOption = SelectOption> = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<T['value'] | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: T['value'] | null\n defaultValue?: T['value'] | null\n onChange?: (value: T['value'] | null) => void\n\n // Options\n options: T[]\n renderOption?: (props: { option: T; colored: boolean; index: number }) => React.ReactNode\n\n colored?: boolean\n shape?: 'rectangle' | 'oval'\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nexport function Select<T extends SelectOption = SelectOption>(props: SelectProps<T>) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const buttonRef = React.useRef<HTMLButtonElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? null, props.onChange)\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n buttonRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Find the selected option for display\n const selectedOption = React.useMemo(() => {\n return props.options.find((option) => option.value === value) ?? null\n }, [props.options, value])\n\n const selectedKey = value != null ? toKey(value) : null\n\n return (\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaSelect\n aria-labelledby={props.label ? labelId : undefined}\n aria-label={!props.label ? 'Select' : undefined}\n selectedKey={selectedKey}\n onSelectionChange={(key) => {\n const newValue = fromKey(key, props.options)\n setValue(newValue)\n commitValidation()\n }}\n isDisabled={props.isDisabled || props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n >\n {/* Select button */}\n <Button\n ref={buttonRef}\n className={tw(\n 'relative overflow-hidden ring-1 shadow-sm transition-all duration-200 max-w-96',\n props.shape === 'oval' ? 'rounded-full' : 'rounded',\n 'focus:outline-2 focus:-outline-offset-1',\n (props.isDisabled || props.isReadonly) &&\n 'ring-neutral-200 bg-neutral-50 text-neutral-400 shadow-none cursor-not-allowed',\n isInvalid && 'ring-negative-300 bg-negative-50 focus:outline-negative-500',\n hasWarning && 'ring-notice-300 bg-notice-50 focus:outline-notice-500',\n !isInvalid &&\n !hasWarning &&\n !props.isDisabled &&\n !props.isReadonly &&\n 'ring-black/10 hover:ring-black/20 focus:outline-primary-500',\n 'w-full cursor-default py-2 pl-3 pr-10 text-left text-sm max-sm:text-base placeholder:text-neutral-400'\n )}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n >\n {props.colored && selectedOption ? (\n <div\n className=\"absolute left-0 top-0 w-[3px] h-full\"\n style={{ backgroundColor: getColor(stringToNumber(String(selectedOption.value))) }}\n />\n ) : null}\n {selectedOption ? (\n <span className=\"block truncate text-sm min-h-5\">{selectedOption.label}</span>\n ) : (\n <span className=\"block truncate text-sm min-h-5 text-neutral-400 italic\">\n {props.placeholder ?? 'Select an option'}\n </span>\n )}\n <span className=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2\">\n <Icon name=\"chevron-up-down\" className=\"h-4 w-4 text-neutral-400\" />\n </span>\n </Button>\n\n {/* Options dropdown */}\n <AnimatedPopover offset={4} className=\"w-(--trigger-width)\">\n <FormFieldComponents.DropdownListBox\n items={props.options.map((option, index) => ({ ...option, index, id: toKey(option.value) }))}\n renderEmptyState={() => <div className=\"px-3 py-2 text-neutral-500 text-sm\">No options available</div>}\n >\n {(item) => {\n const option = item as T & { index: number; id: string }\n if (props.renderOption) {\n return (\n <ListBoxItem\n id={option.id}\n textValue={option.label}\n isDisabled={option.isDisabled}\n className=\"outline-none\"\n >\n {props.renderOption({ option, colored: props.colored ?? false, index: option.index })}\n </ListBoxItem>\n )\n }\n\n const color = props.colored ? getColor(stringToNumber(String(option.value))) : null\n return (\n <FormFieldComponents.DropdownItem\n id={option.id}\n textValue={option.label}\n label={option.label}\n secondary={option.secondary}\n isDisabled={option.isDisabled}\n color={color}\n />\n )\n }}\n </FormFieldComponents.DropdownListBox>\n </AnimatedPopover>\n </AriaSelect>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────────────────\n\nconst NULL_KEY = '__select_null__'\n\nfunction toKey(value: string | number | null): string {\n if (value === null) return NULL_KEY\n return String(value)\n}\n\nfunction fromKey<T extends SelectOption>(key: React.Key | null, options: T[]): T['value'] | null {\n if (key === null || key === undefined) return null\n const keyStr = String(key)\n if (keyStr === NULL_KEY) return null\n const option = options.find((o) => toKey(o.value) === keyStr)\n return option?.value ?? null\n}\n"}]},{"name":"boolean-select","dependencies":["tw","animated-popover","form-field","colors","icon","select"],"files":[{"name":"components/boolean-select.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add boolean-select --directory app\n\nimport * as React from 'react'\nimport { Select, type SelectProps, type SelectOption } from './select'\n\nexport type BooleanSelectProps = Omit<SelectProps, 'options' | 'value' | 'defaultValue' | 'onChange'> & {\n // Value management\n value?: boolean | null\n defaultValue?: boolean | null\n onChange?: (value: boolean | null) => void\n\n // Customizable labels\n trueLabel?: string\n falseLabel?: string\n nullLabel?: string\n includeNull?: boolean\n}\n\nexport function BooleanSelect({\n trueLabel = 'Yes',\n falseLabel = 'No',\n nullLabel = 'Not Set',\n includeNull = false,\n value,\n defaultValue,\n onChange,\n ...restProps\n}: BooleanSelectProps) {\n const options: SelectOption[] = [\n ...(includeNull ? [{ value: 'null', label: nullLabel }] : []),\n { value: 'true', label: trueLabel },\n { value: 'false', label: falseLabel }\n ]\n\n // Convert boolean value to string for Select component\n const stringValue = value === null ? 'null' : value === undefined ? undefined : String(value)\n const stringDefaultValue = defaultValue === null ? 'null' : defaultValue === undefined ? undefined : String(defaultValue)\n\n // Convert string value back to boolean for onChange\n const handleChange = (val: string | number | null) => {\n if (!onChange) return\n if (val === 'null' || val === null) onChange(null)\n else if (val === 'true') onChange(true)\n else if (val === 'false') onChange(false)\n }\n\n return (\n <Select {...restProps} options={options} value={stringValue} defaultValue={stringDefaultValue} onChange={handleChange} />\n )\n}\n"}]},{"name":"autocomplete","dependencies":["tw","animated-popover","form-field","colors","icon"],"files":[{"name":"components/autocomplete.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add autocomplete --directory app\n\nimport * as React from 'react'\nimport { ComboBox as AriaComboBox, ListBoxItem } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport type { Key } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { getColor, stringToNumber } from '../utils/colors'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { matchSorter } from 'match-sorter'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type AutocompleteOption = {\n value: string | number\n label: string\n secondary?: string\n}\n\nexport type AutocompleteProps<T extends AutocompleteOption = AutocompleteOption> = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<T['value'] | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: T['value'] | null\n defaultValue?: T['value'] | null\n onChange?: (value: T['value'] | null) => void\n\n // Options\n options: T[]\n renderOption?: (props: { option: T; colored: boolean; index: number }) => React.ReactNode\n\n colored?: boolean\n\n // Filtering\n filterFunction?: ((items: readonly T[], value: string) => T[]) | (keyof T)[]\n\n // Loading\n isLoading?: boolean\n\n // Input\n inputAutocomplete?: React.ComponentProps<'input'>['autoComplete']\n id?: string\n shape?: 'rectangle' | 'oval'\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nexport function Autocomplete<T extends AutocompleteOption = AutocompleteOption>(props: AutocompleteProps<T>) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Controlled state for the selected value\n const [selectedValue, setSelectedValue] = HeadlessForm.useControlledState(\n props.value,\n props.defaultValue ?? null,\n props.onChange\n )\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: selectedValue,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Find the selected option\n const selectedOption = React.useMemo(() => {\n return props.options.find((option) => option.value === selectedValue) ?? null\n }, [props.options, selectedValue])\n\n // Input value state (what's displayed in the input)\n const [inputValue, setInputValue] = React.useState(() => {\n const initial = props.value ?? props.defaultValue ?? null\n if (initial != null) {\n const option = props.options.find((o) => o.value === initial)\n return option?.label ?? ''\n }\n return ''\n })\n\n // Sync inputValue when external props.value changes\n const lastPropsValue = React.useRef(props.value)\n React.useEffect(() => {\n if (props.value !== lastPropsValue.current) {\n lastPropsValue.current = props.value\n if (props.value != null) {\n const option = props.options.find((o) => o.value === props.value)\n setInputValue(option?.label ?? '')\n } else {\n setInputValue('')\n }\n }\n }, [props.value, props.options])\n\n // Filter options based on input value\n const filteredOptions = React.useMemo(() => {\n if (props.isLoading) return []\n\n // When input matches the selected option's label, show all options (not filtered)\n const isShowingSelectedLabel = selectedOption && inputValue === selectedOption.label\n const searchTerm = isShowingSelectedLabel ? '' : inputValue\n\n const filterFn = props.filterFunction\n let filtered: T[]\n\n if (typeof filterFn === 'function') {\n filtered = filterFn(props.options, searchTerm).slice(0, 50)\n } else if (Array.isArray(filterFn)) {\n filtered = matchSorter(props.options, searchTerm, {\n keys: filterFn as string[]\n }).slice(0, 50)\n } else {\n filtered = defaultFilterFunction(props.options, searchTerm).slice(0, 50)\n }\n\n // Ensure selected option is always included\n if (selectedOption && !filtered.some((o) => o.value === selectedOption.value)) {\n filtered = [selectedOption, ...filtered]\n }\n\n return filtered\n }, [props.options, inputValue, props.filterFunction, props.isLoading, selectedOption])\n\n // Items for React Aria (with id for key management)\n const items = React.useMemo(\n () => filteredOptions.map((option, index) => ({ ...option, index, id: String(option.value) })),\n [filteredOptions]\n )\n\n // Selected key for React Aria\n const selectedKey = selectedValue != null ? String(selectedValue) : null\n\n const handleSelectionChange = (key: Key | null) => {\n if (key === null) {\n setSelectedValue(null)\n setInputValue('')\n } else {\n const option = props.options.find((o) => String(o.value) === String(key))\n setSelectedValue(option?.value ?? null)\n setInputValue(option?.label ?? '')\n }\n commitValidation()\n }\n\n const handleInputChange = (value: string) => {\n setInputValue(value)\n // Clear selection when input doesn't match any option label\n const matchingOption = props.options.find((o) => o.label === value)\n if (!matchingOption) {\n setSelectedValue(null)\n }\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={selectedValue ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaComboBox\n aria-labelledby={props.label ? labelId : undefined}\n aria-label={!props.label ? 'Autocomplete' : undefined}\n selectedKey={selectedKey}\n onSelectionChange={handleSelectionChange}\n inputValue={inputValue}\n onInputChange={handleInputChange}\n items={items}\n isDisabled={props.isDisabled || props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n allowsEmptyCollection\n menuTrigger=\"focus\"\n onBlur={() => commitValidation()}\n >\n {/* Input and button wrapper */}\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex w-full items-center cursor-default\"\n >\n {props.colored && selectedOption ? (\n <div\n className=\"absolute left-0 top-0 w-[3px] h-full\"\n style={{ backgroundColor: getColor(stringToNumber(String(selectedOption.value))) }}\n />\n ) : null}\n <FormFieldComponents.FieldInput\n ref={inputRef}\n autoComplete={props.inputAutocomplete ?? 'off'}\n className=\"pr-1\"\n placeholder={props.placeholder ?? undefined}\n aria-describedby={helpTextId}\n id={props.id}\n />\n <FormFieldComponents.ChevronButton />\n </FormFieldComponents.FieldGroup>\n\n {/* Dropdown */}\n <AnimatedPopover offset={4} className=\"w-(--trigger-width)\">\n <FormFieldComponents.DropdownListBox\n renderEmptyState={() => (\n <div className=\"px-3 py-2 text-neutral-500 text-sm\">\n {props.isLoading ? 'Loading...' : inputValue ? 'No results found' : 'No options available'}\n </div>\n )}\n >\n {(item) => {\n const option = item as T & { index: number; id: string }\n if (props.renderOption) {\n return (\n <ListBoxItem id={option.id} textValue={option.label} className=\"outline-none\">\n {props.renderOption({ option, colored: props.colored ?? false, index: option.index })}\n </ListBoxItem>\n )\n }\n\n const color = props.colored ? getColor(stringToNumber(String(option.value))) : null\n return (\n <FormFieldComponents.DropdownItem\n id={option.id}\n textValue={option.label}\n label={option.label}\n secondary={option.secondary}\n color={color}\n />\n )\n }}\n </FormFieldComponents.DropdownListBox>\n </AnimatedPopover>\n </AriaComboBox>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────────\n\nfunction defaultFilterFunction<T extends AutocompleteOption>(options: readonly T[], inputValue: string): T[] {\n if (!inputValue) return [...options]\n return matchSorter(options, inputValue, {\n keys: ['label', 'secondary']\n })\n}\n"}]},{"name":"autosuggest","dependencies":["tw","animated-popover","form-field","icon"],"files":[{"name":"components/autosuggest.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add autosuggest --directory app\n\nimport * as React from 'react'\nimport { ComboBox as AriaComboBox, ListBoxItem } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport type { Key } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { matchSorter } from 'match-sorter'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type AutosuggestOption = {\n value: string\n label: string\n secondary?: string\n}\n\nexport type AutosuggestProps<T extends AutosuggestOption = AutosuggestOption> = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: string | null\n defaultValue?: string | null\n onChange?: (value: string | null) => void\n onCustomValue?: (value: string) => void\n\n // Options\n options: T[]\n renderOption?: (props: { option: T; isCustom: boolean; index: number }) => React.ReactNode\n\n // Filtering\n filterFunction?: ((items: readonly T[], value: string) => T[]) | (keyof T)[]\n\n // Loading\n isLoading?: boolean\n\n // Input\n inputAutocomplete?: React.ComponentProps<'input'>['autoComplete']\n id?: string\n\n // Shape\n shape?: 'rectangle' | 'oval'\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nconst CUSTOM_VALUE_KEY = '__autosuggest_custom__'\n\nexport function Autosuggest<T extends AutosuggestOption = AutosuggestOption>(props: AutosuggestProps<T>) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n // Controlled state for the selected value\n const [selectedValue, setSelectedValue] = HeadlessForm.useControlledState(\n props.value,\n props.defaultValue ?? null,\n props.onChange\n )\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: selectedValue,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Find if selected value matches an option\n const selectedOption = React.useMemo(() => {\n return props.options.find((option) => option.value === selectedValue) ?? null\n }, [props.options, selectedValue])\n\n // Input value state (what's displayed in the input)\n const [inputValue, setInputValue] = React.useState(() => {\n const initial = props.value ?? props.defaultValue ?? null\n if (initial != null) {\n const option = props.options.find((o) => o.value === initial)\n return option?.label ?? initial\n }\n return ''\n })\n\n // Sync inputValue when external props.value changes\n const lastPropsValue = React.useRef(props.value)\n React.useEffect(() => {\n if (props.value !== lastPropsValue.current) {\n lastPropsValue.current = props.value\n if (props.value != null) {\n const option = props.options.find((o) => o.value === props.value)\n setInputValue(option?.label ?? props.value)\n } else {\n setInputValue('')\n }\n }\n }, [props.value, props.options])\n\n // Filter options based on input value\n const filteredOptions = React.useMemo(() => {\n if (props.isLoading) return []\n\n // When input matches the selected option's label, show all options (not filtered)\n const isShowingSelectedLabel = selectedOption && inputValue === selectedOption.label\n const searchTerm = isShowingSelectedLabel ? '' : inputValue\n\n const filterFn = props.filterFunction\n let filtered: T[]\n\n if (typeof filterFn === 'function') {\n filtered = filterFn(props.options, searchTerm).slice(0, 50)\n } else if (Array.isArray(filterFn)) {\n filtered = matchSorter(props.options, searchTerm, {\n keys: filterFn as string[]\n }).slice(0, 50)\n } else {\n filtered = defaultFilterFunction(props.options, searchTerm).slice(0, 50)\n }\n\n return filtered\n }, [props.options, inputValue, props.filterFunction, props.isLoading, selectedOption])\n\n // Check if we should show a custom value option\n const showCustomOption = React.useMemo(() => {\n if (!inputValue || props.isLoading) return false\n return !filteredOptions.some((option) => option.label === inputValue || option.value === inputValue)\n }, [inputValue, filteredOptions, props.isLoading])\n\n // Items for React Aria (with id for key management), including custom option\n const items = React.useMemo(() => {\n const mapped = filteredOptions.map((option, index) => ({\n ...option,\n index,\n id: option.value,\n isCustom: false\n }))\n\n if (showCustomOption) {\n return [\n { value: inputValue, label: inputValue, index: -1, id: CUSTOM_VALUE_KEY, isCustom: true } as T & {\n index: number\n id: string\n isCustom: boolean\n },\n ...mapped\n ]\n }\n\n return mapped\n }, [filteredOptions, showCustomOption, inputValue])\n\n // Selected key for React Aria\n const selectedKey = selectedValue != null ? (selectedOption ? selectedOption.value : CUSTOM_VALUE_KEY) : null\n\n const handleSelectionChange = (key: Key | null) => {\n if (key === null) {\n // Don't clear - keep the custom value in input\n return\n }\n\n if (key === CUSTOM_VALUE_KEY) {\n // Custom value selected\n setSelectedValue(inputValue)\n props.onCustomValue?.(inputValue)\n } else {\n const option = props.options.find((o) => o.value === String(key))\n setSelectedValue(option?.value ?? null)\n setInputValue(option?.label ?? '')\n }\n commitValidation()\n }\n\n const handleInputChange = (value: string) => {\n setInputValue(value)\n // Update selected value to the input value (allows custom values)\n setSelectedValue(value || null)\n }\n\n const handleBlur = () => {\n // Keep the custom value if entered\n if (inputValue && !selectedOption) {\n setSelectedValue(inputValue)\n props.onCustomValue?.(inputValue)\n }\n commitValidation()\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden input for form submission */}\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={selectedValue ?? ''} form={props.form} />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaComboBox\n aria-labelledby={props.label ? labelId : undefined}\n aria-label={!props.label ? 'Autosuggest' : undefined}\n selectedKey={selectedKey}\n onSelectionChange={handleSelectionChange}\n inputValue={inputValue}\n onInputChange={handleInputChange}\n items={items}\n isDisabled={props.isDisabled || props.isReadonly}\n isInvalid={isInvalid}\n isRequired={props.isRequired}\n allowsCustomValue\n allowsEmptyCollection\n menuTrigger=\"focus\"\n onBlur={handleBlur}\n >\n {/* Input and button wrapper */}\n <FormFieldComponents.FieldGroup\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex w-full items-center cursor-default\"\n >\n <FormFieldComponents.FieldInput\n ref={inputRef}\n autoComplete={props.inputAutocomplete ?? 'off'}\n className=\"pr-1\"\n placeholder={props.placeholder ?? undefined}\n aria-describedby={helpTextId}\n id={props.id}\n />\n <FormFieldComponents.ChevronButton />\n </FormFieldComponents.FieldGroup>\n\n {/* Dropdown */}\n <AnimatedPopover offset={4} className=\"w-(--trigger-width)\">\n <FormFieldComponents.DropdownListBox\n renderEmptyState={() => (\n <div className=\"px-3 py-2 text-neutral-500 text-sm\">\n {props.isLoading ? 'Loading...' : inputValue ? 'No matching suggestions' : 'No options available'}\n </div>\n )}\n >\n {(item) => {\n const option = item as T & { index: number; id: string; isCustom: boolean }\n if (props.renderOption) {\n return (\n <ListBoxItem id={option.id} textValue={option.label} className=\"outline-none\">\n {props.renderOption({ option, isCustom: option.isCustom, index: option.index })}\n </ListBoxItem>\n )\n }\n\n return (\n <FormFieldComponents.DropdownItem\n id={option.id}\n textValue={option.label}\n label={option.label}\n secondary={option.secondary}\n customValuePrefix={option.isCustom ? 'Use' : undefined}\n />\n )\n }}\n </FormFieldComponents.DropdownListBox>\n </AnimatedPopover>\n </AriaComboBox>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction defaultFilterFunction<T extends AutosuggestOption>(options: readonly T[], inputValue: string): T[] {\n if (!inputValue) return [...options]\n return matchSorter(options, inputValue, {\n keys: ['label', 'secondary']\n })\n}\n"}]},{"name":"multiselect","dependencies":["tw","animated-popover","form-field","colors","compose-refs","icon"],"files":[{"name":"components/multiselect.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add multiselect --directory app\n\nimport * as React from 'react'\nimport { ListBox, ListBoxItem } from 'react-aria-components'\nimport type { Selection } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { getColor, stringToNumber } from '../utils/colors'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { useElementSize } from '../utils/use-element-size'\nimport { composeRefs } from '../utils/compose-refs'\nimport { Icon } from './icon'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type MultiselectOption = {\n value: string | number\n label: string\n secondary?: string\n isDisabled?: boolean\n}\n\nexport type MultiselectProps<T extends MultiselectOption = MultiselectOption> = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<T['value'][]>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: T['value'][]\n defaultValue?: T['value'][]\n onChange?: (value: T['value'][]) => void\n\n // Options\n options: T[]\n showSelectAll?: boolean\n renderOption?: (props: { option: T; selected: boolean; colored: boolean; index: number }) => React.ReactNode\n\n colored?: boolean\n shape?: 'rectangle' | 'oval'\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nexport function Multiselect<T extends MultiselectOption = MultiselectOption>(props: MultiselectProps<T>) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const buttonRef = React.useRef<HTMLButtonElement>(null)\n const popoverContentRef = React.useRef<HTMLDivElement>(null)\n const [containerRef, setContainerRef] = React.useState<HTMLButtonElement | null>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n const [isOpen, setIsOpen] = React.useState(false)\n\n // Controlled state management\n const [selectedValues, setSelectedValues] = HeadlessForm.useControlledState(\n props.value,\n props.defaultValue ?? [],\n props.onChange\n )\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: selectedValues,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n buttonRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n // Close dropdown on outside click\n React.useEffect(() => {\n if (!isOpen) return\n const handlePointerDown = (e: PointerEvent) => {\n const target = e.target as Node\n if (popoverContentRef.current?.contains(target) || buttonRef.current?.contains(target)) return\n setIsOpen(false)\n commitValidation()\n }\n document.addEventListener('pointerdown', handlePointerDown)\n return () => document.removeEventListener('pointerdown', handlePointerDown)\n })\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n // Find selected options\n const selectedOptions = React.useMemo(() => {\n return props.options.filter((option) => selectedValues.includes(option.value))\n }, [props.options, selectedValues])\n\n const containerSize = useElementSize(containerRef)\n\n const handleRemoveOption = (option: T) => {\n const newValues = selectedValues.filter((v) => v !== option.value)\n setSelectedValues(newValues)\n commitValidation()\n }\n\n const handleSelectAll = () => {\n const availableOptions = props.options.filter((o) => !o.isDisabled)\n if (selectedValues.length === availableOptions.length) {\n setSelectedValues([])\n } else {\n setSelectedValues(availableOptions.map((o) => o.value))\n }\n commitValidation()\n }\n\n // Convert selectedValues to a Set<string> for React Aria\n const selectedKeys = React.useMemo(() => {\n return new Set(selectedValues.map((v) => String(v)))\n }, [selectedValues])\n\n const handleSelectionChange = (keys: Selection) => {\n if (keys === 'all') {\n const availableOptions = props.options.filter((o) => !o.isDisabled)\n setSelectedValues(availableOptions.map((o) => o.value))\n } else {\n const keySet = keys as Set<string>\n const newValues = props.options.filter((o) => keySet.has(String(o.value))).map((o) => o.value)\n setSelectedValues(newValues)\n }\n commitValidation()\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden inputs for form submission */}\n {selectedValues.map((value, i) => (\n <HeadlessForm.HiddenInput\n key={i}\n ref={i === 0 ? hiddenInputRef : undefined}\n name={props.name}\n value={value}\n form={props.form}\n />\n ))}\n {selectedValues.length === 0 && (\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value=\"[]\" form={props.form} />\n )}\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n {/* Trigger button */}\n <button\n ref={composeRefs(buttonRef, setContainerRef)}\n type=\"button\"\n role=\"combobox\"\n aria-expanded={isOpen}\n aria-haspopup=\"listbox\"\n aria-labelledby={props.label ? labelId : undefined}\n aria-label={!props.label ? 'Multiselect' : undefined}\n aria-invalid={isInvalid || undefined}\n aria-describedby={helpTextId}\n disabled={props.isDisabled}\n onClick={() => {\n if (!props.isDisabled) setIsOpen((prev) => !prev)\n }}\n onBlur={() => {\n if (!isOpen) {\n commitValidation()\n }\n }}\n className={tw(\n 'relative overflow-hidden border transition-all duration-200 max-w-96',\n props.shape === 'oval' ? 'rounded-full' : 'rounded',\n 'focus:outline-none focus:ring-1',\n props.isDisabled && 'border-neutral-100 bg-neutral-50 text-neutral-400 cursor-not-allowed',\n isInvalid && 'border-negative-200 bg-negative-50 focus:border-negative-500 focus:ring-negative-500/20',\n hasWarning && 'border-notice-200 bg-notice-50 focus:border-notice-500 focus:ring-notice-500/20',\n !isInvalid &&\n !hasWarning &&\n !props.isDisabled &&\n 'border-neutral-200 hover:border-neutral-300 focus:border-primary-500 focus:ring-primary-500/20',\n 'w-full cursor-default bg-white py-2 pl-3 pr-10 text-left'\n )}\n >\n <div className=\"block text-sm min-h-5\">\n {selectedOptions.length === 0 ? (\n <span className=\"text-neutral-300 italic\">{props.placeholder ?? 'Select options'}</span>\n ) : (\n <div className=\"flex flex-wrap gap-1.5\">\n {selectedOptions.map((option) => {\n const chipColor = props.colored ? getColor(stringToNumber(String(option.value))) : undefined\n return (\n <span\n key={option.value}\n className={tw(\n 'inline-flex items-center rounded-full font-normal transition-colors max-w-full px-2 py-0.5 text-xs',\n !chipColor && 'bg-primary-500/10 text-primary-500',\n props.isDisabled && 'opacity-50'\n )}\n style={\n chipColor\n ? {\n backgroundColor: `color-mix(in srgb, ${chipColor} 15%, white)`,\n color: `color-mix(in srgb, ${chipColor} 50%, black)`\n }\n : undefined\n }\n >\n <span className=\"truncate\">{option.label}</span>\n {!props.isDisabled && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n handleRemoveOption(option)\n }}\n className=\"inline-flex items-center justify-center rounded-full transition-colors translate-x-1 h-4 w-4 -m-0.5 p-0.5 hover:bg-black/10\"\n aria-label={`Remove ${option.label}`}\n tabIndex={-1}\n >\n <Icon name=\"x-mark\" className=\"h-2 w-2\" />\n </button>\n )}\n </span>\n )\n })}\n </div>\n )}\n </div>\n <span className=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2\">\n <Icon name=\"chevron-up-down\" className=\"h-4 w-4 text-neutral-400\" />\n </span>\n </button>\n\n {/* Options dropdown */}\n {isOpen && !props.isDisabled && (\n <div\n ref={popoverContentRef}\n className={tw(\n 'absolute z-50 mt-1 overflow-hidden rounded bg-white border border-neutral-200 shadow-lg text-sm w-full focus:outline-none'\n )}\n style={{ width: containerSize.width, minWidth: containerSize.width }}\n >\n {props.showSelectAll && (\n <button\n type=\"button\"\n onClick={handleSelectAll}\n className=\"w-full px-3 py-2 text-left hover:bg-neutral-50 border-b border-neutral-100 text-sm font-medium\"\n >\n {selectedValues.length === props.options.filter((o) => !o.isDisabled).length ? 'Deselect All' : 'Select All'}\n </button>\n )}\n <ListBox\n aria-label={props.label ? String(props.label) : 'Multiselect'}\n selectionMode=\"multiple\"\n selectedKeys={selectedKeys}\n onSelectionChange={handleSelectionChange}\n className=\"max-h-60 overflow-y-auto focus:outline-none\"\n items={props.options.map((option, index) => ({ ...option, index, id: String(option.value) }))}\n renderEmptyState={() => <div className=\"px-3 py-2 text-neutral-500 text-sm\">No options available</div>}\n >\n {(item) => {\n const option = item as T & { index: number; id: string }\n const color = props.colored ? getColor(stringToNumber(String(option.value))) : null\n\n if (props.renderOption) {\n return (\n <ListBoxItem\n id={option.id}\n textValue={option.label}\n isDisabled={option.isDisabled}\n className=\"outline-none\"\n >\n {({ isSelected }) => (\n <>\n {props.renderOption!({\n option,\n selected: isSelected,\n colored: props.colored ?? false,\n index: option.index\n })}\n </>\n )}\n </ListBoxItem>\n )\n }\n\n return (\n <FormFieldComponents.DropdownItem\n id={option.id}\n textValue={option.label}\n label={option.label}\n secondary={option.secondary}\n isDisabled={option.isDisabled}\n color={color}\n className=\"px-3\"\n />\n )\n }}\n </ListBox>\n </div>\n )}\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n"},{"name":"utils/use-element-size.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add multiselect --directory app\n\nimport { useMemo, useEffect, useReducer } from 'react'\n\nfunction computeSize(element: HTMLElement | null) {\n if (element === null) return { width: 0, height: 0 }\n const { width, height } = element.getBoundingClientRect()\n return { width, height }\n}\n\nexport function useElementSize(element: HTMLElement | null, unit?: false): { width: number; height: number }\nexport function useElementSize(element: HTMLElement | null, unit: true): { width: string; height: string }\nexport function useElementSize(element: HTMLElement | null, unit = false) {\n const [identity, forceRerender] = useReducer(() => ({}), {})\n\n const size = useMemo(() => computeSize(element), [element, identity])\n\n useEffect(() => {\n if (!element) return\n\n const observer = new ResizeObserver(forceRerender)\n observer.observe(element)\n\n return () => {\n observer.disconnect()\n }\n }, [element])\n\n if (unit) {\n return {\n width: `${size.width}px`,\n height: `${size.height}px`\n }\n }\n\n return size\n}\n"}]},{"name":"multicomplete","dependencies":["tw","animated-popover","form-field","colors","icon"],"files":[{"name":"components/multicomplete.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add multicomplete --directory app\n\nimport * as React from 'react'\nimport { ComboBox as AriaComboBox, Input, ListBox, ListBoxItem } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport type { Key } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { getColor, stringToNumber } from '../utils/colors'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { matchSorter } from 'match-sorter'\nimport { Icon } from './icon'\nimport { useElementSize } from '../utils/use-element-size'\nimport { composeRefs } from '../utils/compose-refs'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type MulticompleteOption = {\n value: string | number\n label: string\n secondary?: string\n}\n\nexport type MulticompleteProps<T extends MulticompleteOption = MulticompleteOption> = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<T['value'][]>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: T['value'][]\n defaultValue?: T['value'][]\n onChange?: (value: T['value'][]) => void\n\n // Options\n options: T[]\n renderOption?: (props: { option: T; colored: boolean; index: number }) => React.ReactNode\n\n colored?: boolean\n\n // Filtering\n filterFunction?: ((items: readonly T[], value: string) => T[]) | (keyof T)[]\n\n // Loading\n isLoading?: boolean\n\n // Input\n inputAutocomplete?: React.ComponentProps<'input'>['autoComplete']\n id?: string\n shape?: 'rectangle' | 'oval'\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nconst EMPTY_ARR: never[] = []\n\nexport function Multicomplete<T extends MulticompleteOption = MulticompleteOption>(props: MulticompleteProps<T>) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const fieldGroupRef = React.useRef<HTMLDivElement>(null)\n const [fieldGroupEl, setFieldGroupEl] = React.useState<HTMLDivElement | null>(null)\n const fieldGroupSize = useElementSize(fieldGroupEl)\n const helpTextId = React.useId()\n const labelId = React.useId()\n const [query, setQuery] = React.useState('')\n\n // Controlled state management (array of values)\n const [selectedValues, setSelectedValues] = HeadlessForm.useControlledState(\n props.value,\n props.defaultValue ?? EMPTY_ARR,\n props.onChange\n )\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: selectedValues,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const isInteractive = !props.isDisabled && !props.isReadonly\n\n // Find selected options\n const selectedOptions = React.useMemo(() => {\n return props.options.filter((option) => selectedValues.includes(option.value))\n }, [props.options, selectedValues])\n\n // Filter unselected options based on query\n const availableOptions = React.useMemo(() => {\n if (props.isLoading) return []\n\n const unselected = props.options.filter((option) => !selectedValues.includes(option.value))\n const filterFn = props.filterFunction\n\n if (typeof filterFn === 'function') {\n return filterFn(unselected, query).slice(0, 50)\n }\n\n if (Array.isArray(filterFn)) {\n return matchSorter(unselected, query, {\n keys: filterFn as string[]\n }).slice(0, 50)\n }\n\n return defaultFilterFunction(unselected, query).slice(0, 50)\n }, [props.options, selectedValues, query, props.filterFunction, props.isLoading])\n\n // Items for React Aria (with id for key management)\n const items = React.useMemo(\n () => availableOptions.map((option, index) => ({ ...option, index, id: String(option.value) })),\n [availableOptions]\n )\n\n const handleRemoveOption = (value: T['value']) => {\n const newValues = selectedValues.filter((v) => v !== value)\n setSelectedValues(newValues)\n commitValidation()\n }\n\n const handleSelectionChange = (key: Key | null) => {\n if (key === null) return\n const option = props.options.find((o) => String(o.value) === String(key))\n if (option && !selectedValues.includes(option.value)) {\n setSelectedValues([...selectedValues, option.value])\n commitValidation()\n }\n setQuery('')\n }\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === 'Backspace' && !query && selectedOptions.length > 0) {\n e.preventDefault()\n handleRemoveOption(selectedOptions[selectedOptions.length - 1]!.value)\n }\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden inputs for form submission */}\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n value={JSON.stringify(selectedValues)}\n form={props.form}\n />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaComboBox\n aria-labelledby={props.label ? labelId : undefined}\n aria-label={!props.label ? 'Multicomplete' : undefined}\n selectedKey={null}\n onSelectionChange={handleSelectionChange}\n inputValue={query}\n onInputChange={setQuery}\n items={items}\n isDisabled={props.isDisabled || props.isReadonly}\n isInvalid={isInvalid}\n allowsEmptyCollection\n menuTrigger=\"focus\"\n onBlur={() => commitValidation()}\n onKeyDown={handleKeyDown}\n >\n {/* Input container with chips */}\n <FormFieldComponents.FieldGroup\n ref={composeRefs(fieldGroupRef, setFieldGroupEl)}\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <div className=\"flex flex-wrap items-center gap-1.5 px-3 py-1.5 w-full\">\n {/* Display selected chips */}\n {selectedOptions.map((option) => {\n const chipColor = props.colored ? getColor(stringToNumber(String(option.value))) : undefined\n return (\n <span\n key={option.value}\n className={tw(\n 'inline-flex items-center rounded-full font-normal transition-colors max-w-full px-2 py-0.5 text-xs',\n !chipColor && 'bg-primary-500/10 text-primary-500',\n props.isDisabled && 'opacity-50'\n )}\n style={\n chipColor\n ? {\n backgroundColor: `color-mix(in srgb, ${chipColor} 15%, white)`,\n color: `color-mix(in srgb, ${chipColor} 50%, black)`\n }\n : undefined\n }\n >\n <span className=\"truncate\">{option.label}</span>\n {isInteractive && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n handleRemoveOption(option.value)\n }}\n className=\"inline-flex items-center justify-center rounded-full transition-colors translate-x-1 h-4 w-4 -m-0.5 p-0.5 hover:bg-black/10\"\n aria-label={`Remove ${option.label}`}\n tabIndex={-1}\n >\n <Icon name=\"x-mark\" className=\"h-2 w-2\" />\n </button>\n )}\n </span>\n )\n })}\n\n {/* Search input */}\n <Input\n ref={inputRef}\n autoComplete={props.inputAutocomplete ?? 'off'}\n className=\"flex-1 min-w-[120px] px-0 bg-transparent border-0 focus:ring-0 focus:outline-none text-sm text-neutral-900 placeholder:text-neutral-400 py-0.5 disabled:cursor-not-allowed\"\n placeholder={selectedOptions.length === 0 ? (props.placeholder ?? 'Search to add...') : ''}\n aria-describedby={helpTextId}\n id={props.id}\n />\n </div>\n <FormFieldComponents.ChevronButton className=\"py-0\" />\n </FormFieldComponents.FieldGroup>\n\n {/* Options dropdown */}\n <AnimatedPopover\n triggerRef={fieldGroupRef}\n offset={4}\n style={{ width: fieldGroupSize.width, minWidth: fieldGroupSize.width }}\n >\n <div\n className={tw(\n 'overflow-hidden rounded bg-white border border-neutral-200 shadow-lg text-sm w-full focus:outline-none'\n )}\n >\n <ListBox\n aria-label={props.label ? String(props.label) : 'Multicomplete'}\n className=\"max-h-60 overflow-y-auto focus:outline-none\"\n renderEmptyState={() => (\n <div className=\"px-3 py-2 text-neutral-500 text-sm\">\n {props.isLoading ? 'Loading...' : query ? 'No results found' : 'No options available'}\n </div>\n )}\n >\n {(item) => {\n const option = item as T & { index: number; id: string }\n const color = props.colored ? getColor(stringToNumber(String(option.value))) : null\n\n if (props.renderOption) {\n return (\n <ListBoxItem id={option.id} textValue={option.label} className=\"outline-none\">\n {props.renderOption({ option, colored: props.colored ?? false, index: option.index })}\n </ListBoxItem>\n )\n }\n\n return (\n <FormFieldComponents.DropdownItem\n id={option.id}\n textValue={option.label}\n label={option.label}\n secondary={option.secondary}\n color={color}\n showCheckIcon={false}\n className=\"px-3\"\n />\n )\n }}\n </ListBox>\n </div>\n </AnimatedPopover>\n </AriaComboBox>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────────\n\nfunction defaultFilterFunction<T extends MulticompleteOption>(options: readonly T[], inputValue: string): T[] {\n if (!inputValue) return [...options]\n return matchSorter(options, inputValue, {\n keys: ['label', 'secondary']\n })\n}\n"}]},{"name":"multisuggest","dependencies":["tw","animated-popover","form-field","icon"],"files":[{"name":"components/multisuggest.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add multisuggest --directory app\n\nimport * as React from 'react'\nimport { ComboBox as AriaComboBox, Input, ListBox, ListBoxItem } from 'react-aria-components'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport type { Key } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { matchSorter } from 'match-sorter'\nimport { Icon } from './icon'\nimport { useElementSize } from '../utils/use-element-size'\nimport { composeRefs } from '../utils/compose-refs'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type MultisuggestOption = {\n value: string\n label: string\n secondary?: string\n}\n\nexport type MultisuggestProps<T extends MultisuggestOption = MultisuggestOption> = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n placeholder?: string\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string[]>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management\n value?: string[]\n defaultValue?: string[]\n onChange?: (value: string[]) => void\n onCustomValue?: (value: string) => void\n\n // Options\n options: T[]\n renderOption?: (props: { option: T; isCustom: boolean; index: number }) => React.ReactNode\n\n // Filtering\n filterFunction?: ((items: readonly T[], value: string) => T[]) | (keyof T)[]\n\n // Loading\n isLoading?: boolean\n\n // Input\n inputAutocomplete?: React.ComponentProps<'input'>['autoComplete']\n id?: string\n shape?: 'rectangle' | 'oval'\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nconst CUSTOM_VALUE_KEY = '__multisuggest_custom__'\nconst EMPTY_ARR: never[] = []\n\nexport function Multisuggest<T extends MultisuggestOption = MultisuggestOption>(props: MultisuggestProps<T>) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const fieldGroupRef = React.useRef<HTMLDivElement>(null)\n const [fieldGroupEl, setFieldGroupEl] = React.useState<HTMLDivElement | null>(null)\n const fieldGroupSize = useElementSize(fieldGroupEl)\n const helpTextId = React.useId()\n const labelId = React.useId()\n const [query, setQuery] = React.useState('')\n const queryRef = React.useRef(query)\n queryRef.current = query\n\n // Controlled state management (array of values)\n const [selectedValues, setSelectedValues] = HeadlessForm.useControlledState(\n props.value,\n props.defaultValue ?? EMPTY_ARR,\n props.onChange\n )\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: selectedValues,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const isInteractive = !props.isDisabled && !props.isReadonly\n\n // Find selected options (both from options list and custom values)\n const selectedOptions = React.useMemo(() => {\n return selectedValues.map((val) => {\n const existingOption = props.options.find((opt) => opt.value === val)\n if (existingOption) return existingOption\n return { value: val, label: val } as T\n })\n }, [props.options, selectedValues])\n\n // Filter unselected options based on query\n const availableOptions = React.useMemo(() => {\n if (props.isLoading) return []\n\n const unselected = props.options.filter((option) => !selectedValues.includes(option.value))\n const filterFn = props.filterFunction\n\n if (typeof filterFn === 'function') {\n return filterFn(unselected, query).slice(0, 50)\n }\n\n if (Array.isArray(filterFn)) {\n return matchSorter(unselected, query, {\n keys: filterFn as string[]\n }).slice(0, 50)\n }\n\n return defaultFilterFunction(unselected, query).slice(0, 50)\n }, [props.options, selectedValues, query, props.filterFunction, props.isLoading])\n\n // Check if we should show a custom value option\n const showCustomOption = React.useMemo(() => {\n if (!query || props.isLoading) return false\n const queryNotInOptions = !availableOptions.some((option) => option.label === query || option.value === query)\n const queryNotSelected = !selectedValues.includes(query)\n return queryNotInOptions && queryNotSelected\n }, [query, availableOptions, selectedValues, props.isLoading])\n\n // Items for React Aria (with id for key management), including custom option\n const items = React.useMemo(() => {\n const mapped = availableOptions.map((option, index) => ({\n ...option,\n index,\n id: option.value,\n isCustom: false\n }))\n\n if (showCustomOption) {\n return [\n { value: query, label: query, index: -1, id: CUSTOM_VALUE_KEY, isCustom: true } as T & {\n index: number\n id: string\n isCustom: boolean\n },\n ...mapped\n ]\n }\n\n return mapped\n }, [availableOptions, showCustomOption, query])\n\n const handleRemoveOption = (value: string) => {\n const newValues = selectedValues.filter((v) => v !== value)\n setSelectedValues(newValues)\n commitValidation()\n }\n\n const addValue = (val: string, isCustom: boolean) => {\n if (selectedValues.includes(val)) return\n setSelectedValues([...selectedValues, val])\n if (isCustom) props.onCustomValue?.(val)\n setQuery('')\n commitValidation()\n }\n\n const handleSelectionChange = (key: Key | null) => {\n if (key === null) return\n const keyStr = String(key)\n\n if (keyStr === CUSTOM_VALUE_KEY) {\n addValue(queryRef.current, true)\n } else {\n const option = props.options.find((o) => o.value === keyStr)\n if (option && !selectedValues.includes(option.value)) {\n addValue(option.value, false)\n }\n }\n }\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === 'Backspace' && !query && selectedOptions.length > 0) {\n e.preventDefault()\n handleRemoveOption(selectedOptions[selectedOptions.length - 1]!.value)\n }\n }\n\n return (\n <div className=\"relative\">\n {/* Hidden inputs for form submission */}\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n value={JSON.stringify(selectedValues)}\n form={props.form}\n />\n\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />\n\n <AriaComboBox\n aria-labelledby={props.label ? labelId : undefined}\n aria-label={!props.label ? 'Multisuggest' : undefined}\n selectedKey={null}\n onSelectionChange={handleSelectionChange}\n inputValue={query}\n onInputChange={setQuery}\n items={items}\n isDisabled={props.isDisabled || props.isReadonly}\n isInvalid={isInvalid}\n allowsEmptyCollection\n allowsCustomValue\n menuTrigger=\"focus\"\n onBlur={() => {\n if (queryRef.current && showCustomOption) {\n addValue(queryRef.current, true)\n }\n commitValidation()\n }}\n onKeyDown={handleKeyDown}\n >\n {/* Input container with chips */}\n <FormFieldComponents.FieldGroup\n ref={composeRefs(fieldGroupRef, setFieldGroupEl)}\n isDisabled={props.isDisabled}\n isReadonly={props.isReadonly}\n isInvalid={isInvalid}\n hasWarning={hasWarning}\n shape={props.shape}\n className=\"flex\"\n >\n <div className=\"flex flex-wrap items-center gap-1.5 px-3 py-1.5 w-full\">\n {/* Display selected chips */}\n {selectedOptions.map((option) => (\n <span\n key={option.value}\n className={tw(\n 'inline-flex items-center rounded-full font-normal transition-colors max-w-full px-2 py-0.5 text-xs',\n 'bg-primary-500/10 text-primary-500',\n props.isDisabled && 'opacity-50'\n )}\n >\n <span className=\"truncate\">{option.label}</span>\n {isInteractive && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n handleRemoveOption(option.value)\n }}\n className=\"inline-flex items-center justify-center rounded-full transition-colors translate-x-1 h-4 w-4 -m-0.5 p-0.5 hover:bg-black/10\"\n aria-label={`Remove ${option.label}`}\n tabIndex={-1}\n >\n <Icon name=\"x-mark\" className=\"h-2 w-2\" />\n </button>\n )}\n </span>\n ))}\n\n {/* Search input */}\n <Input\n ref={inputRef}\n autoComplete={props.inputAutocomplete ?? 'off'}\n className=\"flex-1 min-w-[120px] px-0 bg-transparent border-0 focus:ring-0 focus:outline-none text-sm text-neutral-900 placeholder:text-neutral-400 py-0.5 disabled:cursor-not-allowed\"\n placeholder={selectedOptions.length === 0 ? (props.placeholder ?? 'Search or type to add...') : ''}\n aria-describedby={helpTextId}\n id={props.id}\n />\n </div>\n <FormFieldComponents.ChevronButton className=\"py-0\" />\n </FormFieldComponents.FieldGroup>\n\n {/* Options dropdown */}\n <AnimatedPopover\n triggerRef={fieldGroupRef}\n offset={4}\n style={{ width: fieldGroupSize.width, minWidth: fieldGroupSize.width }}\n >\n <div\n className={tw(\n 'overflow-hidden rounded bg-white border border-neutral-200 shadow-lg text-sm w-full focus:outline-none'\n )}\n >\n <ListBox\n aria-label={props.label ? String(props.label) : 'Multisuggest'}\n className=\"max-h-60 overflow-y-auto focus:outline-none\"\n renderEmptyState={() => (\n <div className=\"px-3 py-2 text-neutral-500 text-sm\">\n {props.isLoading ? 'Loading...' : query ? 'No matching suggestions' : 'No options available'}\n </div>\n )}\n >\n {(item) => {\n const option = item as T & { index: number; id: string; isCustom: boolean }\n\n if (props.renderOption) {\n return (\n <ListBoxItem id={option.id} textValue={option.label} className=\"outline-none\">\n {props.renderOption({ option, isCustom: option.isCustom, index: option.index })}\n </ListBoxItem>\n )\n }\n\n return (\n <FormFieldComponents.DropdownItem\n id={option.id}\n textValue={option.label}\n label={option.label}\n secondary={option.secondary}\n showCheckIcon={false}\n customValuePrefix={option.isCustom ? 'Add' : undefined}\n />\n )\n }}\n </ListBox>\n </div>\n </AnimatedPopover>\n </AriaComboBox>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────────\n\nfunction defaultFilterFunction<T extends MultisuggestOption>(options: readonly T[], inputValue: string): T[] {\n if (!inputValue) return [...options]\n return matchSorter(options, inputValue, {\n keys: ['label', 'secondary']\n })\n}\n"}]},{"name":"checkbox","dependencies":["tw","form-field","icon"],"files":[{"name":"components/checkbox.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add checkbox --directory app\n\nimport * as React from 'react'\nimport { Checkbox as AriaCheckbox } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { Icon } from './icon'\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\nexport type PlainCheckboxProps = {\n name?: string\n form?: string\n\n isDisabled?: boolean\n isRequired?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<boolean>\n\n checked?: boolean\n defaultChecked?: boolean\n onChange?: (checked: boolean) => void\n\n 'aria-label'?: string\n}\n\nexport type LabeledCheckboxProps = {\n name?: string\n form?: string\n\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n\n isRequired?: boolean\n isDisabled?: boolean\n isIndeterminate?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<boolean>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n checked?: boolean\n defaultChecked?: boolean\n onChange?: (checked: boolean) => void\n\n value?: string\n}\n\n// ── Plain Checkbox ─────────────────────────────────────────────────────────────\n\nexport function PlainCheckbox(props: PlainCheckboxProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n\n const [checked, setChecked] = HeadlessForm.useControlledState(props.checked, props.defaultChecked ?? false, props.onChange)\n\n const { isInvalid: validationInvalid, commitValidation } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: checked,\n name: props.name,\n isRequired: false,\n form: props.form,\n focus() {\n hiddenInputRef.current?.closest('label')?.focus()\n }\n },\n hiddenInputRef\n )\n\n return (\n <AriaCheckbox\n isSelected={checked}\n onChange={(v) => {\n setChecked(v)\n commitValidation()\n }}\n isDisabled={props.isDisabled}\n isInvalid={props.isInvalid || validationInvalid}\n aria-label={props['aria-label']}\n className=\"group inline-flex items-center focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600\"\n >\n {({ isSelected, isIndeterminate, isDisabled }) => (\n <>\n {props.isDisabled || !props.name ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n form={props.form}\n value={checked ? 'true' : 'false'}\n />\n )}\n <CheckboxBox isSelected={isSelected} isIndeterminate={isIndeterminate} isDisabled={isDisabled} />\n </>\n )}\n </AriaCheckbox>\n )\n}\n\nPlainCheckbox.displayName = 'PlainCheckbox'\n\n// ── Labeled Checkbox ───────────────────────────────────────────────────────────\n\nexport function LabeledCheckbox(props: LabeledCheckboxProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n\n const [checked, setChecked] = HeadlessForm.useControlledState(props.checked, props.defaultChecked ?? false, props.onChange)\n\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value: checked,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n hiddenInputRef.current?.closest('label')?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n\n return (\n <AriaCheckbox\n isSelected={checked}\n isIndeterminate={props.isIndeterminate}\n onChange={(v) => {\n setChecked(v)\n commitValidation()\n }}\n isDisabled={props.isDisabled}\n isInvalid={isInvalid}\n aria-describedby={helpTextId}\n className=\"group relative flex items-start gap-3 select-none focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600\"\n >\n {({ isSelected, isIndeterminate, isDisabled }) => (\n <>\n {props.isDisabled || !props.name ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n form={props.form}\n value={checked ? 'true' : 'false'}\n />\n )}\n <div className=\"shrink-0\">\n <CheckboxBox isSelected={isSelected} isIndeterminate={isIndeterminate} isDisabled={isDisabled} />\n </div>\n <div className=\"flex flex-col justify-center\">\n <div className=\"flex items-center gap-2 justify-between\">\n <FormFieldComponents.Label as=\"span\">{props.label}</FormFieldComponents.Label>\n {props.cornerHint && <div className=\"text-sm text-neutral-500\">{props.cornerHint}</div>}\n </div>\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n UNSAFE_className=\"mt-0.5\"\n />\n </div>\n </>\n )}\n </AriaCheckbox>\n )\n}\n\nLabeledCheckbox.displayName = 'LabeledCheckbox'\n\n// ── Helper components ──────────────────────────────────────────────────────────\n\nfunction CheckboxBox({\n isSelected,\n isIndeterminate,\n isDisabled\n}: {\n isSelected: boolean\n isIndeterminate: boolean\n isDisabled: boolean\n}) {\n return (\n <div\n className={tw(\n 'relative size-5 rounded border transition-colors duration-200',\n isSelected || isIndeterminate ? 'bg-primary-600 border-primary-600' : 'bg-white border-neutral-300',\n !isDisabled && !(isSelected || isIndeterminate) && 'group-hovered:border-neutral-400',\n isDisabled && 'cursor-not-allowed border-neutral-300 bg-neutral-100'\n )}\n >\n {isIndeterminate ? (\n <Icon name=\"minus\" className=\"h-5/6 absolute inset-0 m-auto aspect-square stroke-white text-white\" />\n ) : (\n <Icon name=\"check\" className=\"h-5/6 absolute inset-0 m-auto aspect-square stroke-white text-white\" />\n )}\n </div>\n )\n}\n"}]},{"name":"checkbox-group","dependencies":["tw","form-field","icon","checkbox"],"files":[{"name":"components/checkbox-group.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add checkbox-group --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { LabeledCheckbox } from './checkbox'\n\nexport type CheckboxGroupOption = {\n value: string\n label: React.ReactNode\n description?: React.ReactNode\n isDisabled?: boolean\n}\n\nexport type CheckboxGroupProps = {\n name?: string\n form?: string\n\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string[]>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n value?: string[]\n defaultValue?: string[]\n onChange?: (value: string[]) => void\n\n options: CheckboxGroupOption[]\n orientation?: 'horizontal' | 'vertical'\n showSelectAll?: boolean\n minSelection?: number\n maxSelection?: number\n}\n\nexport function CheckboxGroup(props: CheckboxGroupProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? [], props.onChange)\n\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus is handled by individual checkboxes\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const orientation = props.orientation ?? 'vertical'\n\n const enabledOptions = props.options.filter((o) => !o.isDisabled)\n const allSelected = value.length === enabledOptions.length\n const someSelected = value.length > 0 && !allSelected\n\n const handleOptionToggle = (optionValue: string) => {\n const newValue = value.includes(optionValue) ? value.filter((v) => v !== optionValue) : [...value, optionValue]\n\n if (props.minSelection && newValue.length < props.minSelection) {\n return\n }\n if (props.maxSelection && newValue.length > props.maxSelection) {\n return\n }\n\n setValue(newValue)\n commitValidation()\n }\n\n const handleSelectAll = () => {\n if (allSelected) {\n setValue([])\n } else {\n setValue(enabledOptions.map((o) => o.value))\n }\n commitValidation()\n }\n\n return (\n <div\n className=\"relative\"\n role=\"group\"\n aria-labelledby={props.label ? `${helpTextId}-label` : undefined}\n aria-describedby={helpTextId}\n >\n {value.map((v, i) => (\n <HeadlessForm.HiddenInput\n key={i}\n ref={i === 0 ? hiddenInputRef : undefined}\n name={props.name}\n value={v}\n form={props.form}\n />\n ))}\n {value.length === 0 && <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value=\"\" form={props.form} />}\n\n {props.label && (\n <div className=\"flex justify-between items-center mb-2\">\n <FormFieldComponents.Label as=\"span\" id={`${helpTextId}-label`}>\n {props.label}\n </FormFieldComponents.Label>\n {props.cornerHint && <div className=\"text-sm text-neutral-500\">{props.cornerHint}</div>}\n </div>\n )}\n\n {props.showSelectAll && (\n <div className=\"mb-2 pb-2 border-b border-neutral-200\">\n <LabeledCheckbox\n checked={allSelected}\n isIndeterminate={someSelected}\n onChange={handleSelectAll}\n isDisabled={props.isDisabled}\n label={<span className=\"text-sm font-medium\">Select All</span>}\n />\n </div>\n )}\n\n <div className={tw('flex', orientation === 'vertical' ? 'flex-col space-y-1.5' : 'flex-row flex-wrap gap-4')}>\n {props.options.map((option) => (\n <LabeledCheckbox\n key={option.value}\n label={option.label}\n helpText={option.description ?? null}\n checked={value.includes(option.value)}\n onChange={() => handleOptionToggle(option.value)}\n isDisabled={option.isDisabled || props.isDisabled}\n />\n ))}\n </div>\n\n {(props.minSelection || props.maxSelection) && (\n <div className=\"mt-2 text-xs text-neutral-500\">\n {props.minSelection && props.maxSelection\n ? `Select between ${props.minSelection} and ${props.maxSelection} options`\n : props.minSelection\n ? `Select at least ${props.minSelection} option${props.minSelection > 1 ? 's' : ''}`\n : `Select up to ${props.maxSelection} option${props.maxSelection! > 1 ? 's' : ''}`}\n {' · '}\n {value.length} selected\n </div>\n )}\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nCheckboxGroup.displayName = 'CheckboxGroup'\n"}]},{"name":"radio","dependencies":["tw"],"files":[{"name":"components/radio.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add radio --directory app\n\nimport * as React from 'react'\nimport { Radio as AriaRadio } from 'react-aria-components'\nimport { tw } from '../utils/tw'\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\nexport type PlainRadioProps = {\n value: string\n isDisabled?: boolean\n 'aria-label'?: string\n}\n\nexport type LabeledRadioProps = {\n value: string\n label?: React.ReactNode\n description?: React.ReactNode\n isDisabled?: boolean\n}\n\n// ── Plain Radio ──────────────────────────────────────────────────────────────\n\nexport function PlainRadio(props: PlainRadioProps) {\n return (\n <AriaRadio\n value={props.value}\n isDisabled={props.isDisabled}\n aria-label={props['aria-label']}\n className=\"group inline-flex items-center cursor-pointer data-[disabled]:cursor-not-allowed focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600\"\n >\n {({ isSelected, isDisabled }) => <RadioCircle isSelected={isSelected} isDisabled={isDisabled} />}\n </AriaRadio>\n )\n}\n\nPlainRadio.displayName = 'PlainRadio'\n\n// ── Labeled Radio ────────────────────────────────────────────────────────────\n\nexport function LabeledRadio(props: LabeledRadioProps) {\n return (\n <AriaRadio\n value={props.value}\n isDisabled={props.isDisabled}\n className=\"group flex items-start gap-3 cursor-pointer data-[disabled]:cursor-not-allowed select-none focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600\"\n >\n {({ isSelected, isDisabled }) => (\n <>\n <div className=\"shrink-0\">\n <RadioCircle isSelected={isSelected} isDisabled={isDisabled} />\n </div>\n {props.label && (\n <div className=\"flex flex-col\">\n <span className={tw('text-sm font-medium', isDisabled ? 'text-neutral-400' : 'text-neutral-700')}>\n {props.label}\n </span>\n {props.description && (\n <span className={tw('text-xs mt-0.5', isDisabled ? 'text-neutral-400' : 'text-neutral-500')}>\n {props.description}\n </span>\n )}\n </div>\n )}\n </>\n )}\n </AriaRadio>\n )\n}\n\nLabeledRadio.displayName = 'LabeledRadio'\n\n// ── Helper components ─────────────────────────────────────────────────────────\n\nfunction RadioCircle({ isSelected, isDisabled }: { isSelected: boolean; isDisabled: boolean }) {\n return (\n <div\n className={tw(\n 'size-5 rounded-full border-2 transition-colors duration-200 flex items-center justify-center',\n isSelected ? 'border-primary-600 bg-primary-600' : 'border-neutral-300 bg-white',\n !isDisabled && !isSelected && 'group-hovered:border-neutral-400',\n isDisabled && 'cursor-not-allowed border-neutral-300 bg-neutral-100'\n )}\n >\n <div\n className={tw(\n 'size-2 rounded-full bg-white transition-transform duration-200',\n isSelected ? 'scale-100' : 'scale-0'\n )}\n />\n </div>\n )\n}\n"}]},{"name":"radio-group","dependencies":["tw","form-field"],"files":[{"name":"components/radio-group.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add radio-group --directory app\n\nimport * as React from 'react'\nimport { RadioGroup as AriaRadioGroup } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\n\nexport type RadioGroupProps = {\n name?: string\n form?: string\n\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n value?: string | null\n defaultValue?: string | null\n onChange?: (value: string | null) => void\n\n children?: React.ReactNode\n orientation?: 'horizontal' | 'vertical' | 'grid'\n className?: string\n}\n\nexport function RadioGroup(props: RadioGroupProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const labelId = React.useId()\n\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? null, props.onChange)\n\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n // Focus is handled by AriaRadioGroup\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const orientation = props.orientation ?? 'vertical'\n\n return (\n <div className=\"relative\">\n <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value ?? ''} form={props.form} />\n\n {props.label && (\n <div className=\"flex justify-between items-center mb-2\">\n <FormFieldComponents.Label as=\"span\" id={labelId}>\n {props.label}\n </FormFieldComponents.Label>\n {props.cornerHint && <div className=\"text-sm text-neutral-500\">{props.cornerHint}</div>}\n </div>\n )}\n\n <AriaRadioGroup\n aria-labelledby={props.label ? labelId : undefined}\n aria-describedby={helpTextId}\n value={value}\n onChange={(newValue) => {\n setValue(newValue)\n commitValidation()\n }}\n isDisabled={props.isDisabled}\n orientation={orientation === 'grid' ? undefined : orientation}\n >\n <div\n className={tw(\n 'flex',\n orientation === 'vertical'\n ? 'flex-col space-y-2'\n : orientation === 'horizontal'\n ? 'flex-row flex-wrap gap-4'\n : 'grid gap-4',\n props.className\n )}\n >\n {props.children}\n </div>\n </AriaRadioGroup>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nRadioGroup.displayName = 'RadioGroup'\n"}]},{"name":"switch","dependencies":["tw","form-field"],"files":[{"name":"components/switch.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add switch --directory app\n\nimport * as React from 'react'\nimport { Switch as AriaSwitch } from 'react-aria-components'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { FormFieldComponents } from './helpers/form-field'\n\n// ── Props ──────────────────────────────────────────────────────────────────────\n\nexport type PlainSwitchProps = {\n name?: string\n form?: string\n\n isDisabled?: boolean\n\n value?: boolean\n defaultValue?: boolean\n onChange?: (value: boolean) => void\n\n 'aria-label'?: string\n}\n\nexport type LabeledSwitchProps = {\n name?: string\n form?: string\n\n label?: React.ReactNode\n description?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<boolean>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n value?: boolean\n defaultValue?: boolean\n onChange?: (value: boolean) => void\n\n labelPosition?: 'left' | 'right'\n}\n\n// ── Plain Switch ───────────────────────────────────────────────────────────────\n\nexport function PlainSwitch(props: PlainSwitchProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? false, props.onChange)\n\n return (\n <AriaSwitch\n isSelected={value}\n onChange={setValue}\n isDisabled={props.isDisabled}\n aria-label={props['aria-label']}\n className=\"group inline-flex items-center cursor-pointer data-[disabled]:cursor-not-allowed focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600\"\n >\n {({ isSelected, isDisabled }) => (\n <>\n {props.isDisabled || !props.name ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n form={props.form}\n value={value ? 'true' : 'false'}\n />\n )}\n <SwitchTrack isSelected={isSelected} isDisabled={isDisabled} />\n </>\n )}\n </AriaSwitch>\n )\n}\n\nPlainSwitch.displayName = 'PlainSwitch'\n\n// ── Labeled Switch ─────────────────────────────────────────────────────────────\n\nexport function LabeledSwitch(props: LabeledSwitchProps) {\n const hiddenInputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? false, props.onChange)\n\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n hiddenInputRef.current?.closest('label')?.focus()\n }\n },\n hiddenInputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const labelPosition = props.labelPosition ?? 'right'\n\n const labelElement = props.label && (\n <div className=\"flex flex-col text-left\">\n <FormFieldComponents.Label as=\"span\">{props.label}</FormFieldComponents.Label>\n {props.description && (\n <span className={tw('text-xs mt-0.5', props.isDisabled ? 'text-neutral-400' : 'text-neutral-500')}>\n {props.description}\n </span>\n )}\n </div>\n )\n\n return (\n <div className=\"relative\">\n <AriaSwitch\n isSelected={value}\n onChange={(checked) => {\n setValue(checked)\n commitValidation()\n }}\n isDisabled={props.isDisabled}\n aria-describedby={helpTextId}\n className=\"group flex items-center gap-3 cursor-pointer data-[disabled]:cursor-not-allowed select-none focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600\"\n >\n {({ isSelected, isDisabled }) => (\n <>\n {props.isDisabled || !props.name ? null : (\n <HeadlessForm.HiddenInput\n ref={hiddenInputRef}\n name={props.name}\n form={props.form}\n value={value ? 'true' : 'false'}\n />\n )}\n {labelPosition === 'left' && labelElement}\n <div className=\"shrink-0 flex items-center\">\n <SwitchTrack isSelected={isSelected} isDisabled={isDisabled} />\n </div>\n {labelPosition === 'right' && labelElement}\n {props.cornerHint && props.label && <div className=\"ml-auto text-sm text-neutral-500\">{props.cornerHint}</div>}\n </>\n )}\n </AriaSwitch>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={validationMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n )\n}\n\nLabeledSwitch.displayName = 'LabeledSwitch'\n\n// ── Helpers ────────────────────────────────────────────────────────────────────\n\nfunction SwitchTrack({ isSelected, isDisabled }: { isSelected: boolean; isDisabled: boolean }) {\n return (\n <div\n className={tw(\n 'relative inline-flex h-6 w-11 shrink-0 rounded-full border-2 border-transparent transition-colors duration-200',\n isSelected ? 'bg-primary-600' : 'bg-neutral-200',\n isDisabled && 'cursor-not-allowed bg-neutral-300'\n )}\n >\n <span\n className={tw(\n 'pointer-events-none inline-block size-5 rounded-full bg-white shadow-sm ring-1 ring-black/5 transition-transform duration-200',\n isSelected ? 'translate-x-5' : 'translate-x-0'\n )}\n />\n </div>\n )\n}\n"}]},{"name":"file-input","dependencies":["tw","form-field","compose-refs","file-input-utils","icon","headless-file-input"],"files":[{"name":"components/file-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add file-input --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { formatFileSize, validateFileMaxSize, validateFileType } from '../utils/file-input'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessFileInput } from './headless-file-input'\nimport { Icon } from './icon'\n\nexport type FileInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n renderPreview?: (props: FileInputPreviewProps) => React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string | File | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (File object or string URL, held in memory until form submit)\n value?: string | File | null\n defaultValue?: string | File | null\n onChange?: (value: string | File | null, hasError: boolean) => void\n\n // File specific\n accept?: string\n maxSize?: number // in bytes\n}\n\nexport interface FileInputPreviewProps {\n file: File | null\n fileUrl: string | null\n isDisabled: boolean\n onChange(value: string | null | File): unknown\n inputId: string\n name: string | undefined\n accept: string | undefined\n inputRef: React.Ref<HTMLInputElement>\n formatFileSize: (bytes: number) => string\n setDragging: (dragging: boolean) => void\n maxSize?: number\n}\n\nexport function FileInput(props: FileInputProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n const [, setDragging] = React.useState(false)\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? null, (value) => {\n if (props.onChange) {\n props.onChange(value, Boolean(validateFileMaxSize(value, props.maxSize) || validateFileType(value, props.accept)))\n }\n })\n\n const exceededMaxSize = React.useMemo(() => validateFileMaxSize(value, props.maxSize), [value, props.maxSize])\n const formatNotSupported = React.useMemo(() => validateFileType(value, props.accept), [value, props.accept])\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid || Boolean(exceededMaxSize || formatNotSupported)\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const errorMessage = exceededMaxSize || formatNotSupported || validationMessage\n\n const FilePreview = props.renderPreview ?? DefaultFileInputFilePreview\n\n return (\n <HeadlessFileInput\n value={value}\n onChange={(v) => {\n setValue(v)\n commitValidation()\n }}\n >\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <HeadlessFileInput.Preview>\n {(files) => {\n const file = files.length !== 0 ? files[0]!.file : null\n const fileUrl = files.length !== 0 ? files[0]!.fileUrl : null\n\n return (\n <div\n className={tw(\n 'relative',\n hasWarning &&\n '[&_.file-preview-border]:border-notice-300 [&_.file-preview-border]:hover:border-notice-400 [&_.file-preview-border]:text-notice-900 [&_.file-preview-border]:placeholder-notice-300 [&_.file-preview-border]:focus-within:border-notice-500 [&_.file-preview-border]:focus-within:ring-notice-500',\n isInvalid &&\n '[&_.file-preview-border]:border-negative-300 [&_.file-preview-border]:hover:border-negative-400 [&_.file-preview-border]:text-negative-900 [&_.file-preview-border]:placeholder-negative-300 [&_.file-preview-border]:focus-within:border-negative-500 [&_.file-preview-border]:focus-within:ring-negative-500'\n )}\n >\n {FilePreview({\n file,\n fileUrl,\n isDisabled: !!props.isDisabled,\n onChange: (v) => {\n setValue(v)\n commitValidation()\n },\n inputId,\n inputRef,\n name: props.name,\n accept: props.accept,\n formatFileSize,\n setDragging,\n maxSize: props.maxSize\n })}\n </div>\n )\n }}\n </HeadlessFileInput.Preview>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={errorMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </HeadlessFileInput>\n )\n}\n\nfunction DragOverContent() {\n return (\n <div\n className=\"absolute inset-0 p-12 rounded flex items-center justify-center\"\n style={{\n background: `repeating-linear-gradient(45deg, #e5e7eb, #e5e7eb 20px, #d1d5db 20px, #d1d5db 50px)`\n }}\n >\n <div>\n <Icon name=\"cloud-arrow-up\" className=\"h-12 w-12 mx-auto text-gray-400\" />\n <span className=\"mt-2 block text-sm font-semibold text-gray-900\">Upload a File</span>\n </div>\n </div>\n )\n}\n\nexport function DefaultFileInputFilePreview({\n file,\n isDisabled,\n onChange,\n inputId,\n name,\n accept,\n formatFileSize,\n setDragging,\n maxSize\n}: FileInputPreviewProps) {\n return (\n <>\n <HeadlessFileInput.DropZone\n accept={accept}\n name={name}\n inputId={inputId}\n onDragChange={setDragging}\n disabled={isDisabled}\n >\n {({ dragOver }) => (\n <div\n className={tw(\n 'file-preview-border relative block w-full rounded-md text-center focus:outline-none focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',\n 'border-2 border-neutral-200 hover:border-neutral-300',\n file ? '' : 'border-dashed',\n isDisabled && 'border-neutral-100 cursor-not-allowed'\n )}\n >\n {file ? (\n <div className=\"relative text-left\">\n <div className=\"flex items-center justify-between p-3 rounded\">\n <div className=\"flex items-center gap-3\">\n <Icon name=\"document\" className=\"h-8 w-8 text-neutral-400\" />\n <div>\n <p className=\"text-sm font-medium text-neutral-900\">{file.name || 'File'}</p>\n <p className=\"text-xs text-neutral-500\">{formatFileSize(file.size)}</p>\n </div>\n </div>\n </div>\n {dragOver && <DragOverContent />}\n </div>\n ) : dragOver ? (\n <DragOverContent />\n ) : (\n <div className=\"flex items-center justify-between p-3\">\n <div className=\"flex items-center gap-3\">\n <Icon name=\"cloud-arrow-up\" className=\"h-8 w-8 text-neutral-400\" />\n <div className=\"text-left\">\n <p className=\"text-sm text-neutral-600 font-medium\">Click to upload or drag and drop</p>\n <div className=\"text-xs text-neutral-500 mt-0.5\">\n {accept && <span>{accept}</span>}\n {accept && maxSize && <span> • </span>}\n {maxSize && <span>Max: {formatFileSize(maxSize)}</span>}\n </div>\n </div>\n </div>\n </div>\n )}\n </div>\n )}\n </HeadlessFileInput.DropZone>\n\n {file && !isDisabled && (\n <button\n type=\"button\"\n aria-label=\"Remove file\"\n onClick={() => onChange(null)}\n className=\"absolute top-1/2 -translate-y-1/2 right-3 p-1 hover:bg-neutral-50 rounded transition-colors\"\n >\n <Icon name=\"x-mark\" className=\"h-5 w-5 text-neutral-500\" />\n </button>\n )}\n </>\n )\n}\n"}]},{"name":"multi-file-input","dependencies":["tw","form-field","compose-refs","file-input-utils","icon","headless-file-input"],"files":[{"name":"components/multi-file-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add multi-file-input --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { formatFileSize, checkFileType, checkFileMaxSize } from '../utils/file-input'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessFileInput } from './headless-file-input'\nimport { Icon } from './icon'\n\nexport interface UrlFileReference {\n name: string\n url: string\n}\n\nexport type FileLike = File | string | UrlFileReference\n\nexport type MultiFileInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isReadonly?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<FileLike[]>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (File objects array or strings, held in memory until form submit)\n value?: FileLike[]\n defaultValue?: FileLike[]\n onChange?: (value: FileLike[], hasError: boolean) => void\n\n // File specific\n accept?: string | null\n maxSize?: number // in bytes per file\n maxFiles?: number\n}\n\nexport function MultiFileInput(props: MultiFileInputProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n const exceededMaxSizeFn = React.useCallback(\n (value: FileLike[]) => {\n for (let i = 0; i < value.length; i++) {\n const v = value[i]\n if (v instanceof File && props.maxSize) {\n const error = checkFileMaxSize(v, props.maxSize)\n if (error) return `File ${i + 1} exceeded max size of ${formatFileSize(props.maxSize)}`\n }\n }\n return null\n },\n [props.maxSize]\n )\n\n const formatNotSupportedFn = React.useCallback(\n (value: FileLike[]) => {\n if (!props.accept) return null\n for (const v of value) {\n if (v instanceof File) {\n const error = checkFileType(v, props.accept)\n if (error) return error\n }\n }\n return null\n },\n [props.accept]\n )\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? [], (value) => {\n if (props.onChange) {\n props.onChange(value, Boolean(exceededMaxSizeFn(value) || formatNotSupportedFn(value)))\n }\n })\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate(files) {\n const exceededMaxSize = exceededMaxSizeFn(files)\n const formatNotSupported = formatNotSupportedFn(files)\n const exceededMaxFiles = props.maxFiles !== undefined && files.length > props.maxFiles ? 'Too many files' : null\n const custom = [props.validate ? props.validate(files) : []].flat()\n\n const errors = [exceededMaxSize, formatNotSupported, exceededMaxFiles, ...custom].filter(Boolean)\n return errors as string[]\n },\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const errorMessage = validationMessage\n\n const handleRemove = (index: number) => {\n const newFiles = value.filter((_, i) => i !== index)\n setValue(newFiles)\n }\n\n return (\n <HeadlessFileInput\n value={value.map((v) => (v instanceof File ? v : typeof v === 'string' ? v : v.url))}\n onChange={(v) => {\n setValue(\n v.map((a) =>\n a instanceof File\n ? a\n : (value.find((b) => (b instanceof File || typeof b === 'string' ? false : b.url === a)) ?? {\n name: a,\n url: a\n })\n )\n )\n commitValidation()\n }}\n multiple\n >\n <HeadlessForm.HiddenInput value=\"\" form={props.form} name={undefined} ref={inputRef} />\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <HeadlessFileInput.Preview>\n {(files) => (\n <>\n {files.length > 0 && (\n <div className=\"space-y-2 mb-3\">\n {files.map((fileItem, index) => {\n const fileName = fileItem.file\n ? fileItem.file.name\n : (value.filter((v) => !(v instanceof File) && typeof v !== 'string') as UrlFileReference[]).find(\n (v) => v.url === fileItem.fileUrl\n )?.name\n return (\n <div\n key={index}\n className=\"flex items-center text-left justify-between px-6 py-2 rounded border border-neutral-200 bg-white\"\n >\n <div className=\"flex items-center gap-2\">\n <Icon name=\"document\" className=\"h-5 w-5 text-neutral-400\" />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-neutral-900 truncate\">{fileName || 'File'}</p>\n {fileItem.file && <p className=\"text-xs text-neutral-500\">{formatFileSize(fileItem.file.size)}</p>}\n </div>\n </div>\n {!props.isDisabled && !props.isReadonly && (\n <button\n type=\"button\"\n onClick={() => handleRemove(index)}\n className=\"p-1 hover:bg-neutral-100 rounded transition-colors\"\n >\n <Icon name=\"x-mark\" className=\"h-4 w-4 text-neutral-500\" />\n </button>\n )}\n </div>\n )\n })}\n </div>\n )}\n </>\n )}\n </HeadlessFileInput.Preview>\n\n <HeadlessFileInput.DropZone\n accept={props.accept ?? undefined}\n name={props.name}\n inputId={inputId}\n disabled={props.isDisabled || props.isReadonly}\n >\n {({ dragOver }) => (\n <div\n className={tw(\n 'relative block w-full rounded-md text-center focus:outline-none focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',\n 'border-2 border-dashed border-neutral-200 hover:border-neutral-300',\n 'px-6 py-2 min-h-[60px] flex items-center justify-center',\n (props.isDisabled || props.isReadonly) && 'border-neutral-100 cursor-not-allowed',\n hasWarning &&\n 'border-notice-300 hover:border-notice-400 text-notice-900 placeholder-notice-300 focus-within:border-notice-500 focus-within:ring-notice-500',\n isInvalid &&\n 'border-negative-300 hover:border-negative-400 text-negative-900 placeholder-negative-300 focus-within:border-negative-500 focus-within:ring-negative-500'\n )}\n >\n {dragOver ? (\n <DragOverContent />\n ) : (\n <div className=\"flex items-center w-full gap-3\">\n <Icon name=\"cloud-arrow-up\" className=\"h-5 w-5 text-neutral-400 flex-shrink-0\" />\n <div className=\"text-left\">\n <p className=\"text-sm font-medium text-neutral-600\">Click to upload or drag and drop</p>\n <p className=\"text-xs text-neutral-500\">\n {value.length > 0 ? 'Add more files' : 'Select files'}\n {props.accept && ` (${props.accept})`}\n {props.maxSize && ` • Max: ${formatFileSize(props.maxSize)}`}\n </p>\n </div>\n </div>\n )}\n </div>\n )}\n </HeadlessFileInput.DropZone>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={errorMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </HeadlessFileInput>\n )\n}\n\nfunction DragOverContent() {\n return (\n <div className=\"flex items-center gap-3\">\n <Icon name=\"cloud-arrow-up\" className=\"h-5 w-5 text-gray-400 flex-shrink-0\" />\n <span className=\"text-sm font-semibold text-gray-900\">Drop files here</span>\n </div>\n )\n}\n"}]},{"name":"image-input","dependencies":["tw","form-field","compose-refs","file-input-utils","icon","headless-file-input"],"files":[{"name":"components/image-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add image-input --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { formatFileSize, checkFileType, checkFileMaxSize } from '../utils/file-input'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessFileInput } from './headless-file-input'\nimport { Icon } from './icon'\n\nexport type ImageInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n emptyState?: React.ReactNode\n renderPreview?: (props: ImageInputPreviewProps) => React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string | File | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (File object or string URL, held in memory until form submit)\n value?: string | File | null\n defaultValue?: string | File | null\n onChange?: (value: string | File | null) => void\n\n // Image specific\n accept?: string\n maxSize?: number // in bytes\n}\n\nexport interface ImageInputPreviewProps {\n fileUrl: string\n isDisabled: boolean\n onChange(value: string | null | File): unknown\n inputId: string\n name: string | undefined\n accept: string | undefined\n inputRef: React.Ref<HTMLInputElement>\n}\n\nexport function ImageInput(props: ImageInputProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n const exceededMaxSizeFn = React.useCallback(\n (value: string | File | null) => {\n if (value && props.maxSize && value instanceof File) {\n return checkFileMaxSize(value, props.maxSize)\n }\n return null\n },\n [props.maxSize]\n )\n\n const formatNotSupportedFn = React.useCallback(\n (value: string | File | null) => {\n if (value instanceof File) {\n return checkFileType(value, props.accept ?? 'image/*')\n }\n return null\n },\n [props.accept]\n )\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? null, (value) => {\n if (props.onChange) {\n props.onChange(value)\n }\n })\n\n const exceededMaxSize = React.useMemo(() => exceededMaxSizeFn(value), [value, exceededMaxSizeFn])\n const formatNotSupported = React.useMemo(() => formatNotSupportedFn(value), [value, formatNotSupportedFn])\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid || Boolean(exceededMaxSize || formatNotSupported)\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const errorMessage = exceededMaxSize || formatNotSupported || validationMessage\n\n const ImagePreview = props.renderPreview ?? DefaultImageInputPreview\n\n const accept = props.accept ?? 'image/*'\n\n const emptyState = props.emptyState ?? (\n <div className=\"px-6 py-12\">\n <Icon name=\"photo\" className=\"h-12 w-12 mx-auto mb-3 text-neutral-400\" />\n <p className=\"text-sm text-neutral-600 font-medium\">Click to upload or drag and drop</p>\n <p className=\"text-xs text-neutral-500 mt-1\">{accept}</p>\n {props.maxSize ? <p className=\"text-xs text-neutral-500 mt-1\">Max size: {formatFileSize(props.maxSize)}</p> : null}\n </div>\n )\n\n return (\n <HeadlessFileInput\n value={value}\n onChange={(v) => {\n setValue(v)\n commitValidation()\n }}\n >\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <HeadlessFileInput.Preview>\n {(files) => {\n const hasFile = files.length > 0\n return (\n <div\n className={tw(\n 'image-input-border relative block w-full rounded-md overflow-hidden text-center focus:outline-none focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',\n 'border-2 border-neutral-200 hover:border-neutral-300 min-h-56',\n !hasFile && 'border-dashed',\n props.isDisabled && 'border-neutral-100 cursor-not-allowed',\n hasWarning &&\n 'border-notice-300 hover:border-notice-400 text-notice-900 placeholder-notice-300 focus-within:border-notice-500 focus-within:ring-notice-500',\n isInvalid &&\n 'border-negative-300 hover:border-negative-400 text-negative-900 placeholder-negative-300 focus-within:border-negative-500 focus-within:ring-negative-500'\n )}\n >\n {hasFile ? (\n ImagePreview({\n isDisabled: !!props.isDisabled,\n fileUrl: files[0]!.fileUrl,\n onChange: (v) => {\n setValue(v)\n commitValidation()\n },\n inputId,\n inputRef,\n name: props.name,\n accept\n })\n ) : (\n <HeadlessFileInput.DropZone\n inputRef={inputRef}\n accept={accept}\n name={props.name}\n inputId={inputId}\n disabled={props.isDisabled}\n className=\"size-full\"\n >\n {({ dragOver }) => (\n <div className=\"relative pointer-events-none\">\n {emptyState}\n {dragOver ? (\n <div className=\"absolute inset-0\">\n <DragOverContent />\n </div>\n ) : null}\n </div>\n )}\n </HeadlessFileInput.DropZone>\n )}\n </div>\n )\n }}\n </HeadlessFileInput.Preview>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={errorMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </HeadlessFileInput>\n )\n}\n\nfunction DragOverContent() {\n return (\n <div\n className=\"absolute inset-0 p-12 rounded flex items-center justify-center\"\n style={{\n background: `repeating-linear-gradient(45deg, #e5e7eb, #e5e7eb 20px, #d1d5db 20px, #d1d5db 50px)`\n }}\n >\n <div>\n <Icon name=\"cloud-arrow-up\" className=\"h-12 w-12 mx-auto text-gray-400\" />\n <span className=\"mt-2 block text-sm font-semibold text-gray-900\">Upload an Image</span>\n </div>\n </div>\n )\n}\n\nexport function DefaultImageInputPreview({\n name,\n accept,\n fileUrl,\n inputRef,\n isDisabled,\n onChange,\n inputId\n}: ImageInputPreviewProps) {\n return (\n <>\n <HeadlessFileInput.DropZone\n inputRef={inputRef}\n accept={accept ?? 'image/*'}\n name={name}\n inputId={inputId}\n disabled={isDisabled}\n >\n {({ dragOver }) => (\n <div className=\"min-h-56 pointer-events-none\">\n {dragOver ? (\n <div className=\"absolute inset-0\">\n <DragOverContent />\n </div>\n ) : null}\n <div className=\"relative\">\n <div\n aria-hidden=\"true\"\n className=\"absolute inset-x-0 top-0 rounded-t-md h-20 bg-gradient-to-b from-white opacity-75 mix-blend-lighten\"\n />\n <img src={fileUrl} alt=\"Preview\" className=\"w-full object-cover rounded-md\" />\n </div>\n </div>\n )}\n </HeadlessFileInput.DropZone>\n <div className=\"absolute top-2 right-2\">\n {!isDisabled ? (\n <button\n type=\"button\"\n aria-label=\"Remove\"\n onClick={() => onChange(null)}\n className=\"p-1 h-5 w-5 flex items-center justify-center bg-gray-800/60 hover:bg-gray-500/60 rounded-full transition-colors\"\n >\n <Icon name=\"x-mark\" className=\"h-4 w-4 text-gray-200\" />\n </button>\n ) : null}\n </div>\n </>\n )\n}\n"}]},{"name":"multi-image-input","dependencies":["tw","form-field","compose-refs","file-input-utils","icon","headless-file-input"],"files":[{"name":"components/multi-image-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add multi-image-input --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { formatFileSize, checkFileType, checkFileMaxSize } from '../utils/file-input'\nimport { useStableAccessor } from '../utils/use-stable-accessor'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessFileInput } from './headless-file-input'\nimport { Icon } from './icon'\n\nexport type MultiImageInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n placeholder?: string\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<(string | File)[]>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (File objects array or strings, held in memory until form submit)\n value?: (string | File)[]\n defaultValue?: (string | File)[]\n onChange?: (value: (string | File)[]) => void\n\n // Image specific\n accept?: string\n maxSize?: number // in bytes per image\n maxImages?: number\n aspectRatio?: number // width/height ratio\n columns?: number // Grid columns for display\n allowReordering?: boolean\n\n renderPreview?: (props: MultiImageInputPreviewProps) => React.ReactNode\n}\n\nexport interface MultiImageInputPreviewProps {\n fileUrl: string\n isDisabled: boolean\n index: number\n onChange(value: string | null | File): unknown\n}\n\nexport function MultiImageInput(props: MultiImageInputProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n const labelRef = React.useRef<HTMLLabelElement>(null)\n\n const exceededMaxSizeFn = React.useCallback(\n (value: (string | File)[]) => {\n for (let i = 0; i < value.length; i++) {\n const v = value[i]\n if (v instanceof File && props.maxSize) {\n const error = checkFileMaxSize(v, props.maxSize)\n if (error) return `Image ${i + 1} exceeded max size of ${formatFileSize(props.maxSize)}`\n }\n }\n return null\n },\n [props.maxSize]\n )\n\n const formatNotSupportedFn = React.useCallback(\n (value: (string | File)[]) => {\n const accept = props.accept ?? 'image/*'\n for (const v of value) {\n if (v instanceof File) {\n const error = checkFileType(v, accept)\n if (error) return error\n }\n }\n return null\n },\n [props.accept]\n )\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? [], (value) => {\n if (props.onChange) {\n props.onChange(value)\n }\n })\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate(files) {\n const exceededMaxSize = exceededMaxSizeFn(files)\n const formatNotSupported = formatNotSupportedFn(files)\n const exceededMaxImages = props.maxImages !== undefined && files.length > props.maxImages ? 'Too many images' : null\n const custom = [props.validate ? props.validate(files) : []].flat()\n\n const errors = [exceededMaxSize, formatNotSupported, exceededMaxImages, ...custom].filter(Boolean)\n return errors as string[]\n },\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n labelRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const errorMessage = validationMessage\n\n // Stable handlers via useStableAccessor to avoid re-creating on every render\n const getValue = useStableAccessor(value)\n const getSetValue = useStableAccessor(setValue)\n\n const handleReorder = React.useCallback(\n (fromIndex: number, toIndex: number) => {\n const newFiles = [...getValue()]\n const [movedFile] = newFiles.splice(fromIndex, 1)\n newFiles.splice(toIndex, 0, movedFile!)\n getSetValue()(newFiles)\n },\n [getValue, getSetValue]\n )\n\n const handleRemove = React.useCallback(\n (index: number) => {\n getSetValue()(getValue().filter((_: string | File, i: number) => i !== index))\n },\n [getValue, getSetValue]\n )\n\n const handleReplace = React.useCallback(\n (index: number, file: string | File) => {\n getSetValue()(getValue().map((f: string | File, i: number) => (i === index ? file : f)))\n },\n [getValue, getSetValue]\n )\n\n const getCommitValidation = useStableAccessor(commitValidation)\n const handleFileInputChange = React.useCallback(\n (v: (string | File)[]) => {\n getSetValue()(v)\n getCommitValidation()()\n },\n [getSetValue, getCommitValidation]\n )\n\n // Stable component reference\n const imagePreview = React.useMemo(() => props.renderPreview ?? DefaultImagePreview, [props.renderPreview])\n\n // Stable keys for grid items\n const fileKeyMapRef = React.useRef(new WeakMap<File, string>())\n const fileKeyCounterRef = React.useRef(0)\n const getFileKey = React.useCallback((item: string | File): string => {\n if (typeof item === 'string') return item\n let key = fileKeyMapRef.current.get(item)\n if (!key) {\n key = `file-${fileKeyCounterRef.current++}`\n fileKeyMapRef.current.set(item, key)\n }\n return key\n }, [])\n\n // Memoize grid style\n const gridStyle = React.useMemo(\n () => ({ gridTemplateColumns: `repeat(${props.columns ?? 4}, minmax(0, 1fr))` }),\n [props.columns]\n )\n\n return (\n <HeadlessFileInput value={value} onChange={handleFileInputChange} multiple>\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <HeadlessFileInput.Preview>\n {(files) => {\n const canAddMore = !props.maxImages || files.length < props.maxImages\n\n return (\n <>\n {/* Drop zone */}\n {canAddMore && (\n <HeadlessFileInput.DropZone\n accept={props.accept ?? 'image/*'}\n name={props.name}\n inputId={inputId}\n inputRef={inputRef}\n ref={labelRef}\n disabled={props.isDisabled}\n >\n {({ dragOver }) => (\n <div\n className={tw(\n 'relative flex flex-col items-center justify-center rounded-md border-2 border-dashed cursor-pointer transition-all duration-200 mb-2 p-8',\n 'focus:outline-none focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2 pointer-events-none',\n 'border-neutral-200 hover:border-neutral-300',\n props.isDisabled && 'border-neutral-100 cursor-not-allowed',\n hasWarning &&\n 'border-notice-300 hover:border-notice-400 text-notice-900 placeholder-notice-300 focus-within:border-notice-500 focus-within:ring-notice-500',\n isInvalid &&\n 'border-negative-300 hover:border-negative-400 text-negative-900 placeholder-negative-300 focus-within:border-negative-500 focus-within:ring-negative-500'\n )}\n >\n {dragOver ? (\n <div className=\"min-h-20\">\n <DragOverContent />\n </div>\n ) : (\n <div className=\"text-center\">\n <Icon name=\"cloud-arrow-up\" className=\"h-10 w-10 mx-auto mb-3 text-neutral-400\" />\n <p className=\"text-sm text-neutral-600 font-medium\">\n {props.placeholder || 'Drop images here or click to browse'}\n </p>\n {props.maxSize && (\n <p className=\"text-xs text-neutral-500 mt-1\">\n Max size: {formatFileSize(props.maxSize)} per image\n </p>\n )}\n {props.maxImages && (\n <p className=\"text-xs text-neutral-500 mt-1\">\n {files.length} / {props.maxImages} images uploaded\n </p>\n )}\n </div>\n )}\n </div>\n )}\n </HeadlessFileInput.DropZone>\n )}\n\n {/* Image previews grid */}\n {files.length > 0 && (\n <div className={tw('grid gap-3')} style={gridStyle}>\n {files.map((fileItem, index) => (\n <GridItem\n key={getFileKey(value[index]!)}\n fileUrl={fileItem.fileUrl}\n index={index}\n aspectRatio={props.aspectRatio ?? 1}\n isDisabled={props.isDisabled ?? false}\n allowReordering={props.allowReordering ?? false}\n onReorder={handleReorder}\n onRemove={handleRemove}\n onReplace={handleReplace}\n ImagePreview={imagePreview}\n />\n ))}\n </div>\n )}\n\n {/* Message when at max capacity */}\n {!canAddMore && (\n <div className=\"mt-3 text-sm text-neutral-500 text-center\">\n Maximum of {props.maxImages} images reached\n </div>\n )}\n </>\n )\n }}\n </HeadlessFileInput.Preview>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={errorMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </HeadlessFileInput>\n )\n}\n\n// ── Memoized sub-components ─────────────────────────────────────────────────\n\nconst GridItem = React.memo(function GridItem({\n fileUrl,\n index,\n aspectRatio,\n isDisabled,\n allowReordering,\n onReorder,\n onRemove,\n onReplace,\n ImagePreview\n}: {\n fileUrl: string\n index: number\n aspectRatio: number\n isDisabled: boolean\n allowReordering: boolean\n onReorder: (fromIndex: number, toIndex: number) => void\n onRemove: (index: number) => void\n onReplace: (index: number, file: string | File) => void\n ImagePreview: (props: MultiImageInputPreviewProps) => React.ReactNode\n}) {\n const handleDragStart = React.useCallback(\n (e: React.DragEvent) => e.dataTransfer.setData('index', index.toString()),\n [index]\n )\n\n const handleDrop = React.useCallback(\n (e: React.DragEvent) => {\n e.preventDefault()\n const fromIndex = parseInt(e.dataTransfer.getData('index'))\n if (fromIndex !== index) onReorder(fromIndex, index)\n },\n [index, onReorder]\n )\n\n const handleChange = React.useCallback(\n (v: string | null | File) => {\n if (v === null) onRemove(index)\n else onReplace(index, v)\n },\n [index, onRemove, onReplace]\n )\n\n return (\n <div\n className=\"relative group overflow-hidden rounded-md\"\n style={{ aspectRatio }}\n draggable={!isDisabled && allowReordering}\n onDragStart={handleDragStart}\n onDragOver={preventDefaultHandler}\n onDrop={handleDrop}\n >\n <ImagePreview isDisabled={isDisabled} fileUrl={fileUrl} index={index} onChange={handleChange} />\n </div>\n )\n})\n\nconst DefaultImagePreview = React.memo(function DefaultImagePreview({\n fileUrl,\n isDisabled,\n onChange\n}: MultiImageInputPreviewProps) {\n return (\n <>\n <img src={fileUrl} alt=\"Preview\" className=\"w-full h-full object-cover rounded-md\" />\n {!isDisabled && (\n <button\n type=\"button\"\n aria-label=\"Remove image\"\n onClick={() => onChange(null)}\n className=\"absolute top-1 right-1 p-1 bg-black/50 hover:bg-red-600 rounded-full transition-all duration-200 hover:scale-110\"\n >\n <Icon name=\"x-mark\" className=\"h-4 w-4 text-white\" />\n </button>\n )}\n </>\n )\n})\n\n// ── Static helpers ──────────────────────────────────────────────────────────\n\nconst DRAG_OVER_STYLE = {\n background: 'repeating-linear-gradient(45deg, #e5e7eb, #e5e7eb 20px, #d1d5db 20px, #d1d5db 50px)'\n} as const\n\nfunction DragOverContent() {\n return (\n <div className=\"absolute inset-0 p-6 rounded-md flex items-center justify-center text-center\" style={DRAG_OVER_STYLE}>\n <div>\n <Icon name=\"cloud-arrow-up\" className=\"h-12 w-12 mx-auto text-gray-400\" />\n <span className=\"mt-2 block text-sm font-semibold text-gray-900\">Drop Image</span>\n </div>\n </div>\n )\n}\n\nfunction preventDefaultHandler(e: React.DragEvent) {\n e.preventDefault()\n}\n"}]},{"name":"pdf-input","dependencies":["tw","form-field","compose-refs","file-input-utils","pdf-dist","icon","headless-file-input","pdf","use-pagination"],"files":[{"name":"components/pdf-input.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add pdf-input --directory app\n\nimport * as React from 'react'\nimport { HeadlessForm } from '@maestro-js/form'\nimport type { ValidationFunction, ValidationResult } from '@maestro-js/form'\nimport { tw } from '../utils/tw'\nimport { formatFileSize, checkFileType, checkFileMaxSize } from '../utils/file-input'\nimport { FormFieldComponents } from './helpers/form-field'\nimport { HeadlessFileInput } from './headless-file-input'\nimport { Icon } from './icon'\nimport { Pdf } from './pdf'\n\nexport type PdfInputProps = {\n // Form identification\n name?: string\n form?: string\n\n // Display elements\n label?: React.ReactNode\n cornerHint?: React.ReactNode\n helpText?: React.ReactNode\n warningMessage?: React.ReactNode\n emptyState?: React.ReactNode\n renderPreview?: (props: PdfInputPreviewProps) => React.ReactNode\n\n // Validation and state\n isRequired?: boolean\n isDisabled?: boolean\n isInvalid?: boolean\n validate?: ValidationFunction<string | File | null>\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n\n // Value management (File object or string URL, held in memory until form submit)\n value?: string | File | null\n defaultValue?: string | File | null\n onChange?: (value: string | File | null) => void\n\n maxSize?: number // in bytes\n}\n\nexport interface PdfInputPreviewProps {\n fileUrl: string\n isDisabled: boolean\n onChange(value: string | null | File): unknown\n inputId: string\n name: string | undefined\n inputRef: React.Ref<HTMLInputElement>\n}\n\nexport function PdfInput(props: PdfInputProps) {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const helpTextId = React.useId()\n const inputId = React.useId()\n\n const exceededMaxSizeFn = React.useCallback(\n (value: string | File | null) => {\n if (value && props.maxSize && value instanceof File) {\n return checkFileMaxSize(value, props.maxSize)\n }\n return null\n },\n [props.maxSize]\n )\n\n const formatNotSupportedFn = React.useCallback((value: string | File | null) => {\n if (value instanceof File) {\n return checkFileType(value, 'application/pdf')\n }\n return null\n }, [])\n\n // Controlled state management\n const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? null, (value) => {\n if (props.onChange) {\n props.onChange(value)\n }\n })\n\n const exceededMaxSize = React.useMemo(() => exceededMaxSizeFn(value), [value, exceededMaxSizeFn])\n const formatNotSupported = React.useMemo(() => formatNotSupportedFn(value), [value, formatNotSupportedFn])\n\n // Form validation\n const {\n validationMessage,\n isInvalid: validationInvalid,\n commitValidation\n } = HeadlessForm.useValidation(\n {\n validate: props.validate,\n value,\n name: props.name,\n isRequired: props.isRequired,\n form: props.form,\n focus() {\n inputRef.current?.focus()\n }\n },\n inputRef\n )\n\n const isInvalid = props.isInvalid || validationInvalid || Boolean(exceededMaxSize || formatNotSupported)\n const hasWarning = Boolean(props.warningMessage) && !isInvalid\n const errorMessage = exceededMaxSize || formatNotSupported || validationMessage\n\n const PdfPreview = props.renderPreview ?? DefaultPdfInputPreview\n\n const emptyState = props.emptyState ?? (\n <div className=\"px-6 py-12\">\n <Icon name=\"document\" className=\"h-12 w-12 mx-auto mb-3 text-neutral-400\" />\n <p className=\"text-sm text-neutral-600 font-medium\">Click to upload or drag and drop</p>\n <p className=\"text-xs text-neutral-500 mt-1\">PDF</p>\n {props.maxSize ? <p className=\"text-xs text-neutral-500 mt-1\">Max size: {formatFileSize(props.maxSize)}</p> : null}\n </div>\n )\n\n return (\n <HeadlessFileInput\n value={value}\n onChange={(v) => {\n setValue(v)\n commitValidation()\n }}\n >\n <div className=\"relative\">\n <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />\n\n <HeadlessFileInput.Preview>\n {(files) => {\n const hasFile = files.length > 0\n return (\n <div\n className={tw(\n 'pdf-input-border relative block w-full rounded-md overflow-hidden text-center focus:outline-none focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2',\n 'border-2 border-neutral-200 hover:border-neutral-300 min-h-56',\n !hasFile && 'border-dashed',\n props.isDisabled && 'border-neutral-100 cursor-not-allowed',\n hasWarning &&\n 'border-notice-300 hover:border-notice-400 text-notice-900 placeholder-notice-300 focus-within:border-notice-500 focus-within:ring-notice-500',\n isInvalid &&\n 'border-negative-300 hover:border-negative-400 text-negative-900 placeholder-negative-300 focus-within:border-negative-500 focus-within:ring-negative-500'\n )}\n >\n {hasFile ? (\n PdfPreview({\n isDisabled: !!props.isDisabled,\n fileUrl: files[0]!.fileUrl,\n onChange: (v) => {\n setValue(v)\n commitValidation()\n },\n inputId,\n inputRef,\n name: props.name\n })\n ) : (\n <HeadlessFileInput.DropZone\n inputRef={inputRef}\n accept=\"application/pdf\"\n name={props.name}\n inputId={inputId}\n disabled={props.isDisabled}\n className=\"size-full\"\n >\n {({ dragOver }) => (\n <div className=\"relative pointer-events-none\">\n {emptyState}\n {dragOver ? (\n <div className=\"absolute inset-0\">\n <DragOverContent />\n </div>\n ) : null}\n </div>\n )}\n </HeadlessFileInput.DropZone>\n )}\n </div>\n )\n }}\n </HeadlessFileInput.Preview>\n\n <FormFieldComponents.FormFieldHelpText\n id={helpTextId}\n isInvalid={isInvalid}\n validationMessage={errorMessage}\n hasWarning={hasWarning}\n warningMessage={props.warningMessage}\n helpText={props.helpText}\n />\n </div>\n </HeadlessFileInput>\n )\n}\n\nfunction DragOverContent() {\n return (\n <div\n className=\"absolute inset-0 p-12 rounded flex items-center justify-center\"\n style={{\n background: `repeating-linear-gradient(45deg, #e5e7eb, #e5e7eb 20px, #d1d5db 20px, #d1d5db 50px)`\n }}\n >\n <div>\n <Icon name=\"cloud-arrow-up\" className=\"h-12 w-12 mx-auto text-gray-400\" />\n <span className=\"mt-2 block text-sm font-semibold text-gray-900\">Upload a PDF</span>\n </div>\n </div>\n )\n}\n\nexport function DefaultPdfInputPreview({ name, fileUrl, inputRef, isDisabled, onChange, inputId }: PdfInputPreviewProps) {\n return (\n <>\n <HeadlessFileInput.DropZone\n inputRef={inputRef}\n accept=\"application/pdf\"\n name={name}\n inputId={inputId}\n disabled={isDisabled}\n >\n {({ dragOver }) => (\n <div className=\"min-h-56 pointer-events-none\">\n {dragOver ? (\n <div className=\"absolute inset-0\">\n <DragOverContent />\n </div>\n ) : null}\n <div className=\"relative\">\n <div\n aria-hidden=\"true\"\n className=\"absolute inset-x-0 top-0 rounded-t-md h-20 bg-gradient-to-b from-white opacity-75 mix-blend-lighten\"\n />\n <Pdf src={fileUrl} className=\"w-full min-h-96 rounded-md border-0\" />\n </div>\n </div>\n )}\n </HeadlessFileInput.DropZone>\n <div className=\"absolute top-2 right-2\">\n {!isDisabled ? (\n <button\n type=\"button\"\n aria-label=\"Remove\"\n onClick={() => onChange(null)}\n className=\"p-1 h-5 w-5 flex items-center justify-center bg-gray-800/60 hover:bg-gray-500/60 rounded-full transition-colors\"\n >\n <Icon name=\"x-mark\" className=\"h-4 w-4 text-gray-200\" />\n </button>\n ) : null}\n </div>\n </>\n )\n}\n"}]},{"name":"dialog","dependencies":["tw","headless-button","compose-refs","use-render-props","icon"],"files":[{"name":"components/dialog.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add dialog --directory app\n\nimport React from 'react'\nimport type { To, RelativeRoutingType } from 'react-router'\nimport { useNavigate } from 'react-router'\nimport { Modal, ModalOverlay, Dialog as AriaDialog, Heading } from 'react-aria-components'\nimport { tw } from '../utils/tw'\nimport { Button } from './button'\n\nexport type DialogProps = {\n children?: React.ReactNode\n title?: React.ReactNode\n isOpen?: boolean\n close?:\n | ((isOpen: boolean) => void)\n | { to: To; replace?: boolean; relative?: RelativeRoutingType; reloadDocument?: boolean }\n size?: 'xs' | 'sm' | 'lg'\n className?: string\n isDismissable?: boolean\n actions?: React.ReactNode\n}\n\ninterface DialogContextValue {\n close(): void\n}\n\nconst DialogContext = React.createContext<DialogContextValue>({ close: () => undefined })\n\nfunction Dialog_({\n children,\n title,\n isOpen = false,\n close,\n size = 'sm',\n className,\n isDismissable = false,\n actions\n}: DialogProps) {\n const navigate = useNavigate()\n\n const onOpenChange = close\n ? typeof close === 'function'\n ? close\n : (open: boolean) => {\n if (!open) navigate(close.to, { replace: close.replace, relative: close.relative })\n }\n : undefined\n\n return (\n <ModalOverlay\n isOpen={isOpen}\n onOpenChange={onOpenChange}\n isDismissable={isDismissable}\n isKeyboardDismissDisabled={!isDismissable}\n className={tw(\n 'fixed inset-0 z-[2000] flex items-center justify-center bg-black/25 backdrop-blur-sm',\n 'transition-opacity duration-300 ease-in-out',\n 'data-[entering]:opacity-0',\n 'data-[exiting]:opacity-0'\n )}\n >\n <Modal\n className={tw(\n 'w-[calc(100%-4rem)] m-auto',\n size === 'lg' ? 'max-w-[1080px]' : size === 'xs' ? 'max-w-[400px]' : 'max-w-[640px]',\n 'transition-all duration-300 ease-in-out',\n 'data-[entering]:scale-95 data-[entering]:opacity-0',\n 'data-[exiting]:scale-95 data-[exiting]:opacity-0'\n )}\n >\n <AriaDialog\n className={tw(\n 'w-full max-h-[calc(100vh-6rem)] bg-white shadow-xl ring-1 ring-black/5 outline-none rounded-xl',\n 'flex flex-col overflow-x-hidden overflow-y-auto',\n className\n )}\n >\n {({ close }) => (\n <DialogContext.Provider value={{ close }}>\n <div className=\"flex items-start justify-between gap-4 pt-6 px-6\">\n {title ? (\n <Heading slot=\"title\" className=\"flex-1 text-lg font-semibold text-neutral-900\">\n {title}\n </Heading>\n ) : (\n <div />\n )}\n </div>\n\n <div className=\"flex flex-col gap-3 px-6 pb-6\">\n <div className=\"\">{children}</div>\n <div className=\"flex gap-2 items-start\">{actions}</div>\n </div>\n </DialogContext.Provider>\n )}\n </AriaDialog>\n </Modal>\n </ModalOverlay>\n )\n}\n\nfunction CancelButton({ children = 'Cancel' }: { children?: React.ReactNode }) {\n const context = React.useContext(DialogContext)\n return (\n <Button color=\"neutral\" onClick={context.close} variant=\"soft\" shape=\"oval\">\n {children}\n </Button>\n )\n}\n\nexport const Dialog = Object.assign(Dialog_, {\n CancelButton\n})\n"}]},{"name":"drawer","dependencies":["tw","headless-button","compose-refs","use-render-props","icon"],"files":[{"name":"components/drawer.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add drawer --directory app\n\nimport React from 'react'\nimport type { To, RelativeRoutingType } from 'react-router'\nimport { useNavigate } from 'react-router'\nimport { Drawer as VaulDrawer } from 'vaul'\nimport { tw } from '../utils/tw'\n\nexport type DrawerProps = {\n children?: React.ReactNode\n title?: React.ReactNode\n isOpen?: boolean\n close?:\n | ((isOpen: boolean) => void)\n | { to: To; replace?: boolean; relative?: RelativeRoutingType; reloadDocument?: boolean }\n size?: 'sm' | 'md' | 'lg'\n className?: string\n isDismissable?: boolean\n position?: 'left' | 'right' | 'top' | 'bottom'\n actions?: React.ReactNode\n contentsContainerClassName?: string\n}\n\nexport function Drawer({\n children,\n title,\n isOpen = false,\n close,\n size = 'md',\n className,\n isDismissable = true,\n position = 'right',\n actions,\n contentsContainerClassName\n}: DrawerProps) {\n const navigate = useNavigate()\n\n const onOpenChange = close\n ? typeof close === 'function'\n ? close\n : (open: boolean) => {\n if (!open) navigate(close.to, { replace: close.replace, relative: close.relative })\n }\n : undefined\n\n return (\n <VaulDrawer.Root open={isOpen} onOpenChange={onOpenChange} direction={position} dismissible={isDismissable}>\n <VaulDrawer.Portal>\n <VaulDrawer.Overlay className=\"fixed inset-0 z-49 bg-black/25 backdrop-blur-sm\" />\n <VaulDrawer.Content\n className={tw(\n 'flex h-auto flex-col text-sm group/drawer-content fixed z-50',\n 'data-[vaul-drawer-direction=bottom]:bottom-2 data-[vaul-drawer-direction=bottom]:left-2 data-[vaul-drawer-direction=bottom]:right-2 data-[vaul-drawer-direction=bottom]:max-h-[80vh]',\n 'data-[vaul-drawer-direction=left]:left-2 data-[vaul-drawer-direction=left]:top-2 data-[vaul-drawer-direction=left]:bottom-2 data-[vaul-drawer-direction=left]:h-[calc(100%-1rem)] data-[vaul-drawer-direction=left]:w-3/4',\n 'data-[vaul-drawer-direction=right]:right-2 data-[vaul-drawer-direction=right]:top-2 data-[vaul-drawer-direction=right]:bottom-2 data-[vaul-drawer-direction=right]:h-[calc(100%-1rem)] data-[vaul-drawer-direction=right]:w-3/4',\n 'data-[vaul-drawer-direction=top]:top-2 data-[vaul-drawer-direction=top]:left-2 data-[vaul-drawer-direction=top]:right-2 data-[vaul-drawer-direction=top]:max-h-[80vh]',\n size === 'sm'\n ? 'data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm'\n : size === 'md'\n ? 'data-[vaul-drawer-direction=left]:sm:max-w-lg data-[vaul-drawer-direction=right]:sm:max-w-lg'\n : 'data-[vaul-drawer-direction=left]:sm:max-w-3xl data-[vaul-drawer-direction=right]:sm:max-w-3xl',\n className\n )}\n style={{ '--initial-transform': 'calc(100% + 8px)' } as React.CSSProperties}\n >\n {isDismissable ? (\n <div\n className={tw(\n 'absolute bg-neutral-300 rounded-full shrink-0 group-data-[vaul-drawer-direction=bottom]/drawer-content:block',\n 'group-data-[vaul-drawer-direction=left]/drawer-content:h-25 group-data-[vaul-drawer-direction=left]/drawer-content:w-1 group-data-[vaul-drawer-direction=left]/drawer-content:right-2 group-data-[vaul-drawer-direction=left]/drawer-content:top-[calc(50%-50px)]',\n 'group-data-[vaul-drawer-direction=right]/drawer-content:h-25 group-data-[vaul-drawer-direction=right]/drawer-content:w-1 group-data-[vaul-drawer-direction=right]/drawer-content:left-2 group-data-[vaul-drawer-direction=right]/drawer-content:top-[calc(50%-50px)]',\n 'group-data-[vaul-drawer-direction=top]/drawer-content:w-25 group-data-[vaul-drawer-direction=top]/drawer-content:h-1 group-data-[vaul-drawer-direction=top]/drawer-content:bottom-2 group-data-[vaul-drawer-direction=top]/drawer-content:left-[calc(50%-50px)]',\n 'group-data-[vaul-drawer-direction=bottom]/drawer-content:w-25 group-data-[vaul-drawer-direction=bottom]/drawer-content:h-1 group-data-[vaul-drawer-direction=bottom]/drawer-content:top-2 group-data-[vaul-drawer-direction=bottom]/drawer-content:left-[calc(50%-50px)]'\n )}\n />\n ) : null}\n\n <div className=\"bg-white size-full pt-6 px-6 pb-6 rounded-xl shadow-xl ring-1 ring-black/5\">\n <div className=\"flex items-start justify-between gap-4 pb-5\">\n {title ? (\n <VaulDrawer.Title className=\"flex-1 text-xl font-semibold text-neutral-900\">{title}</VaulDrawer.Title>\n ) : (\n <div />\n )}\n <div className=\"flex gap-2 items-start\">{actions}</div>\n </div>\n\n <div className={tw(contentsContainerClassName)}>{children}</div>\n </div>\n </VaulDrawer.Content>\n </VaulDrawer.Portal>\n </VaulDrawer.Root>\n )\n}\n"}]},{"name":"alert-dialog","dependencies":["tw","spinner","button-context","get-button-classes","headless-button","compose-refs","use-prevent-default","use-render-props","use-spin-delay","use-stable-accessor","icon","button-group","button"],"files":[{"name":"components/alert-dialog.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add alert-dialog --directory app\n\nimport * as React from 'react'\nimport { Dialog, Modal, ModalOverlay, Heading } from 'react-aria-components'\nimport { tw } from '../utils/tw'\nimport { Icon } from './icon'\nimport { Button } from './button'\nimport { ButtonGroupContext } from './button-group'\nimport { ButtonContext } from './helpers/button-context'\n\nexport type AlertDialogProps = {\n title: string\n description?: string\n variant?: 'info' | 'positive' | 'neutral' | 'negative' | 'notice'\n icon?: React.ReactNode\n children?: React.ReactNode | ((props: { close(): void }) => React.ReactNode)\n isOpen?: boolean\n onOpenChange?: (isOpen: boolean) => void\n isDismissable?: boolean\n className?: string\n}\n\nconst variantStyles = {\n info: {\n container: 'border-info-500/20',\n icon: 'text-info-500',\n iconBg: 'bg-info-500/10',\n header: 'text-info-500'\n },\n positive: {\n container: 'border-positive-200',\n icon: 'text-positive-600',\n iconBg: 'bg-positive-100',\n header: 'text-positive-800'\n },\n notice: {\n container: 'border-notice-200',\n icon: 'text-notice-600',\n iconBg: 'bg-notice-100',\n header: 'text-notice-800'\n },\n negative: {\n container: 'border-negative-200',\n icon: 'text-negative-600',\n iconBg: 'bg-negative-100',\n header: 'text-negative-800'\n },\n neutral: {\n container: 'border-neutral-200',\n icon: 'text-neutral-600',\n iconBg: 'bg-neutral-100',\n header: 'text-neutral-700'\n }\n}\n\nconst defaultIconNames = {\n info: 'information-circle',\n positive: 'check',\n neutral: 'information-circle',\n negative: 'x-mark',\n notice: 'exclamation-triangle'\n} as const\n\ninterface AlertDialogContextValue {\n close(): void\n}\n\nconst AlertDialogContext = React.createContext<AlertDialogContextValue>({ close: () => undefined })\n\nfunction AlertDialog_({\n description,\n title,\n variant = 'info',\n icon,\n isOpen,\n onOpenChange,\n isDismissable = true,\n className,\n children\n}: AlertDialogProps) {\n const displayIcon = icon !== undefined ? icon : <Icon name={defaultIconNames[variant]} className=\"h-8 w-8\" />\n const styles = variantStyles[variant]\n\n return (\n <ModalOverlay\n isOpen={isOpen}\n onOpenChange={onOpenChange}\n isDismissable={isDismissable}\n isKeyboardDismissDisabled={!isDismissable}\n className={tw(\n 'fixed inset-0 z-[4000] flex items-center justify-center bg-black/25 backdrop-blur-sm',\n 'transition-opacity duration-200 ease-out',\n 'data-[entering]:opacity-0',\n 'data-[exiting]:opacity-0'\n )}\n >\n <Modal\n className={tw(\n 'w-[calc(100%-2rem)] max-w-sm',\n 'transition-all duration-200 ease-out',\n 'data-[entering]:scale-95 data-[entering]:opacity-0 data-[entering]:translate-y-2',\n 'data-[exiting]:scale-95 data-[exiting]:opacity-0 data-[exiting]:translate-y-2'\n )}\n >\n <Dialog className={tw('outline-none bg-white rounded-xl shadow-xl ring-1 ring-black/5 w-full', className)} role=\"alertdialog\">\n {({ close }) => (\n <AlertDialogContext.Provider value={{ close }}>\n <ButtonGroupContext.Provider\n value={{ orientation: 'horizontal', alignment: 'center', fullWidth: true, className: 'flex-nowrap' }}\n >\n <ButtonContext.Provider value={{ shape: 'rectangle', variant: 'soft', fullWidth: true }}>\n <div className={tw('border rounded-xl p-6', styles.container)}>\n <div className=\"flex flex-col items-center text-center\">\n {displayIcon && (\n <div className={tw('w-12 h-12 rounded-xl flex items-center justify-center mb-4', styles.iconBg)}>\n <div className={tw(styles.icon)}>{displayIcon}</div>\n </div>\n )}\n <Heading\n slot=\"title\"\n className={tw('text-xl font-semibold', description ? 'mb-2' : 'mb-5', styles.header)}\n >\n {title}\n </Heading>\n {description && <div className=\"text-sm text-neutral-600 mb-5\">{description}</div>}\n {typeof children === 'function' ? children({ close }) : children}\n </div>\n </div>\n </ButtonContext.Provider>\n </ButtonGroupContext.Provider>\n </AlertDialogContext.Provider>\n )}\n </Dialog>\n </Modal>\n </ModalOverlay>\n )\n}\n\nAlertDialog_.displayName = 'AlertDialog'\n\nfunction CancelButton({ children = 'Cancel' }: { children?: React.ReactNode }) {\n const context = React.useContext(AlertDialogContext)\n return (\n <Button color=\"neutral\" onClick={context.close} variant=\"soft\" shape=\"rectangle\">\n {children}\n </Button>\n )\n}\n\nexport const AlertDialog = Object.assign(AlertDialog_, {\n CancelButton\n})\n"}]},{"name":"menu","dependencies":["tw","animated-popover"],"files":[{"name":"components/menu.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add menu --directory app\n\nimport * as React from 'react'\nimport {\n Menu as AriaMenu,\n MenuItem as AriaMenuItem,\n MenuSection as AriaMenuSection,\n MenuTrigger as AriaMenuTrigger,\n Pressable,\n Separator as AriaSeparator\n} from 'react-aria-components'\nimport { Link, useHref, type RelativeRoutingType, type To } from 'react-router'\nimport { AnimatedPopover } from './helpers/animated-popover'\nimport { tw } from '../utils/tw'\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type MenuProps = {\n /** Trigger element. Can be a ReactNode or render function receiving `{ isOpen }`. */\n Button: React.ReactNode | ((props: { isOpen: boolean }) => React.ReactElement)\n children?: React.ReactNode\n className?: string\n containerClassName?: string\n placement?: 'bottom' | 'bottom start' | 'bottom end' | 'top' | 'top start' | 'top end' | 'left' | 'right'\n}\n\nexport type MenuItemProps = {\n children?: React.ReactNode\n className?: string\n isDisabled?: boolean\n id?: string\n textValue?: string\n type?: 'button' | 'submit'\n onAction?: () => void\n onClick?: ((e: React.MouseEvent<Element & HTMLOrSVGElement, MouseEvent>) => void) | undefined\n form?: string\n name?: string\n value?: string\n}\n\nexport type MenuLinkItemProps = {\n to: To\n relative?: RelativeRoutingType\n children?: React.ReactNode\n className?: string\n isDisabled?: boolean\n target?: React.HTMLAttributeAnchorTarget\n rel?: string\n id?: string\n textValue?: string\n}\n\nexport type MenuGroupProps = {\n children?: React.ReactNode\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction itemClassName({ isFocused, isDisabled }: { isFocused: boolean; isDisabled: boolean }, className?: string) {\n return tw(\n isFocused ? 'bg-neutral-50 text-neutral-900' : 'text-neutral-700',\n 'group flex w-full items-center px-4 py-2 text-sm gap-2 outline-none',\n isDisabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',\n className\n )\n}\n\n// ── MenuMain ──────────────────────────────────────────────────────────────────\n\nfunction MenuMain({ Button: ButtonProp, children, className, containerClassName, placement = 'bottom end' }: MenuProps) {\n const [isOpen, setIsOpen] = React.useState(false)\n\n const triggerContent = typeof ButtonProp === 'function' ? ButtonProp({ isOpen }) : ButtonProp\n\n return (\n <div className={tw('relative inline-block text-left', containerClassName)}>\n <AriaMenuTrigger isOpen={isOpen} onOpenChange={setIsOpen}>\n <Pressable>\n {React.isValidElement(triggerContent) ? (\n triggerContent\n ) : (\n <button type=\"button\" className=\"inline-flex items-center focus:outline-none\">\n {triggerContent}\n </button>\n )}\n </Pressable>\n <AnimatedPopover placement={placement} offset={8}>\n <AriaMenu\n className={tw(\n 'overflow-hidden min-w-56 w-max rounded-lg bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none',\n className\n )}\n autoFocus=\"first\"\n >\n {children}\n </AriaMenu>\n </AnimatedPopover>\n </AriaMenuTrigger>\n </div>\n )\n}\n\nMenuMain.displayName = 'Menu'\n\n// ── MenuItem ──────────────────────────────────────────────────────────────────\n\nfunction Item({\n children,\n className,\n isDisabled,\n id,\n textValue,\n type,\n onAction,\n onClick,\n form,\n name,\n value\n}: MenuItemProps) {\n const itemRef = React.useRef<HTMLDivElement>(null)\n const isSubmit = type === 'submit'\n\n return (\n <AriaMenuItem\n id={id}\n ref={isSubmit ? itemRef : undefined}\n onClick={onClick}\n textValue={textValue ?? (typeof children === 'string' ? children : undefined)}\n onAction={\n isSubmit\n ? () => {\n onAction?.()\n const targetForm = form\n ? (document.getElementById(form) as HTMLFormElement | null)\n : (itemRef.current?.closest('form') ?? null)\n if (!targetForm) return\n targetForm.requestSubmit()\n }\n : onAction\n }\n isDisabled={isDisabled}\n className={(renderProps) => itemClassName(renderProps, className)}\n >\n {children}\n {isSubmit && <input type=\"submit\" className=\"sr-only\" name={name} value={value ?? ''} form={form} />}\n </AriaMenuItem>\n )\n}\n\n// ── LinkItem ──────────────────────────────────────────────────────────────────\n\nfunction LinkItem({ to, children, className, relative, isDisabled, target, rel, id, textValue }: MenuLinkItemProps) {\n return (\n <AriaMenuItem\n id={id}\n href={useHref(to, { relative })}\n target={target}\n rel={rel}\n textValue={textValue ?? (typeof children === 'string' ? children : undefined)}\n isDisabled={isDisabled}\n className={(renderProps) => itemClassName(renderProps, className)}\n render={(props) => <Link {...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)} relative={relative} to={to} />}\n >\n {children}\n </AriaMenuItem>\n )\n}\n\n// ── Group ─────────────────────────────────────────────────────────────────────\n\nfunction Group({ children }: MenuGroupProps) {\n return <AriaMenuSection className=\"py-1\">{children}</AriaMenuSection>\n}\n\n// ── Separator ─────────────────────────────────────────────────────────────────\n\nfunction MenuSeparator() {\n return <AriaSeparator className=\"border-t border-neutral-950/5\" />\n}\n\n// ── Export ─────────────────────────────────────────────────────────────────────\n\nexport const Menu = Object.assign(MenuMain, {\n Item,\n LinkItem,\n Group,\n Separator: MenuSeparator\n})\n"}]},{"name":"tabs","dependencies":["tw","use-tab-indicator"],"files":[{"name":"components/tabs.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add tabs --directory app\n\nimport * as React from 'react'\nimport { Tabs as AriaTabs, TabList as AriaTabList, Tab as AriaTab, TabPanel as AriaTabPanel } from 'react-aria-components'\nimport type { Key } from 'react-aria-components'\nimport { tw } from '../utils/tw'\nimport {\n TAB_BASE_CLASSES,\n TAB_DISABLED_CLASSES,\n TAB_INACTIVE_CLASSES,\n TAB_OVAL_BASE_CLASSES,\n TAB_OVAL_SELECTED_CLASSES,\n tabColorMap,\n tabListContainerClasses,\n tabListInnerClasses,\n getIndicatorClasses,\n useAnimatedIndicator\n} from '../utils/use-tab-indicator'\nimport type { TabColor, TabVariant } from '../utils/use-tab-indicator'\n\n// --- Types ---\n\nexport type TabGroupProps = {\n children: React.ReactNode\n className?: string\n defaultSelectedKey?: Key\n selectedKey?: Key\n onSelectionChange?: (key: Key) => void\n keyboardActivation?: 'automatic' | 'manual'\n orientation?: 'horizontal' | 'vertical'\n isDisabled?: boolean\n}\n\nexport type TabListProps = {\n children: React.ReactNode\n className?: string\n color?: TabColor\n variant?: TabVariant\n}\n\nexport type TabProps = {\n id: Key\n children: React.ReactNode\n className?: string\n isDisabled?: boolean\n}\n\nexport type TabPanelsProps = {\n children: React.ReactNode\n className?: string\n}\n\nexport type TabPanelProps = {\n id: Key\n children: React.ReactNode\n className?: string\n shouldForceMount?: boolean\n}\n\n// --- TabGroup ---\n\nfunction TabGroup({ children, className, ...props }: TabGroupProps) {\n return (\n <AriaTabs {...props} className={tw('w-full', className)}>\n {children}\n </AriaTabs>\n )\n}\n\nTabGroup.displayName = 'TabGroup'\n\n// --- TabList ---\n\nconst TabListContext = React.createContext<{ color: TabColor; variant: TabVariant }>({ color: 'primary', variant: 'underline' })\n\nfunction TabList({ children, className, color = 'primary', variant = 'underline' }: TabListProps) {\n const containerRef = React.useRef<HTMLDivElement>(null)\n const indicatorRef = useAnimatedIndicator(containerRef, '[data-selected]', 'data-selected')\n\n return (\n <TabListContext.Provider value={{ color, variant }}>\n <div ref={containerRef} className={tabListContainerClasses[variant]}>\n <AriaTabList className={tw(tabListInnerClasses[variant], className)}>{children}</AriaTabList>\n <div\n ref={indicatorRef}\n className={getIndicatorClasses(variant, color)}\n style={{ opacity: 0 }}\n />\n </div>\n </TabListContext.Provider>\n )\n}\n\nTabList.displayName = 'TabList'\n\n// --- Tab ---\n\nfunction TabItem({ children, className, ...props }: TabProps) {\n const { color, variant } = React.useContext(TabListContext)\n const colors = tabColorMap[color]\n const focusClasses = `outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:${colors.outline}`\n\n return (\n <AriaTab\n {...props}\n className={({ isSelected, isDisabled }) =>\n tw(\n variant === 'oval' ? TAB_OVAL_BASE_CLASSES : TAB_BASE_CLASSES,\n focusClasses,\n isSelected\n ? (variant === 'oval' ? TAB_OVAL_SELECTED_CLASSES : `${colors.text} font-bold`)\n : `${TAB_INACTIVE_CLASSES} data-[hovered]:text-neutral-700`,\n isDisabled && TAB_DISABLED_CLASSES,\n className\n )\n }\n >\n {children}\n </AriaTab>\n )\n}\n\nTabItem.displayName = 'Tab'\n\n// --- TabPanels ---\n\nfunction TabPanels({ children, className }: TabPanelsProps) {\n return <div className={tw('mt-2', className)}>{children}</div>\n}\n\nTabPanels.displayName = 'TabPanels'\n\n// --- TabPanel ---\n\nfunction TabPanelItem({ children, className, ...props }: TabPanelProps) {\n return (\n <AriaTabPanel {...props} className={className}>\n {children}\n </AriaTabPanel>\n )\n}\n\nTabPanelItem.displayName = 'TabPanel'\n\n// --- Export ---\n\nexport const Tabs = {\n Group: TabGroup,\n List: TabList,\n Tab: TabItem,\n Panels: TabPanels,\n Panel: TabPanelItem\n}\n"}]},{"name":"link-tabs","dependencies":["tw","use-tab-indicator"],"files":[{"name":"components/link-tabs.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add link-tabs --directory app\n\nimport * as React from 'react'\nimport { NavLink } from 'react-router'\nimport type { To } from 'react-router'\nimport { tw } from '../utils/tw'\nimport {\n TAB_BASE_CLASSES,\n TAB_DISABLED_CLASSES,\n TAB_INACTIVE_CLASSES,\n TAB_OVAL_BASE_CLASSES,\n TAB_OVAL_SELECTED_CLASSES,\n tabColorMap,\n tabListContainerClasses,\n tabListInnerClasses,\n getIndicatorClasses,\n useAnimatedIndicator\n} from '../utils/use-tab-indicator'\nimport type { TabColor, TabVariant } from '../utils/use-tab-indicator'\n\n// --- Types ---\n\nexport type LinkTabListProps = {\n children: React.ReactNode\n className?: string\n color?: TabColor\n variant?: TabVariant\n}\n\nexport type LinkTabProps = {\n to: To\n children: React.ReactNode\n className?: string\n disabled?: boolean\n replace?: boolean\n end?: boolean\n}\n\n// --- LinkTabList ---\n\nconst LinkTabListContext = React.createContext<{ color: TabColor; variant: TabVariant }>({ color: 'primary', variant: 'underline' })\n\nfunction LinkTabList({ children, className, color = 'primary', variant = 'underline' }: LinkTabListProps) {\n const containerRef = React.useRef<HTMLDivElement>(null)\n const indicatorRef = useAnimatedIndicator(containerRef, '[aria-current=\"page\"]', 'aria-current')\n\n return (\n <LinkTabListContext.Provider value={{ color, variant }}>\n <div ref={containerRef} className={tabListContainerClasses[variant]}>\n <div role=\"tablist\" className={tw(tabListInnerClasses[variant], className)}>\n {children}\n </div>\n <div\n ref={indicatorRef}\n className={getIndicatorClasses(variant, color)}\n style={{ opacity: 0 }}\n />\n </div>\n </LinkTabListContext.Provider>\n )\n}\n\nLinkTabList.displayName = 'LinkTabList'\n\n// --- LinkTab ---\n\nfunction LinkTab({ to, children, className, disabled, replace, end }: LinkTabProps) {\n const { color, variant } = React.useContext(LinkTabListContext)\n const colors = tabColorMap[color]\n const baseClasses = variant === 'oval' ? TAB_OVAL_BASE_CLASSES : TAB_BASE_CLASSES\n const focusClasses = `outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:${colors.outline}`\n\n if (disabled) {\n return (\n <span\n role=\"tab\"\n aria-disabled=\"true\"\n className={tw(\n baseClasses,\n 'outline-none',\n TAB_INACTIVE_CLASSES,\n TAB_DISABLED_CLASSES,\n className\n )}\n >\n {children}\n </span>\n )\n }\n\n return (\n <NavLink\n to={to}\n replace={replace}\n end={end}\n role=\"tab\"\n className={({ isActive }) =>\n tw(\n baseClasses,\n focusClasses,\n isActive\n ? (variant === 'oval' ? TAB_OVAL_SELECTED_CLASSES : `${colors.text} font-bold`)\n : `${TAB_INACTIVE_CLASSES} hover:text-neutral-700`,\n className\n )\n }\n >\n {children}\n </NavLink>\n )\n}\n\nLinkTab.displayName = 'LinkTab'\n\n// --- Export ---\n\nexport const LinkTabs = {\n List: LinkTabList,\n Tab: LinkTab\n}\n"}]},{"name":"stepper","dependencies":["tw","icon"],"files":[{"name":"components/stepper.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add stepper --directory app\n\nimport * as React from 'react'\nimport { Icon } from './icon'\nimport { tw } from '../utils/tw'\n\n// ── Types ────────────────────────────────────────────────────────────────────────\n\nexport type StepperGroupProps = {\n children: React.ReactNode\n className?: string\n activeStep?: number\n orientation?: 'horizontal' | 'vertical'\n}\n\nexport type StepProps = {\n children: React.ReactNode\n className?: string\n completed?: boolean\n error?: boolean\n}\n\nexport type StepLabelProps = {\n children: React.ReactNode\n className?: string\n}\n\n// ── Context ─────────────────────────────────────────────────────────────────────\n\nconst StepperContext = React.createContext<{\n activeStep: number\n orientation: 'horizontal' | 'vertical'\n totalSteps: number\n}>({\n activeStep: 0,\n orientation: 'horizontal',\n totalSteps: 0\n})\n\nconst StepContext = React.createContext<{\n completed: boolean\n active: boolean\n error: boolean\n index: number\n}>({\n completed: false,\n active: false,\n error: false,\n index: 0\n})\n\n// ── StepperGroup ────────────────────────────────────────────────────────────────\n\nfunction StepperGroup({ children, className, activeStep = 0, orientation = 'horizontal' }: StepperGroupProps) {\n const steps = React.Children.toArray(children).filter((child) => React.isValidElement(child))\n const totalSteps = steps.length\n\n return (\n <StepperContext.Provider value={{ activeStep, orientation, totalSteps }}>\n <div\n className={tw('flex', orientation === 'horizontal' ? 'flex-row' : 'flex-col', className)}\n role=\"group\"\n aria-label=\"Progress\"\n >\n {steps.map((step, idx) => {\n if (React.isValidElement<StepProps & { index?: number; isLast?: boolean }>(step)) {\n return React.cloneElement(step, {\n ...step.props,\n index: idx,\n isLast: idx === totalSteps - 1,\n key: idx\n })\n }\n return step\n })}\n </div>\n </StepperContext.Provider>\n )\n}\n\nStepperGroup.displayName = 'StepperGroup'\n\n// ── Step ────────────────────────────────────────────────────────────────────────\n\nfunction Step({\n children,\n className,\n completed = false,\n error = false,\n index = 0,\n isLast\n}: StepProps & { index?: number; isLast?: boolean }) {\n const { activeStep, orientation } = React.useContext(StepperContext)\n const active = activeStep === index\n\n const firstConnectorColor = error ? 'bg-negative-500' : completed || active ? 'bg-primary-500' : 'bg-neutral-300'\n const lastConnectorColor = error ? 'bg-negative-500' : completed ? 'bg-primary-500' : 'bg-neutral-300'\n\n if (orientation === 'horizontal') {\n return (\n <StepContext.Provider value={{ completed, active, error, index }}>\n <div className={tw('flex flex-row items-start flex-1 relative', className)}>\n {index !== 0 ? (\n <div className=\"flex flex-1 items-center pt-4 w-1/2 absolute left-0\">\n <div className={tw('h-0.5 w-full transition-colors', firstConnectorColor)} />\n </div>\n ) : null}\n {!isLast ? (\n <div className=\"flex flex-1 items-center pt-4 w-1/2 absolute right-0\">\n <div className={tw('h-0.5 w-full transition-colors', lastConnectorColor)} />\n </div>\n ) : null}\n <div className=\"flex flex-col items-center w-full relative\">{children}</div>\n </div>\n </StepContext.Provider>\n )\n }\n\n return (\n <StepContext.Provider value={{ completed, active, error, index }}>\n <div className={tw('relative flex flex-col min-h-10', className)}>\n {index !== 0 ? (\n <div className=\"flex flex-1 justify-center pl-4 h-1/2 absolute top-0\">\n <div className={tw('w-0.5 h-full transition-colors', firstConnectorColor)} />\n </div>\n ) : null}\n {!isLast ? (\n <div className=\"flex flex-1 items-center pl-4 h-1/2 absolute bottom-0\">\n <div className={tw('w-0.5 h-full transition-colors', lastConnectorColor)} />\n </div>\n ) : null}\n <div className=\"flex flex-row w-full relative\">{children}</div>\n </div>\n </StepContext.Provider>\n )\n}\n\nStep.displayName = 'Step'\n\n// ── StepLabel ───────────────────────────────────────────────────────────────────\n\nfunction StepLabel({ children, className }: StepLabelProps) {\n const { completed, active, error, index } = React.useContext(StepContext)\n const { orientation } = React.useContext(StepperContext)\n\n const iconBg = error\n ? 'bg-negative-500 border-negative-500 text-white'\n : completed\n ? 'bg-primary-500 border-primary-500 text-white'\n : active\n ? 'bg-primary-500 border-primary-500 text-white'\n : 'bg-white border-neutral-300 text-neutral-600'\n\n const textColor = error ? 'text-negative-600' : active ? 'text-neutral-900' : 'text-neutral-500'\n\n return (\n <div className={tw('flex items-center', orientation === 'horizontal' ? 'flex-col text-center' : 'gap-3', className)}>\n <div\n className={tw('flex size-8 shrink-0 items-center justify-center rounded-full border-2 transition-colors', iconBg)}\n aria-current={active ? 'step' : undefined}\n >\n {completed ? (\n <Icon name=\"check\" className=\"size-4\" aria-hidden=\"true\" />\n ) : (\n <span className=\"text-sm font-semibold\">{index + 1}</span>\n )}\n </div>\n <div className={tw('flex flex-col', orientation === 'horizontal' ? 'mt-2' : '')}>\n <span className={tw('text-sm font-medium', textColor)}>{children}</span>\n </div>\n </div>\n )\n}\n\nStepLabel.displayName = 'StepLabel'\n\n// ── Export ───────────────────────────────────────────────────────────────────────\n\nexport const Stepper = {\n Group: StepperGroup,\n Step,\n Label: StepLabel\n}\n"}]},{"name":"avatar","dependencies":["tw"],"files":[{"name":"components/avatar.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add avatar --directory app\n\nimport * as React from 'react'\nimport { tw } from '../utils/tw'\n\nexport type AvatarShape = 'circle' | 'rounded' | 'square'\n\nexport type AvatarProps = {\n alt?: string\n children?: React.ReactNode\n className?: string\n src?: string\n shape?: AvatarShape\n}\n\nexport const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(\n ({ alt, children, className, src, shape = 'circle' }, ref) => {\n const [imgError, setImgError] = React.useState(false)\n\n const showImage = src && !imgError\n const childrenToRender = children || (alt && stringAvatar(alt))\n\n return (\n <div\n ref={ref}\n className={tw(\n 'size-10 relative inline-flex items-center justify-center overflow-hidden font-medium text-white select-none ring-1 ring-black/5',\n !showImage && 'bg-neutral-300',\n shape === 'circle' && 'rounded-full',\n shape === 'rounded' && 'rounded',\n shape === 'square' && 'rounded-none',\n className\n )}\n >\n {showImage ? (\n <img className=\"size-full absolute inset-0 object-cover\" alt={alt} src={src} onError={() => setImgError(true)} />\n ) : null}\n {!showImage && childrenToRender ? <span>{childrenToRender}</span> : null}\n </div>\n )\n }\n)\n\nAvatar.displayName = 'Avatar'\n\nfunction stringAvatar(name: string) {\n const names = name.split(' ')\n const first = names[0]\n const last = names[names.length - 1]\n\n if (!first) return ''\n\n const initials = names.length === 1 ? first[0] ?? '' : `${first[0] ?? ''}${last?.[0] ?? ''}`\n\n return initials.toUpperCase()\n}\n"}]},{"name":"chip","dependencies":["tw","icon"],"files":[{"name":"components/chip.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add chip --directory app\n\nimport * as React from 'react'\nimport { tw } from '../utils/tw'\nimport { Icon } from './icon'\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\nexport type ChipProps = {\n children: React.ReactNode\n onRemove?: () => void\n isDisabled?: boolean\n size?: 'sm' | 'md' | 'lg'\n color?: 'stone' | 'info' | 'green' | 'yellow' | 'orange' | 'red'\n colorMode?: 'light' | 'dark'\n className?: string\n style?: React.CSSProperties\n}\n\nexport type ChipButtonProps = {\n children: React.ReactNode\n disabled?: boolean\n size?: 'sm' | 'md' | 'lg'\n color?: 'stone' | 'info' | 'green' | 'yellow' | 'orange' | 'red'\n colorMode?: 'light' | 'dark'\n className?: string\n} & Omit<React.ComponentProps<'button'>, 'disabled' | 'size' | 'color'>\n\n// ── Chip ──────────────────────────────────────────────────────────────────────\n\nexport function Chip(props: ChipProps) {\n const {\n children,\n onRemove,\n isDisabled = false,\n size = 'md',\n color = 'stone',\n colorMode = 'light',\n className,\n style\n } = props\n\n const colorClasses = colorMode === 'dark' ? darkColorClasses : lightColorClasses\n\n return (\n <div\n className={tw(\n 'inline-flex items-center rounded-full font-normal transition-colors max-w-full',\n sizeClasses[size],\n !isDisabled && !style && colorClasses[color],\n isDisabled && 'opacity-50 cursor-not-allowed',\n className\n )}\n style={style}\n >\n <span className=\"truncate\">{children}</span>\n {onRemove && (\n <button\n type=\"button\"\n onClick={onRemove}\n disabled={isDisabled}\n className={tw(\n 'inline-flex items-center justify-center rounded-full transition-colors translate-x-1',\n removeBtnSizeClasses[size],\n !isDisabled && 'hover:bg-neutral-950/10',\n isDisabled && 'cursor-not-allowed'\n )}\n aria-label=\"Remove\"\n tabIndex={-1}\n >\n <Icon name=\"x-mark\" className={removeIconSizeClasses[size]} aria-hidden=\"true\" />\n </button>\n )}\n </div>\n )\n}\n\n// ── ChipButton ────────────────────────────────────────────────────────────────\n\nexport const ChipButton = React.forwardRef<HTMLButtonElement, ChipButtonProps>((props, ref) => {\n const { children, size = 'md', color = 'stone', colorMode = 'light', className, disabled, ...rest } = props\n\n const colorClasses = colorMode === 'dark' ? darkHoverColorClasses : lightHoverColorClasses\n\n return (\n <button\n ref={ref}\n type=\"button\"\n disabled={disabled}\n className={tw(\n 'inline-flex items-center rounded-full font-normal transition-colors',\n sizeClasses[size],\n !disabled && colorClasses[color],\n disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',\n className\n )}\n {...rest}\n >\n {children}\n </button>\n )\n})\n\nChipButton.displayName = 'ChipButton'\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nconst sizeClasses = {\n sm: 'px-2 py-0.5 text-xs',\n md: 'px-2.5 py-1 text-sm',\n lg: 'px-3 py-1.5 text-base'\n}\n\nconst lightColorClasses = {\n stone: 'bg-stone-100 text-stone-700',\n info: 'bg-info-100 text-info-700',\n green: 'bg-green-100 text-green-700',\n yellow: 'bg-yellow-100 text-yellow-700',\n orange: 'bg-orange-100 text-orange-700',\n red: 'bg-red-100 text-red-700'\n}\n\nconst darkColorClasses = {\n stone: 'bg-stone-600 text-white',\n info: 'bg-info-600 text-white',\n green: 'bg-green-600 text-white',\n yellow: 'bg-yellow-600 text-white',\n orange: 'bg-orange-600 text-white',\n red: 'bg-red-600 text-white'\n}\n\nconst lightHoverColorClasses = {\n stone: 'bg-stone-100 text-stone-700 hover:bg-stone-200',\n info: 'bg-info-100 text-info-700 hover:bg-info-200',\n green: 'bg-green-100 text-green-700 hover:bg-green-200',\n yellow: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200',\n orange: 'bg-orange-100 text-orange-700 hover:bg-orange-200',\n red: 'bg-red-100 text-red-700 hover:bg-red-200'\n}\n\nconst darkHoverColorClasses = {\n stone: 'bg-stone-600 text-white hover:bg-stone-700',\n info: 'bg-info-600 text-white hover:bg-info-700',\n green: 'bg-green-600 text-white hover:bg-green-700',\n yellow: 'bg-yellow-600 text-white hover:bg-yellow-700',\n orange: 'bg-orange-600 text-white hover:bg-orange-700',\n red: 'bg-red-600 text-white hover:bg-red-700'\n}\n\nconst removeBtnSizeClasses = {\n sm: 'size-4 -m-0.5 p-0.5',\n md: 'size-5 -m-1 p-1',\n lg: 'size-6 -m-1.5 p-1.5'\n}\n\nconst removeIconSizeClasses = {\n sm: 'size-2',\n md: 'size-3',\n lg: 'size-4'\n}\n"}]},{"name":"enum-chip","dependencies":["tw","colors","icon","chip"],"files":[{"name":"components/enum-chip.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add enum-chip --directory app\n\nimport * as React from 'react'\nimport { Chip, type ChipProps } from './chip'\nimport { getColor, stringToNumber } from '../utils/enum-colors'\n\nexport type EnumChipProps = {\n options: string[] | Readonly<string[]>\n value: string\n enumLabels?: { [key: string]: string }\n} & Omit<ChipProps, 'children'>\n\nexport function EnumChip({ options, enumLabels, value, style, ...props }: EnumChipProps) {\n const colorIndex = Object.values(options).indexOf(value)\n const colorSeed = colorIndex >= 0 ? colorIndex : stringToNumber(value)\n const color = getColor(colorSeed)\n\n const label = value ? (enumLabels ? enumLabels[value] : value) : ''\n\n return (\n <Chip\n {...props}\n style={{\n backgroundColor: `${color}19`,\n color: color,\n ...style\n }}\n >\n {label}\n </Chip>\n )\n}\n"},{"name":"utils/enum-colors.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add enum-chip --directory app\n\nexport { COLORS, getColor, stringToNumber } from './colors'\n"}]},{"name":"img","dependencies":["tw"],"files":[{"name":"components/img.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add img --directory app\n\nimport * as React from 'react'\nimport { tw } from '../utils/tw'\n\nexport type ImageFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'\nexport type ImageShape = 'rounded' | 'rectangle' | 'circle'\n\nexport type ImgProps = {\n src: string\n alt?: string\n className?: string\n fit?: ImageFit\n shape?: ImageShape\n style?: React.CSSProperties\n onError?: React.ReactEventHandler<HTMLImageElement>\n}\n\nconst fitClasses: Record<ImageFit, string> = {\n cover: 'object-cover object-center',\n contain: 'object-contain',\n fill: 'object-fill',\n none: 'object-none',\n 'scale-down': 'object-scale-down'\n}\n\nconst shapeClasses: Record<ImageShape, string> = {\n rounded: 'rounded',\n rectangle: 'rounded-none',\n circle: 'rounded-full'\n}\n\nexport const Img = React.forwardRef<HTMLImageElement, ImgProps>(\n ({ src, alt, className, fit = 'cover', shape = 'rounded', style, onError }, ref) => {\n return (\n <img\n ref={ref}\n src={src}\n alt={alt}\n className={tw('w-full h-full', fitClasses[fit], shapeClasses[shape], className)}\n style={style}\n onError={onError}\n />\n )\n }\n)\n\nImg.displayName = 'Img'\n"}]},{"name":"labeled-value","dependencies":["tw"],"files":[{"name":"components/labeled-value.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add labeled-value --directory app\n\nimport * as React from 'react'\nimport { tw } from '../utils/tw'\nimport {\n type Iso,\n dateFns,\n timeFns,\n dateTimeFns,\n instantFns,\n zonedDateTimeFns,\n yearMonthFns,\n monthDayFns,\n durationFns\n} from 'iso-fns'\n\ntype LabeledValueBaseProps = {\n label?: React.ReactNode\n align?: 'start' | 'end'\n labelAlign?: 'start' | 'end'\n orientation?: 'horizontal' | 'vertical'\n emptyText?: React.ReactNode\n className?: string\n labelClassName?: string\n valueClassName?: string\n id?: string\n 'aria-labelledby'?: string\n 'aria-describedby'?: string\n}\n\ntype RangeValue<T> = { start: T; end: T }\n\ntype StringProps = {\n value: string | null | undefined\n formatOptions?: never\n}\n\ntype NumberProps = {\n value: number | RangeValue<number> | null | undefined\n formatOptions?: Intl.NumberFormatOptions\n}\n\ntype BooleanProps = {\n value: boolean | null | undefined\n formatOptions?: {\n trueLabel?: string\n falseLabel?: string\n }\n}\n\ntype ReactNodeProps = {\n value: React.ReactNode\n formatOptions?: never\n}\n\ntype StringListProps = {\n value: string[] | null | undefined\n formatOptions?: Intl.ListFormatOptions\n}\n\ntype DateProps = {\n value: Iso.Date | RangeValue<Iso.Date> | null | undefined\n formatOptions?: {\n dateFormat?: string\n }\n}\n\ntype TimeProps = {\n value: Iso.Time | RangeValue<Iso.Time> | null | undefined\n formatOptions?: {\n timeFormat?: string\n }\n}\n\ntype DateTimeProps = {\n value: Iso.DateTime | RangeValue<Iso.DateTime> | null | undefined\n formatOptions?: {\n dateTimeFormat?: string\n }\n}\n\ntype InstantProps = {\n value: Iso.Instant | RangeValue<Iso.Instant> | null | undefined\n formatOptions?: {\n dateTimeFormat?: string\n timeZone?: string\n }\n}\n\ntype ZonedDateTimeProps = {\n value: Iso.ZonedDateTime | RangeValue<Iso.ZonedDateTime> | null | undefined\n formatOptions?: {\n dateTimeFormat?: string\n }\n}\n\ntype YearMonthProps = {\n value: Iso.YearMonth | RangeValue<Iso.YearMonth> | null | undefined\n formatOptions?: {\n dateFormat?: string\n }\n}\n\ntype MonthDayProps = {\n value: Iso.MonthDay | RangeValue<Iso.MonthDay> | null | undefined\n formatOptions?: {\n dateFormat?: string\n }\n}\n\ntype DurationProps = {\n value: Iso.Duration | RangeValue<Iso.Duration> | null | undefined\n formatOptions?: {\n style?: 'short' | 'long'\n }\n}\n\ntype LabeledValueFormatProps =\n | StringProps\n | NumberProps\n | BooleanProps\n | ReactNodeProps\n | StringListProps\n | DateProps\n | TimeProps\n | DateTimeProps\n | InstantProps\n | ZonedDateTimeProps\n | YearMonthProps\n | MonthDayProps\n | DurationProps\n\nexport type LabeledValueProps = LabeledValueBaseProps & LabeledValueFormatProps\n\nexport const LabeledValue = React.forwardRef<HTMLDivElement, LabeledValueProps>(function LabeledValue(props, ref) {\n const {\n label,\n align = 'start',\n labelAlign = 'start',\n orientation = 'vertical',\n className,\n labelClassName,\n valueClassName,\n id,\n 'aria-labelledby': ariaLabelledBy,\n 'aria-describedby': ariaDescribedBy\n } = props\n\n const labelId = React.useId()\n const actualLabelId = id ?? labelId\n\n const formattedValue = FormattedValue(props)\n\n return (\n <div\n ref={ref}\n className={tw('relative', orientation === 'horizontal' ? 'flex items-baseline gap-4' : 'block', className)}\n >\n {label && (\n <LabeledValueLabel\n id={actualLabelId}\n className={tw(\n orientation === 'horizontal' ? 'shrink-0' : 'mb-1',\n labelAlign === 'end' && 'text-right',\n labelClassName\n )}\n >\n {label}\n </LabeledValueLabel>\n )}\n\n <div\n className={tw(\n 'text-sm text-neutral-900 min-h-4 max-w-full break-words overflow-wrap-anywhere',\n align === 'end' && 'text-right',\n !formattedValue && 'text-neutral-400',\n valueClassName\n )}\n aria-labelledby={ariaLabelledBy ?? (label ? actualLabelId : undefined)}\n aria-describedby={ariaDescribedBy}\n >\n {formattedValue}\n </div>\n </div>\n )\n})\n\nLabeledValue.displayName = 'LabeledValue'\n\nfunction FormattedValue(props: LabeledValueFormatProps & { emptyText?: React.ReactNode }) {\n if (props.value == null || props.value === '') {\n return <>{props.emptyText}</>\n }\n\n if (React.isValidElement(props.value)) {\n return <>{props.value}</>\n }\n\n if (typeof props.value === 'boolean') {\n const boolProps = props as BooleanProps\n return <>{props.value ? (boolProps.formatOptions?.trueLabel ?? 'Yes') : (boolProps.formatOptions?.falseLabel ?? 'No')}</>\n }\n\n if (Array.isArray(props.value)) {\n return <ListValue {...(props as StringListProps)} />\n }\n\n if (typeof props.value === 'object' && props.value && 'start' in props.value && 'end' in props.value) {\n if (typeof props.value.start === 'number') {\n return <NumberValue {...(props as NumberProps)} />\n }\n if (typeof props.value.start === 'string' && dateFns.isValid(props.value.start)) {\n return <DateValue {...(props as DateProps)} />\n }\n if (typeof props.value.start === 'string' && timeFns.isValid(props.value.start)) {\n return <TimeValue {...(props as TimeProps)} />\n }\n if (typeof props.value.start === 'string' && dateTimeFns.isValid(props.value.start)) {\n return <DateTimeValue {...(props as DateTimeProps)} />\n }\n if (typeof props.value.start === 'string' && instantFns.isValid(props.value.start)) {\n return <InstantValue {...(props as InstantProps)} />\n }\n if (typeof props.value.start === 'string' && zonedDateTimeFns.isValid(props.value.start)) {\n return <ZonedDateTimeValue {...(props as ZonedDateTimeProps)} />\n }\n if (typeof props.value.start === 'string' && yearMonthFns.isValid(props.value.start)) {\n return <YearMonthValue {...(props as YearMonthProps)} />\n }\n if (typeof props.value.start === 'string' && monthDayFns.isValid(props.value.start)) {\n return <MonthDayValue {...(props as MonthDayProps)} />\n }\n if (typeof props.value.start === 'string' && durationFns.isValid(props.value.start)) {\n return <DurationValue {...(props as DurationProps)} />\n }\n }\n\n if (typeof props.value === 'string') {\n if (dateFns.isValid(props.value)) {\n return <DateValue {...(props as DateProps)} />\n }\n if (timeFns.isValid(props.value)) {\n return <TimeValue {...(props as TimeProps)} />\n }\n if (dateTimeFns.isValid(props.value)) {\n return <DateTimeValue {...(props as DateTimeProps)} />\n }\n if (instantFns.isValid(props.value)) {\n return <InstantValue {...(props as InstantProps)} />\n }\n if (zonedDateTimeFns.isValid(props.value)) {\n return <ZonedDateTimeValue {...(props as ZonedDateTimeProps)} />\n }\n if (yearMonthFns.isValid(props.value)) {\n return <YearMonthValue {...(props as YearMonthProps)} />\n }\n if (monthDayFns.isValid(props.value)) {\n return <MonthDayValue {...(props as MonthDayProps)} />\n }\n if (durationFns.isValid(props.value)) {\n return <DurationValue {...(props as DurationProps)} />\n }\n\n return <>{props.value}</>\n }\n\n if (typeof props.value === 'number') {\n return <NumberValue {...(props as NumberProps)} />\n }\n\n return <>{String(props.value)}</>\n}\n\nfunction NumberValue(props: NumberProps) {\n const formatter = React.useMemo(() => new Intl.NumberFormat('en-us', props.formatOptions ?? {}), [props.formatOptions])\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${formatter.format(props.value.start)} – ${formatter.format(props.value.end)}`\n } else if (typeof props.value === 'number') {\n return formatter.format(props.value)\n } else {\n return props.value ?? null\n }\n }, [props.value, formatter])\n return <>{formatted}</>\n}\n\nfunction ListValue(props: StringListProps) {\n const formatter = React.useMemo(\n () => new Intl.ListFormat('en-us', props.formatOptions ?? { style: 'short' }),\n [props.formatOptions]\n )\n const formatted = React.useMemo(() => {\n if (props.value && props.value.length) {\n return formatter.format(props.value)\n } else {\n return null\n }\n }, [props.value, formatter])\n return <>{formatted}</>\n}\n\nfunction DateValue(props: DateProps) {\n const format = props.formatOptions?.dateFormat ?? 'M/d/yyyy'\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${dateFns.format(props.value.start, format)} – ${dateFns.format(props.value.end, format)}`\n } else if (typeof props.value === 'string') {\n return dateFns.format(props.value, format)\n } else {\n return null\n }\n }, [props.value, format])\n return <>{formatted}</>\n}\n\nfunction TimeValue(props: TimeProps) {\n const format = props.formatOptions?.timeFormat ?? 'h:mm a'\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${timeFns.format(props.value.start, format)} – ${timeFns.format(props.value.end, format)}`\n } else if (typeof props.value === 'string') {\n return timeFns.format(props.value, format)\n } else {\n return null\n }\n }, [props.value, format])\n return <>{formatted}</>\n}\n\nfunction DateTimeValue(props: DateTimeProps) {\n const format = props.formatOptions?.dateTimeFormat ?? 'M/d/yyyy h:mm a'\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${dateTimeFns.format(props.value.start, format)} – ${dateTimeFns.format(props.value.end, format)}`\n } else if (typeof props.value === 'string') {\n return dateTimeFns.format(props.value, format)\n } else {\n return null\n }\n }, [props.value, format])\n return <>{formatted}</>\n}\n\nfunction InstantValue(props: InstantProps) {\n const timeZone = props.formatOptions?.timeZone\n const format = props.formatOptions?.dateTimeFormat ?? (timeZone ? 'M/d/yyyy h:mm a z' : 'M/d/yyyy h:mm a')\n\n const formatted = React.useMemo(() => {\n const formatInstant = (instant: Iso.Instant) => {\n if (timeZone) {\n const zonedDateTime = instantFns.toZonedDateTime(instant, timeZone)\n return zonedDateTimeFns.format(zonedDateTime, format)\n }\n return instantFns.chain(instant).toZonedDateTime('UTC').format(format).value()\n }\n\n if (props.value && typeof props.value === 'object') {\n return `${formatInstant(props.value.start)} – ${formatInstant(props.value.end)}`\n } else if (typeof props.value === 'string') {\n return formatInstant(props.value)\n } else {\n return null\n }\n }, [props.value, format, timeZone])\n return <>{formatted}</>\n}\n\nfunction ZonedDateTimeValue(props: ZonedDateTimeProps) {\n const format = props.formatOptions?.dateTimeFormat ?? 'M/d/yyyy h:mm a z'\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${zonedDateTimeFns.format(props.value.start, format)} – ${zonedDateTimeFns.format(props.value.end, format)}`\n } else if (typeof props.value === 'string') {\n return zonedDateTimeFns.format(props.value, format)\n } else {\n return null\n }\n }, [props.value, format])\n return <>{formatted}</>\n}\n\nfunction YearMonthValue(props: YearMonthProps) {\n const format = props.formatOptions?.dateFormat ?? 'MMMM yyyy'\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${yearMonthFns.format(props.value.start, format)} – ${yearMonthFns.format(props.value.end, format)}`\n } else if (typeof props.value === 'string') {\n return yearMonthFns.format(props.value, format)\n } else {\n return null\n }\n }, [props.value, format])\n return <>{formatted}</>\n}\n\nfunction MonthDayValue(props: MonthDayProps) {\n const format = props.formatOptions?.dateFormat ?? 'MMMM d'\n const formatted = React.useMemo(() => {\n if (props.value && typeof props.value === 'object') {\n return `${monthDayFns.format(props.value.start, format)} – ${monthDayFns.format(props.value.end, format)}`\n } else if (typeof props.value === 'string') {\n return monthDayFns.format(props.value, format)\n } else {\n return null\n }\n }, [props.value, format])\n return <>{formatted}</>\n}\n\nfunction DurationValue(props: DurationProps) {\n const durationFormat = props.formatOptions?.style ?? 'short'\n\n const formatted = React.useMemo(() => {\n const formatDuration = (duration: Iso.Duration) => {\n const fields = durationFns.getFields(duration)\n\n const partFormatters: Record<string, Record<string, string>> = {\n years: {\n long: Math.abs(fields.years) > 1 ? `${fields.years} years` : `${fields.years} year`,\n short: `${fields.years}y`\n },\n months: {\n long: Math.abs(fields.months) > 1 ? `${fields.months} months` : `${fields.months} month`,\n short: `${fields.months}M`\n },\n weeks: {\n long: Math.abs(fields.weeks) > 1 ? `${fields.weeks} weeks` : `${fields.weeks} week`,\n short: `${fields.weeks}w`\n },\n days: {\n long: Math.abs(fields.days) > 1 ? `${fields.days} days` : `${fields.days} day`,\n short: `${fields.days}d`\n },\n hours: {\n long: Math.abs(fields.hours) > 1 ? `${fields.hours} hours` : `${fields.hours} hour`,\n short: `${fields.hours}h`\n },\n minutes: {\n long: Math.abs(fields.minutes) > 1 ? `${fields.minutes} minutes` : `${fields.minutes} minute`,\n short: `${fields.minutes}m`\n },\n seconds: {\n long: Math.abs(fields.seconds) > 1 ? `${fields.seconds} seconds` : `${fields.seconds} second`,\n short: `${fields.seconds}s`\n },\n milliseconds: {\n long:\n Math.abs(fields.milliseconds) > 1 ? `${fields.milliseconds} milliseconds` : `${fields.milliseconds} millisecond`,\n short: `${fields.milliseconds}ms`\n }\n }\n\n const parts = Object.entries(partFormatters)\n .map(([key, value]) => (fields[key as keyof typeof fields] !== 0 ? value[durationFormat] : null))\n .filter(Boolean) as string[]\n if (!parts.length) {\n return durationFormat === 'long' ? '0 seconds' : '0s'\n } else {\n return parts.join(' ')\n }\n }\n\n if (props.value && typeof props.value === 'object') {\n return `${formatDuration(props.value.start)} – ${formatDuration(props.value.end)}`\n } else if (typeof props.value === 'string') {\n return formatDuration(props.value)\n } else {\n return null\n }\n }, [props.value, durationFormat])\n return <>{formatted}</>\n}\n\nexport function LabeledValueLabel(props: React.ComponentProps<'label'>) {\n return (\n <label\n {...props}\n className={tw('block uppercase text-xs tracking-wider font-medium text-neutral-500', props.className)}\n />\n )\n}\n"}]},{"name":"inline-alert","dependencies":["tw","icon"],"files":[{"name":"components/inline-alert.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add inline-alert --directory app\n\nimport * as React from 'react'\nimport { tw } from '../utils/tw'\nimport { Icon } from './icon'\n\nexport type InlineAlertProps = {\n children?: React.ReactNode\n variant: 'info' | 'positive' | 'notice' | 'negative' | 'neutral'\n icon?: React.ReactNode\n className?: string\n title?: React.ReactNode\n description?: React.ReactNode\n actions?: React.ReactNode\n}\n\nconst variantClasses = {\n info: 'bg-info-50 text-info-800 border-info-200',\n positive: 'bg-positive-50 text-positive-800 border-positive-200',\n notice: 'bg-notice-50 text-notice-800 border-notice-200',\n negative: 'bg-negative-50 text-negative-800 border-negative-200',\n neutral: 'bg-neutral-50 text-neutral-700 border-neutral-200'\n}\n\nconst defaultIconNames = {\n info: 'information-circle',\n positive: 'check',\n notice: 'exclamation-triangle',\n negative: 'x-mark',\n neutral: 'information-circle'\n} as const\n\nexport function InlineAlert({ children, actions, variant, icon, className, title, description }: InlineAlertProps) {\n const displayIcon = icon !== undefined ? icon : <Icon name={defaultIconNames[variant]} className=\"h-5 w-5\" />\n\n return (\n <div\n className={tw(\n 'rounded-lg border px-4 py-3 flex gap-3 items-start justify-between text-left',\n variantClasses[variant],\n className\n )}\n role=\"alert\"\n >\n <div className=\"flex gap-3 items-start\">\n {displayIcon && <div className=\"flex-shrink-0\">{displayIcon}</div>}\n <div className=\"flex-1 gap-1\">\n {title && <div className={tw('font-semibold text-sm')}>{title}</div>}\n {description && <div className=\"text-sm\">{description}</div>}\n {children && <div className=\"text-sm\">{children}</div>}\n </div>\n </div>\n {actions}\n </div>\n )\n}\n"}]},{"name":"pdf","dependencies":["tw","pdf-dist","icon","use-pagination"],"files":[{"name":"components/pdf.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add pdf --directory app\n\nimport React from 'react'\nimport { usePdf } from 'react-pdf-hook'\nimport { tw } from '../utils/tw'\nimport { usePagination } from '../utils/use-pagination'\nimport { Icon } from './icon'\nimport pdfjs from './helpers/pdf-dist.client'\n\nexport interface PdfProps extends React.ComponentProps<'div'> {\n src: string\n onDownload?: () => void\n}\n\nexport function Pdf({ src, onDownload, className, ...props }: PdfProps) {\n const containerRef = React.useRef<HTMLDivElement>(null)\n const [pageNumber, setPageNumber] = React.useState(0)\n\n const { pdf, canvasRef } = usePdf(pdfjs, src, pageNumber + 1, { width: containerRef.current?.clientWidth })\n\n const numPages = pdf?.numPages\n\n return (\n <div ref={containerRef} className={tw('relative overflow-hidden border border-neutral-300', className)} {...props}>\n <canvas\n ref={canvasRef}\n style={{ display: !pdf ? 'none' : undefined, maxWidth: '100%' }}\n className={tw('pdf-page', onDownload && 'cursor-pointer')}\n onClick={onDownload}\n />\n {!pdf || !containerRef.current?.clientWidth ? <div className=\"w-full h-full m-auto rounded\" /> : null}\n {numPages && numPages > 1 ? (\n <div className=\"flex absolute bottom-5 w-full justify-center\">\n <Pagination page={pageNumber} setPageNumber={setPageNumber} count={numPages} />\n </div>\n ) : null}\n </div>\n )\n}\n\nPdf.displayName = 'Pdf'\n\nfunction Pagination({ page, count, setPageNumber }: { page: number; count: number; setPageNumber: (n: number) => void }) {\n const pages = usePagination({ currentPage: page, pageCount: count })\n\n return (\n <div className=\"inline-flex items-center gap-1 pointer-events-auto\">\n <button\n type=\"button\"\n onClick={() => setPageNumber(page > 0 ? page - 1 : 0)}\n disabled={page === 0}\n className=\"p-1 rounded bg-white border border-neutral-300 hover:bg-neutral-50 disabled:opacity-50 disabled:cursor-not-allowed\"\n aria-label=\"Previous page\"\n >\n <Icon name=\"chevron-left\" className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n\n {pages.map((p, i) => (\n <button\n type=\"button\"\n key={p.readonly ? `ellipsis-${i}` : p.label}\n onClick={() => setPageNumber(p.readonly ? page : (p.value as number))}\n disabled={p.readonly}\n className={tw(\n 'min-w-[28px] bg-white h-7 px-2 text-xs font-medium rounded border transition-colors',\n p.selected\n ? 'bg-primary-500 text-white border-primary-500'\n : p.readonly\n ? 'border-transparent cursor-default'\n : 'border-neutral-300 hover:bg-neutral-50'\n )}\n >\n {p.label}\n </button>\n ))}\n\n <button\n type=\"button\"\n onClick={() => setPageNumber(page < count - 1 ? page + 1 : page)}\n disabled={page === count - 1}\n className=\"p-1 rounded bg-white border border-neutral-300 hover:bg-neutral-50 disabled:opacity-50 disabled:cursor-not-allowed\"\n aria-label=\"Next page\"\n >\n <Icon name=\"chevron-right\" className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n )\n}\n"}]},{"name":"container","dependencies":["tw"],"files":[{"name":"components/container.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add container --directory app\n\nimport * as React from 'react'\nimport { tw } from '../utils/tw'\n\n// ── Props ────────────────────────────────────────────────────────────────────\n\nexport type ContainerSize = 'lg' | 'md' | 'sm'\nexport type ContainerElevation = 0 | -1 | -2 | 'inherit'\n\nexport type ContainerProps = React.ComponentProps<'div'> & {\n size?: ContainerSize\n elevation?: ContainerElevation\n}\n\nexport type ContainerSectionProps = ContainerProps & {\n label?: React.ReactNode\n actions?: React.ReactNode\n}\n\nexport type SectionProps = {\n label?: React.ReactNode\n actions?: React.ReactNode\n children?: React.ReactNode\n}\n\n// ── Container ────────────────────────────────────────────────────────────────\n\nexport function Container({ size = 'lg', elevation = 0, className, ...props }: ContainerProps) {\n return <div {...props} className={tw(elevationClasses[String(elevation)], sizeClasses[size], className)} />\n}\n\nContainer.displayName = 'Container'\n\n// ── ContainerSection ─────────────────────────────────────────────────────────\n\nexport function ContainerSection({ label, actions, ...props }: ContainerSectionProps) {\n return (\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex items-start justify-between\">\n <h3 className=\"text-lg font-semibold\">{label}</h3>\n {actions}\n </div>\n <Container {...props} />\n </div>\n )\n}\n\nContainerSection.displayName = 'ContainerSection'\n\n// ── Section ──────────────────────────────────────────────────────────────────\n\nexport function Section({ label, actions, children }: SectionProps) {\n return (\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex items-start justify-between\">\n <h3 className=\"text-lg font-semibold\">{label}</h3>\n {actions}\n </div>\n <div>{children}</div>\n </div>\n )\n}\n\nSection.displayName = 'Section'\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nconst sizeClasses: Record<ContainerSize, string> = {\n lg: 'p-6 rounded-lg',\n md: 'p-3 rounded',\n sm: 'p-2 rounded-sm'\n}\n\nconst elevationClasses: Record<string, string> = {\n '0': 'bg-white ring-1 ring-neutral-950/5',\n '-1': 'bg-neutral-50 ring-1 ring-neutral-950/5',\n '-2': 'bg-neutral-100 ring-1 ring-neutral-950/10',\n inherit: ''\n}\n\nexport function getSizeClassesForContainer(size: ContainerSize) {\n return sizeClasses[size]\n}\n"}]},{"name":"disclosure","dependencies":["tw","icon","container"],"files":[{"name":"components/disclosure.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add disclosure --directory app\n\nimport * as React from 'react'\nimport {\n Disclosure as AriaDisclosure,\n DisclosurePanel as AriaDisclosurePanel,\n DisclosureGroup,\n DisclosureStateContext,\n Button,\n Heading\n} from 'react-aria-components'\nimport { tw } from '../utils/tw'\nimport { type ContainerSize } from './container'\nimport { Icon } from './icon'\n\nexport type DisclosureProps = {\n id?: string | number\n title: React.ReactNode\n children: React.ReactNode\n className?: string\n size?: ContainerSize\n isDisabled?: boolean\n defaultOpen?: boolean\n isOpen?: boolean\n onOpenChange?: (isOpen: boolean) => void\n}\n\nfunction DisclosureRaw({\n id,\n title,\n className,\n defaultOpen,\n isOpen,\n onOpenChange,\n children,\n size = 'md',\n isDisabled = false\n}: DisclosureProps) {\n const paddingX = size === 'lg' ? 'px-6' : size === 'md' ? 'px-3' : 'px-2'\n const paddingBottom = size === 'lg' ? 'pb-6' : size === 'md' ? 'pb-3' : 'pb-2'\n\n return (\n <AriaDisclosure\n id={id}\n defaultExpanded={defaultOpen}\n isExpanded={isOpen}\n onExpandedChange={onOpenChange}\n isDisabled={isDisabled}\n >\n <DisclosureHeader size={size} isDisabled={isDisabled} className={className}>\n {title}\n </DisclosureHeader>\n <AriaDisclosurePanel className=\"h-(--disclosure-panel-height) motion-safe:transition-[height] duration-200 ease-in-out overflow-clip mt-1\">\n <div className={tw(paddingX, paddingBottom)}>{children}</div>\n </AriaDisclosurePanel>\n </AriaDisclosure>\n )\n}\n\nfunction DisclosureHeader({\n size,\n isDisabled,\n className,\n children\n}: {\n size: ContainerSize\n isDisabled: boolean\n className?: string\n children: React.ReactNode\n}) {\n const { isExpanded } = React.useContext(DisclosureStateContext)!\n\n return (\n <Heading>\n <Button\n slot=\"trigger\"\n className={tw(\n 'flex w-full items-center justify-start text-left transition-colors gap-2 font-medium',\n 'hover:bg-neutral-50',\n size === 'lg' ? 'px-4 py-2.5' : size === 'md' ? 'px-2.5 py-1.5' : 'px-2 py-1',\n 'rounded-lg',\n isDisabled && 'opacity-50 cursor-not-allowed hover:bg-transparent',\n className\n )}\n >\n <Icon\n name=\"chevron-right\"\n className={tw(\n 'shrink-0 transition-transform duration-200 ease-in-out',\n size === 'md' || size === 'lg' ? 'size-4' : 'size-3',\n isExpanded && 'rotate-90',\n isDisabled ? 'text-neutral-300' : 'text-neutral-500'\n )}\n />\n <span\n className={tw(\n size === 'lg'\n ? 'text-lg font-semibold'\n : size === 'md'\n ? 'text-base font-medium'\n : 'text-sm font-medium',\n isDisabled ? 'text-neutral-400' : 'text-neutral-700'\n )}\n >\n {children}\n </span>\n </Button>\n </Heading>\n )\n}\n\nexport const Disclosure = Object.assign(DisclosureRaw, {\n Group: DisclosureGroup\n})\n\nObject.defineProperty(DisclosureRaw, 'name', { value: 'Disclosure' })\n"}]},{"name":"use-pagination","files":[{"name":"utils/use-pagination.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add use-pagination --directory app\n\nimport * as React from 'react'\n\nexport interface PaginationPage {\n label: string\n value: number | null\n readonly: boolean\n selected: boolean\n}\n\nexport function usePagination({ currentPage, pageCount }: { currentPage: number; pageCount: number }): PaginationPage[] {\n return React.useMemo(() => {\n function getPage(i: number): PaginationPage {\n return { label: `${i + 1}`, value: i, readonly: false, selected: currentPage === i }\n }\n\n const first: PaginationPage = { label: '1', value: 0, readonly: false, selected: currentPage === 0 }\n const ellipsis: PaginationPage = { label: '...', value: null, readonly: true, selected: false }\n const last: PaginationPage = {\n label: `${pageCount}`,\n value: pageCount - 1,\n readonly: false,\n selected: currentPage === pageCount - 1\n }\n\n if (pageCount < 8) {\n return Array.from({ length: pageCount }, (_, i) => getPage(i))\n } else if (currentPage < 5) {\n const leading = Array.from({ length: 5 }, (_, i) => getPage(i))\n return [...leading, ellipsis, last]\n } else if (currentPage > pageCount - 5) {\n const trailing = Array.from({ length: 5 }, (_, i) => getPage(i + pageCount - 4))\n return [first, ellipsis, ...trailing.filter((p) => p.value! <= pageCount - 1)]\n } else {\n const siblings = Array.from({ length: 3 }, (_, i) => getPage(i + currentPage - 1))\n return [first, ellipsis, ...siblings, ellipsis, last]\n }\n }, [pageCount, currentPage])\n}\n"}]},{"name":"form","files":[{"name":"components/form.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add form --directory app\n\nimport { forwardRef } from 'react'\nimport {\n type FormProps as ReactRouterFormProps,\n Form as ReactRouterForm,\n data,\n type ClientActionFunctionArgs\n} from 'react-router'\nimport { HeadlessForm, type StandardSchema, type ParseFormDataOptions, type ParseFormDataResult } from '@maestro-js/form'\nimport { serverOnly$ } from 'vite-env-only/macros'\n\nconst _Form = forwardRef<HTMLFormElement, ReactRouterFormProps>(function Form({ children, ...props }, ref) {\n return (\n <HeadlessForm as={ReactRouterForm} ref={ref} {...props}>\n {children}\n </HeadlessForm>\n )\n})\n\nexport const Form = Object.assign(_Form, {\n HiddenInput: HeadlessForm.HiddenInput,\n HiddenFileInput: HeadlessForm.HiddenFileInput,\n HiddenSelect: HeadlessForm.HiddenSelect,\n useControlledState: HeadlessForm.useControlledState,\n useTopLevelFormValidation: HeadlessForm.useTopLevelFormValidation,\n useReset: HeadlessForm.useReset,\n clientSafeParse: HeadlessForm.parseFormDataClientSide,\n serverSafeParse<T>(\n schema: StandardSchema<T, T>,\n formData: FormData | Promise<FormData>,\n options?: {\n handleFile: ParseFormDataOptions<T>['handleFile']\n }\n ): Promise<ParseFormDataResult<T>> {\n return HeadlessForm.parseFormData(schema, formData, {\n ...options,\n jsonSchemaLibraryOptions: {\n unrepresentable: 'any',\n override: (ctx) => {\n const meta = ctx.zodSchema.meta()\n const custom = meta?.jsonSchema\n\n if (custom && typeof custom === 'object') {\n Object.assign(ctx.jsonSchema, custom)\n }\n }\n }\n })\n },\n validationError: serverOnly$(validationError) as typeof validationError,\n customValidationError: serverOnly$(customValidationError) as typeof customValidationError,\n composeValidators: HeadlessForm.composeValidators,\n FormError\n})\n\nasync function validationError(\n {\n issues,\n formData,\n formId\n }: {\n issues: { path?: ((string | number | symbol) | { key: string | number | symbol })[] | undefined; message: string }[]\n formData: FormData\n formId?: string | null\n },\n init: RequestInit = {}\n) {\n console.error(issues)\n return data(\n {\n success: false as const,\n formSubmissionError: {\n issues: issues.map((i) => ({\n path: i.path?.map((p) => (p && typeof p === 'object' && 'key' in p ? p.key : p))?.join('.') ?? '',\n message: i.message\n })),\n formId: formId ?? formData.get('__formId')\n }\n },\n { status: 422, ...init }\n )\n}\n\nasync function customValidationError(\n { message, formData }: { message: string; formData: FormData },\n init?: number | ResponseInit\n) {\n console.error(message)\n return data(\n {\n success: false as const,\n formSubmissionError: {\n issues: [{ path: '', message: message }],\n formId: formData.get('__formId')\n }\n },\n { status: typeof init === 'number' ? init : 422, ...(typeof init === 'object' ? init : {}) }\n )\n}\n\nfunction FormError({ formId, children }: { formId?: string; children(message: string): React.ReactNode }) {\n const serverErrors = HeadlessForm.useServerErrors(formId)\n const formErrors = serverErrors.filter((s) => !s.path)\n return formErrors.length ? <>{children(formErrors[0]!.message)}</> : null\n}\n"}]},{"name":"toast","dependencies":["tw","headless-button","compose-refs","use-render-props","icon"],"files":[{"name":"components/toast.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add toast --directory app\n\nimport * as React from 'react'\nimport { flushSync } from 'react-dom'\nimport {\n Text,\n UNSTABLE_Toast as AriaToast,\n UNSTABLE_ToastContent as ToastContent,\n UNSTABLE_ToastQueue as ToastQueue,\n UNSTABLE_ToastRegion as AriaToastRegion\n} from 'react-aria-components'\nimport { tw } from '../utils/tw'\nimport { Icon } from './icon'\nimport { HeadlessButton } from './helpers/headless-button'\n\nexport interface Toast {\n title: string\n description?: string\n variant?: 'info' | 'positive' | 'warning' | 'negative' | 'notice'\n}\n\nconst queue = new ToastQueue<Toast>({\n wrapUpdate(fn) {\n if ('startViewTransition' in document) {\n document.startViewTransition(() => {\n flushSync(fn)\n })\n } else {\n fn()\n }\n }\n})\n\nexport function showToast(\n toast: Toast,\n options?: {\n /** Handler that is called when the toast is closed, either by the user or after a timeout. */\n onClose?: () => void\n /** A timeout to automatically close the toast after, in milliseconds. */\n timeout?: number | null\n }\n) {\n return queue.add(toast, {\n ...options,\n timeout: typeof options?.timeout === 'undefined' ? 5_000 : (options?.timeout ?? undefined)\n })\n}\n\nexport function ToastRegion({ toast }: { toast?: Toast | null }) {\n return (\n <>\n <style>\n {`\n::view-transition-new(.toast):only-child {\n animation: maestro-toast-slide-in 400ms;\n}\n\n::view-transition-old(.toast):only-child {\n animation: maestro-toast-slide-out 400ms;\n animation-fill-mode: forwards;\n}\n\n@keyframes maestro-toast-slide-out {\n to {\n translate: 0 100%;\n opacity: 0;\n visibility: hidden;\n }\n}\n\n@keyframes maestro-toast-slide-in {\n from {\n translate: 0 100%;\n opacity: 0;\n }\n}\n `}\n </style>\n <AriaToastRegion\n className=\"fixed right-4 bottom-4 sm:right-6 sm:bottom-6 z-[6000] flex flex-col gap-2 pointer-events-none outline-none\"\n queue={queue}\n >\n {({ toast }) => <ToastItem toast={toast} />}\n </AriaToastRegion>\n {toast ? <DeclaredToast toast={toast} /> : null}\n </>\n )\n}\n\nToastRegion.displayName = 'ToastRegion'\n\n// --- Internal Components ---\n\nfunction ToastItem({ toast }: { toast: { content: Toast; key: string } }) {\n const variant = toast.content.variant || 'info'\n const hasDescription = !!toast.content.description\n\n return (\n <AriaToast\n toast={toast}\n style={{ viewTransitionName: toast.key } as React.CSSProperties}\n className={tw(\n 'pointer-events-auto',\n 'flex gap-3 rounded-lg px-4 py-3 shadow-lg',\n 'min-w-[300px] max-w-md',\n '[view-transition-class:toast]',\n hasDescription ? 'items-start' : 'items-center',\n variantClasses[variant]\n )}\n >\n <Icon name={variantIconNames[variant]} className={tw('h-5 w-5 shrink-0', hasDescription && 'mt-0.5')} />\n <ToastContent className=\"flex flex-1 flex-col min-w-0\">\n <Text slot=\"title\" className=\"font-semibold text-sm\">\n {toast.content.title}\n </Text>\n {toast.content.description && (\n <Text slot=\"description\" className=\"mt-1 text-sm\">\n {toast.content.description}\n </Text>\n )}\n </ToastContent>\n <HeadlessButton\n onClick={() => queue.close(toast.key)}\n className={tw(\n 'flex-shrink-0 rounded p-1 transition-colors',\n 'hover:bg-black/5 focus:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',\n variantCloseOutlineClasses[variant]\n )}\n >\n <Icon name=\"x-mark\" className=\"h-5 w-5\" />\n </HeadlessButton>\n </AriaToast>\n )\n}\n\nfunction DeclaredToast({\n toast,\n options\n}: {\n toast: Toast\n options?: {\n onClose?: () => void\n timeout?: number | null\n }\n}) {\n React.useEffect(() => {\n setTimeout(() => {\n showToast(toast, options)\n }, 0)\n }, [toast, options])\n return null\n}\n\n// --- Variant Maps ---\n\nconst variantClasses = {\n info: 'bg-info-50 text-info-800 ring-1 ring-info-200',\n positive: 'bg-positive-50 text-positive-800 ring-1 ring-positive-200',\n warning: 'bg-notice-50 text-notice-800 ring-1 ring-notice-200',\n negative: 'bg-negative-50 text-negative-800 ring-1 ring-negative-200',\n notice: 'bg-neutral-50 text-neutral-700 ring-1 ring-neutral-200'\n}\n\nconst variantIconNames = {\n info: 'information-circle',\n positive: 'check',\n warning: 'exclamation-triangle',\n negative: 'x-mark',\n notice: 'information-circle'\n} as const\n\nconst variantCloseOutlineClasses = {\n info: 'focus-visible:outline-info-500',\n positive: 'focus-visible:outline-positive-600',\n warning: 'focus-visible:outline-notice-600',\n negative: 'focus-visible:outline-negative-600',\n notice: 'focus-visible:outline-neutral-400'\n}\n"}]},{"name":"table","files":[{"name":"components/table/index.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { createServerTable } from './server-table'\nimport { TableContainer } from './table-container'\nimport { TableSelectFilter } from './table-filters/select-filter'\nimport { TableSelectSort } from './table-filters/sort'\nimport {\n TableBody,\n TableCell,\n TableDescription,\n TableFooter,\n TableMain,\n TableRow,\n TableTitle,\n TableTitleLink\n} from './table-primitives'\nimport { TableActions, TableHeader, TableSearch } from './table-toolbar'\nimport { TableActionButton, TableMenu, TableSelectionActions, TableSelectionAction } from './table-actions'\nimport { useClientTable } from './use-client-table'\nimport { useServerTable } from './use-server-table'\n\nexport const Table = Object.assign(TableContainer, {\n useClientTable,\n useServerTable,\n createServerTable,\n Main: TableMain,\n Search: TableSearch,\n Body: TableBody,\n Row: TableRow,\n Cell: TableCell,\n Header: TableHeader,\n Footer: TableFooter,\n Title: TableTitle,\n TitleLink: TableTitleLink,\n Description: TableDescription,\n SelectFilter: TableSelectFilter,\n SelectSort: TableSelectSort,\n ActionButton: TableActionButton,\n Menu: TableMenu,\n SelectionActions: TableSelectionActions,\n SelectionAction: TableSelectionAction,\n Actions: TableActions\n})\n"},{"name":"components/table/table-types.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport type { ReactNode } from 'react'\nimport type { Iso } from 'iso-fns'\nimport type { DateRangeRootAdditionalProps } from '../date-range'\n\ntype PaginationState = { limit: number; skip: number }\ntype SetterOrUpdater<T> = T | ((prev: T) => T)\n\nexport type StandardTable<\n FC extends Record<string, any> = Record<string, any>,\n SC extends Record<string, any> = Record<string, any>\n> = {\n getRows(): Record<string, any>[]\n getRowCount(): number\n getFilterConfig(): FC\n getSortConfig(): { [K in keyof SC]: null }\n state: {\n filters: { [K in keyof FC]: FC[K] | null | undefined }\n sort: (keyof SC & string) | null\n search: string\n pagination: PaginationState\n rowSelection?: Set<string>\n }\n setFilters(val: SetterOrUpdater<{ [K in keyof FC]: FC[K] | null | undefined }>): void\n setSort(val: any): void\n setSearch(val: SetterOrUpdater<string>): void\n setPagination(val: SetterOrUpdater<PaginationState>): void\n toggleAllRowSelection?(): void\n toggleRowSelection?(row: Record<string, any>): void\n isRowSelected?(row: Record<string, any>): boolean\n clearRowSelection?(): void\n}\n\ntype DefaultTableFilter<V = any> = {\n label: string\n type?: never\n options: { label: string; value: V; className?: string; icon?: ReactNode }[]\n autoComplete?: boolean\n}\n\ntype DateRangeTableFilter = {\n label: string\n type: 'date-range'\n options?: Pick<DateRangeRootAdditionalProps, 'shouldDisableDate' | 'shouldDisableEndDate' | 'shouldDisableStartDate'> & {\n quickPresets?: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]\n }\n}\n\nexport type TableFilterLocalization<V = any> = 0 extends 1 & V\n ? DefaultTableFilter | DateRangeTableFilter\n : [V] extends [{ startDate: Iso.Date; endDate: Iso.Date }]\n ? DateRangeTableFilter\n : DefaultTableFilter<V>\n\nexport type TableLocalization<\n FC extends Record<string, any> = Record<string, any>,\n SC extends Record<string, any> = Record<string, any>\n> = {\n filters: { [K in keyof FC]: TableFilterLocalization<FC[K]> }\n sorts: { [key in keyof SC]: { label: string } }\n searchPlaceholder?: string\n}\n"},{"name":"components/table/table-primitives.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { NavLink, type NavLinkProps } from 'react-router'\nimport { twMerge } from 'tailwind-merge'\nimport { Icon } from '../icon'\nimport React from 'react'\nimport { PlainCheckbox } from '../checkbox'\nimport type { StandardTable } from './table-types'\n\n// --- Layout ---\n\nexport function TableMain({\n className,\n ...props\n}: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {\n return <div className={twMerge('max-w-full h-auto', className)} {...props} />\n}\n\nexport function TableBody({\n className,\n ...props\n}: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {\n return (\n <div className=\"w-full\">\n <div\n className={twMerge(\n 'flex flex-col flex-nowrap w-full',\n '[&>*+*>*]:border-t [&>*+*>*]:border-gray-950/5' /** select table cells for all but the first row */,\n className\n )}\n {...props}\n />\n </div>\n )\n}\n\nexport function TableCell({\n className,\n ...props\n}: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {\n return <div className={twMerge('basis-0 grow min-w-0 py-3.5 px-2', className)} {...props} />\n}\n\n// --- Row ---\n\nconst TableRowContext = React.createContext<{ table: StandardTable; multiSelect?: boolean }>(null as any)\nexport const TableRowContextProvider = TableRowContext.Provider\n\ninterface TableRowProps {\n row: Record<string, any>\n children?: React.ReactNode\n className?: string\n hideMobileSpacer?: boolean\n ['aria-label']?: string\n}\n\nexport function TableRow({ row, children, className, hideMobileSpacer, 'aria-label': ariaLabel }: TableRowProps) {\n const { table, multiSelect } = React.useContext(TableRowContext)\n const selected = table.isRowSelected?.(row) ?? false\n\n return (\n <div\n className={twMerge('flex flex-row w-full h-auto', selected && 'bg-gray-50', className)}\n onClick={(e) => {\n const isTouchableDevice = navigator.maxTouchPoints > 0\n const isSmallScreen = !window.matchMedia('(min-width: 640px)').matches\n\n const target = e.target as HTMLElement\n const isInteractiveElement = target.closest('input, label, button, a')\n\n if ((isTouchableDevice || isSmallScreen) && !isInteractiveElement) {\n const title = e.currentTarget.querySelector('[data-row-title*=\"true\"]')\n const titleLink = title instanceof HTMLAnchorElement ? title : title?.querySelector('a')\n titleLink?.click()\n }\n }}\n role=\"row\"\n aria-label={ariaLabel}\n >\n {multiSelect ? (\n <div className=\"relative shrink-0 hidden md:flex items-start pt-3.5 pl-4 pr-2 *:scale-90\">\n {selected ? <div className=\"absolute inset-y-0 left-0 w-0.5 bg-primary-600\" /> : null}\n <PlainCheckbox checked={selected} onChange={() => table.toggleRowSelection?.(row)} />\n </div>\n ) : null}\n {!hideMobileSpacer ? <TableCell className={twMerge('relative grow-0 w-3', multiSelect && 'md:hidden')} /> : null}\n\n {children}\n </div>\n )\n}\n\n// --- Title / Description ---\n\nexport interface TableTitleProps extends React.DetailedHTMLProps<\n React.HTMLAttributes<HTMLParagraphElement>,\n HTMLParagraphElement\n> {}\n\nexport function TableTitle({ className, ...props }: TableTitleProps) {\n return <p {...props} data-row-title=\"true\" className={twMerge('truncate text-sm font-medium text-blue-600', className)} />\n}\n\nexport function TableTitleLink({ className, ...props }: NavLinkProps) {\n return (\n <NavLink\n {...props}\n data-row-title=\"true\"\n className={(b) =>\n twMerge('truncate text-sm font-medium text-blue-600', typeof className === 'function' ? className(b) : className)\n }\n />\n )\n}\n\nexport interface TableDescriptionProps extends React.DetailedHTMLProps<\n React.HTMLAttributes<HTMLDivElement>,\n HTMLDivElement\n> {}\n\nexport function TableDescription({ className, ...props }: TableDescriptionProps) {\n return <div {...props} className={twMerge('truncate text-sm text-gray-500', className)} />\n}\n\n// --- Footer ---\n\nexport interface TableFooterProps {\n table: StandardTable\n hideResultCount?: boolean\n}\n\nexport function TableFooter({ table, hideResultCount }: TableFooterProps) {\n const { limit, skip } = table.state.pagination\n const rowCount = table.getRowCount()\n\n const pageCount = Math.ceil(limit ? rowCount / limit : 1)\n const pageNumber = limit > 0 ? Math.floor(skip / limit) : 0\n\n const goToPage = (page: number) => {\n table.setPagination({ limit, skip: page * (limit || 25) })\n }\n\n const canGoPrevious = pageNumber > 0\n const canGoNext = pageNumber < pageCount - 1\n\n const showingFrom = skip + 1\n const showingTo = limit ? Math.min(skip + limit, rowCount) : rowCount\n const pages = getPaginationRange(pageCount, pageNumber)\n\n return (\n <nav className=\"flex items-center justify-between border-t border-gray-950/5 px-4 py-2\">\n {!hideResultCount ? (\n <p className=\"text-xs text-gray-400 tabular-nums\">\n {showingFrom}–{showingTo} of {rowCount}\n </p>\n ) : (\n <div />\n )}\n {pageCount > 1 && (\n <div className=\"flex items-center gap-0.5\">\n <button\n type=\"button\"\n onClick={() => canGoPrevious && goToPage(pageNumber - 1)}\n disabled={!canGoPrevious}\n className=\"inline-flex items-center justify-center rounded-md p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 disabled:opacity-30 disabled:hover:bg-transparent\"\n aria-label=\"Previous page\"\n >\n <Icon name=\"chevron-left\" className=\"size-3.5\" />\n </button>\n <div className=\"flex items-center gap-0.5 max-sm:hidden\">\n {pages.map((page, i) => {\n if (page === '...') {\n return (\n <span key={`ellipsis-${i}`} className=\"px-1 text-xs text-gray-400\">\n ...\n </span>\n )\n }\n const isSelected = page === pageNumber\n return (\n <button\n type=\"button\"\n key={page}\n onClick={() => goToPage(page)}\n className={twMerge(\n 'inline-flex items-center justify-center size-7 rounded-full text-xs tabular-nums',\n isSelected\n ? 'bg-gray-100 text-gray-900 font-medium'\n : 'text-gray-400 hover:bg-gray-50 hover:text-gray-600'\n )}\n >\n {page + 1}\n </button>\n )\n })}\n </div>\n <span className=\"text-xs tabular-nums text-gray-400 sm:hidden px-1\">\n {pageNumber + 1}/{pageCount}\n </span>\n <button\n type=\"button\"\n onClick={() => canGoNext && goToPage(pageNumber + 1)}\n disabled={!canGoNext}\n className=\"inline-flex items-center justify-center rounded-md p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 disabled:opacity-30 disabled:hover:bg-transparent\"\n aria-label=\"Next page\"\n >\n <Icon name=\"chevron-right\" className=\"size-3.5\" />\n </button>\n </div>\n )}\n </nav>\n )\n}\n\nfunction getPaginationRange(pageCount: number, currentPage: number): (number | '...')[] {\n const delta = 1\n const range: (number | '...')[] = []\n\n for (let i = 0; i < pageCount; i++) {\n if (i === 0 || i === pageCount - 1 || (i >= currentPage - delta && i <= currentPage + delta)) {\n range.push(i)\n } else if (range[range.length - 1] !== '...') {\n range.push('...')\n }\n }\n\n return range\n}\n"},{"name":"components/table/table-actions.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport React from 'react'\nimport { twMerge } from 'tailwind-merge'\nimport { Button, type ButtonProps } from '../button'\nimport { Menu, type MenuProps, type MenuItemProps, type MenuLinkItemProps } from '../menu'\nimport { Icon } from '../icon'\n\n// ── TableActionButton ────────────────────────────────────────────────────────\n// Primary action button for the table toolbar (e.g. \"Add person\").\n// Defaults to contained/primary/sm — pass variant/color/size to override.\n\nexport type TableActionButtonProps = ButtonProps\n\nexport function TableActionButton({ size = 'md', children, ...props }: TableActionButtonProps) {\n return (\n <Button size={size} {...props}>\n {children}\n </Button>\n )\n}\n\n// ── TableMenu ────────────────────────────────────────────────────────────────\n// Secondary actions dropdown for the table toolbar (e.g. \"More\").\n// Renders a neutral outlined trigger with a chevron, and a standard Menu popover.\n\nexport type TableMenuProps = {\n label?: string\n children?: React.ReactNode\n placement?: MenuProps['placement']\n}\n\nfunction TableMenuMain({ label = 'More', children, placement }: TableMenuProps) {\n return (\n <Menu\n Button={\n <Button variant=\"outlined\" color=\"neutral\" size=\"md\">\n {label}\n <Icon name=\"chevron-down\" className=\"size-4 text-neutral-400\" />\n </Button>\n }\n placement={placement}\n >\n {children}\n </Menu>\n )\n}\n\nexport const TableMenu = Object.assign(TableMenuMain, {\n Item: Menu.Item,\n LinkItem: Menu.LinkItem,\n Group: Menu.Group,\n Separator: Menu.Separator\n})\n\n// ── TableSelectionAction ────────────────────────────────────────────────────\n// Compact action button for the multi-select selection bar context.\n\nexport function TableSelectionActions({ children }: { children: React.ReactNode }) {\n return <div className=\"flex items-center gap-1\">{children}</div>\n}\n\nexport type TableSelectionActionProps = React.ComponentProps<'button'>\n\nexport function TableSelectionAction({ children, className, ...props }: TableSelectionActionProps) {\n return (\n <button\n type=\"button\"\n {...props}\n className={twMerge(\n 'inline-flex items-center rounded px-2 py-0.5 text-sm font-medium text-primary-700 hover:text-primary-900 hover:bg-primary-100 disabled:opacity-50 disabled:cursor-not-allowed',\n className\n )}\n >\n {children}\n </button>\n )\n}\n"},{"name":"components/table/table-container.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport {\n TableBody,\n TableCell,\n TableDescription,\n TableFooter,\n TableMain,\n TableRow,\n TableRowContextProvider,\n TableTitle,\n TableTitleLink\n} from './table-primitives'\nimport type { StandardTable } from './table-types'\nimport { TableHeader, type TableHeaderProps, TableSearch } from './table-toolbar'\nimport { TableDateRangeFilter } from './table-filters/date-range-filter'\nimport { TableSelectSort } from './table-filters/sort'\nimport { TableSelectFilter } from './table-filters/select-filter'\nimport type { TableFilterLocalization, TableLocalization } from './table-types'\nimport { twMerge } from 'tailwind-merge'\nimport React from 'react'\nimport type { Iso } from 'iso-fns'\n\nexport function TableContainer<TTable extends StandardTable>({\n table,\n localization,\n children,\n actions,\n noRowElement = <StandardNoRows />,\n multiSelect,\n multiSelectActions,\n hideActionsOnMobile = true,\n stickyHeader = true,\n hideSort,\n hideSearch,\n tableBodyClassName,\n headerAction,\n hideResultCount,\n totalRows: totalRowsProp,\n ...props\n}: React.ComponentProps<'div'> & {\n table: TTable\n localization: TableLocalization<ReturnType<TTable['getFilterConfig']>, ReturnType<TTable['getSortConfig']>>\n actions?: React.ReactNode\n noRowElement?: React.ReactNode\n multiSelect?: boolean\n hideActionsOnMobile?: boolean\n hideSort?: boolean\n hideSearch?: boolean\n stickyHeader?: boolean\n multiSelectActions?: React.ReactNode\n tableBodyClassName?: string\n headerAction?: TableHeaderProps['action']\n hideResultCount?: boolean\n totalRows?: number\n}) {\n const rows = table.getRows()\n const filterConfig = table.getFilterConfig()\n const sortConfig = table.getSortConfig()\n const filterKeys = Object.keys(filterConfig).filter((k) => k in localization.filters)\n const sortKeys = Object.keys(sortConfig).filter((k) => k !== 'mostRelevant' && k in localization.sorts)\n\n const hasFilters = React.useMemo(() => {\n const filters = table.state.filters ?? {}\n const hasActiveFilters = Object.entries(filters).some(([, value]) => value != null)\n const hasSort = table.state.sort != null\n const hasSearch = (table.state.search ?? '') !== ''\n return hasActiveFilters || hasSort || hasSearch\n }, [table.state.filters, table.state.sort, table.state.search])\n\n return (\n <div {...props}>\n {/* Search + actions outside the card */}\n <TableSearch\n table={table}\n placeholder={localization.searchPlaceholder}\n actions={actions}\n hasFilters={hasFilters}\n hideSearch={hideSearch}\n hideActionsOnMobile={hideActionsOnMobile}\n />\n\n {/* Bordered card container */}\n <div className=\"rounded-lg border border-gray-950/10 overflow-hidden\">\n <TableMain className={twMerge(stickyHeader && 'relative')}>\n <TableHeader\n table={table}\n multiSelect={multiSelect}\n multiSelectActions={multiSelectActions}\n sticky={stickyHeader}\n hide={!!(hideSort && filterKeys.length === 0 && !multiSelect)}\n action={headerAction}\n >\n <div className={twMerge('flex flex-nowrap items-center gap-x-1', hideSort && 'mr-1')}>\n {filterKeys.map((f) => {\n const filter = localization.filters[f] as TableFilterLocalization\n if (filter.type === 'date-range') {\n return (\n <TableDateRangeFilter\n table={table}\n key={f}\n name={f}\n label={filter.label}\n options={filter.options}\n value={table.state.filters[f] as { startDate: Iso.Date; endDate: Iso.Date }}\n />\n )\n } else {\n return (\n <TableSelectFilter\n table={table}\n label={filter.label}\n name={f}\n options={filter.options}\n autoComplete={filter.autoComplete}\n value={table.state.filters[f]}\n key={f}\n />\n )\n }\n })}\n </div>\n <div className={twMerge('flex flex-nowrap items-center ml-1 mr-1', hideSort && 'hidden')}>\n <TableSelectSort\n table={table}\n value={table.state.sort}\n options={sortKeys.map((s) => ({ value: s, label: localization.sorts[s]?.label ?? s }))}\n search={table.state.search}\n />\n </div>\n </TableHeader>\n <TableRowContextProvider value={{ table, multiSelect }}>\n {rows.length ? <TableBody className={tableBodyClassName}>{children}</TableBody> : noRowElement}\n </TableRowContextProvider>\n {rows.length ? <TableFooter table={table} hideResultCount={hideResultCount} /> : null}\n </TableMain>\n </div>\n </div>\n )\n}\n\nexport function BasicTable({\n table,\n noRowElement = <StandardNoRows />,\n children,\n shouldHideFooter,\n ...props\n}: React.ComponentProps<'div'> & {\n table: StandardTable\n noRowElement?: React.ReactNode\n shouldHideFooter?: boolean\n}) {\n const rows = table.getRows()\n return (\n <div {...props}>\n <div className=\"rounded-lg border border-gray-950/10 overflow-hidden\">\n <TableMain>\n <TableRowContextProvider value={{ table }}>\n {rows.length ? <TableBody>{children}</TableBody> : noRowElement}\n </TableRowContextProvider>\n {rows.length > 0 && !shouldHideFooter ? <TableFooter table={table} /> : null}\n </TableMain>\n </div>\n </div>\n )\n}\n\nfunction StandardNoRows() {\n return (\n <div className=\"text-center py-8\">\n <h3 className=\"mt-2 text-sm font-semibold text-gray-900\">No results</h3>\n <p className=\"mt-1 text-sm text-gray-500\">No results were found matching your query.</p>\n </div>\n )\n}\n"},{"name":"components/table/table-toolbar.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { twMerge } from 'tailwind-merge'\nimport { type To, Link, useNavigate } from 'react-router'\nimport { Icon } from '../icon'\nimport { OptionalLink, type OptionalLinkProps } from '../optional-link'\nimport { usePress } from '@react-aria/interactions'\nimport React from 'react'\nimport { mergeProps } from '@react-aria/utils'\nimport { HeadlessButton } from '../helpers/headless-button'\nimport { PlainCheckbox } from '../checkbox'\nimport { Button, Menu, MenuItem, MenuTrigger, Popover } from 'react-aria-components'\nimport type { StandardTable } from './table-types'\n\n// --- Header ---\n\nexport interface TableHeaderProps {\n table: StandardTable\n multiSelect?: boolean\n multiSelectActions?: React.ReactNode\n children: React.ReactNode\n sticky?: boolean\n hide?: boolean\n action?: React.ReactNode\n}\n\nexport function TableHeader({ table, multiSelect, multiSelectActions, children, sticky, hide, action }: TableHeaderProps) {\n const rowCount = table.getRows().length\n const selectionSize = table.state.rowSelection?.size ?? 0\n const hasSelection = multiSelect && selectionSize > 0\n\n if (hide) return null\n\n return (\n <div\n className={twMerge(\n 'bg-gray-100 border-b border-gray-950/10 py-0.5',\n sticky && 'sticky z-20 md:z-[2] top-0',\n hasSelection && 'bg-primary-50 border-primary-200'\n )}\n >\n <div className=\"flex items-center gap-2 pl-4 pr-1.5 min-h-9\">\n {action}\n {multiSelect ? (\n <div className=\"flex items-center *:scale-90\">\n <PlainCheckbox\n checked={rowCount > 0 ? selectionSize === rowCount : false}\n onChange={() => table.toggleAllRowSelection?.()}\n isDisabled={rowCount === 0}\n />\n </div>\n ) : null}\n {hasSelection ? (\n <>\n <span className=\"text-sm text-primary-700 font-medium\">{selectionSize} selected</span>\n <button\n type=\"button\"\n onClick={() => table.clearRowSelection?.()}\n className=\"text-sm text-gray-500 hover:text-gray-700 flex items-center gap-1\"\n >\n <Icon name=\"x-mark\" className=\"size-4\" />\n <span className=\"max-sm:hidden\">Clear</span>\n </button>\n <div className=\"grow\" />\n {multiSelectActions}\n </>\n ) : (\n <>\n <div className=\"grow\" />\n <div className=\"flex overflow-x-scroll scrollbar-hide md:overflow-x-visible h-full gap-2\">{children}</div>\n </>\n )}\n </div>\n </div>\n )\n}\n\n// --- Search ---\n\ninterface TableSearchProps {\n table: StandardTable\n actions?: React.ReactNode\n placeholder?: string | null\n hideActionsOnMobile?: boolean\n hasFilters?: boolean\n hideSearch?: boolean\n}\n\nexport function TableSearch({ table, actions, placeholder, hideActionsOnMobile, hasFilters, hideSearch }: TableSearchProps) {\n const [inputValue, setInputValue] = React.useState(table.state.search ?? '')\n\n React.useEffect(() => {\n setInputValue(table.state.search ?? '')\n }, [table.state.search])\n\n const commitSearch = React.useCallback(\n (value: string) => {\n table.setSearch(value)\n },\n [table]\n )\n\n const clearAll = () => {\n table.setSearch('')\n }\n\n return (\n <div className=\"w-full mb-3\">\n <div\n className={twMerge(\n 'flex',\n hideActionsOnMobile ? 'md:flex-row-reverse md:gap-x-2 flex-wrap' : 'flex-row-reverse gap-x-2'\n )}\n >\n <div className={twMerge(hideActionsOnMobile && 'basis-full md:basis-auto')}>{actions}</div>\n {hideSearch ? null : (\n <form\n className=\"flex grow\"\n onSubmit={(e) => {\n e.preventDefault()\n commitSearch(inputValue)\n }}\n >\n <div className=\"grow relative max-w-2xl\" role=\"search\">\n <div className=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3\">\n <Icon name=\"magnifying-glass\" className=\"size-4 text-gray-400\" aria-hidden=\"true\" />\n </div>\n <input\n className={twMerge(\n 'w-full max-w-2xl pl-9 rounded-md border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-black/10 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 text-sm/6 max-sm:text-base/6',\n hasFilters ? 'pr-8' : 'pr-3'\n )}\n enterKeyHint=\"search\"\n placeholder={placeholder || undefined}\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n />\n {table.state.search ? (\n <button\n type=\"button\"\n onClick={clearAll}\n className=\"absolute inset-y-0 right-0 flex items-center pr-2.5\"\n aria-label=\"Clear search\"\n >\n <Icon name=\"x-mark\" className=\"size-4 text-gray-400 hover:text-gray-600\" />\n </button>\n ) : null}\n </div>\n </form>\n )}\n </div>\n </div>\n )\n}\n\n// --- Actions ---\n\nexport type NavigationAction = { label: string; to: To; replace?: boolean; preventScrollReset?: boolean; disabled?: boolean }\nexport type ButtonAction = { label: string; onClick: () => void; disabled?: boolean }\nexport type ActionParams = NavigationAction | ButtonAction\n\nexport type TableActionProps = {\n actions: (ActionParams | null | undefined)[] | null | undefined\n maxVisibleActions?: number\n compact?: boolean\n}\n\nfunction isNavigationAction(action: ActionParams): action is NavigationAction {\n return 'to' in action\n}\n\nexport function TableActions({ actions: rawActions, maxVisibleActions = 4, compact }: TableActionProps) {\n const navigate = useNavigate()\n const actions = (rawActions?.filter(Boolean) || []) as ActionParams[]\n\n // If we have more actions than the max, always use dropdown on desktop too\n const useDropdownOnDesktop = actions.length > maxVisibleActions\n\n const dropdownTriggerClassName = compact\n ? 'inline-flex items-center rounded px-2 py-0.5 text-sm font-medium text-primary-700 hover:text-primary-900 hover:bg-primary-100 truncate'\n : twMerge(\n 'inline-flex items-center rounded bg-white px-2 py-1 text-sm font-semibold text-gray-900',\n 'shadow-sm ring-1 ring-black/10',\n 'disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-white',\n 'hover:bg-gray-50 truncate'\n )\n\n return (\n <>\n <div className={useDropdownOnDesktop ? '' : 'xl:hidden'}>\n {actions.length ? (\n <MenuTrigger>\n <Button className={dropdownTriggerClassName}>\n Actions\n <Icon name=\"chevron-down\" className=\"h-5 w-5\" />\n </Button>\n <Popover offset={4} className=\"w-48 rounded-md shadow-lg ring-1 ring-black/10 bg-white z-20\">\n <Menu className=\"outline-none py-1\">\n {actions.map((a) => (\n <MenuItem\n key={a.label}\n isDisabled={a.disabled}\n onAction={() => {\n if (isNavigationAction(a)) {\n navigate(a.to, { replace: a.replace, preventScrollReset: a.preventScrollReset })\n } else {\n a.onClick()\n }\n }}\n className={({ isFocused }) =>\n twMerge(\n 'text-sm px-4 py-2 cursor-pointer outline-none',\n isFocused ? 'text-white bg-primary-500' : 'text-gray-900',\n a.disabled && 'opacity-50 cursor-not-allowed'\n )\n }\n >\n {a.label}\n </MenuItem>\n ))}\n </Menu>\n </Popover>\n </MenuTrigger>\n ) : null}\n </div>\n {!useDropdownOnDesktop ? (\n <div className=\"gap-x-2 hidden xl:flex\">\n {actions.map((a) =>\n isNavigationAction(a) ? (\n <ActionLink\n to={a.to}\n replace={a.replace}\n key={a.label}\n preventScrollReset={a.preventScrollReset}\n disabled={a.disabled}\n compact={compact}\n >\n {a.label}\n </ActionLink>\n ) : (\n <ActionButton key={a.label} onClick={a.onClick} disabled={a.disabled} compact={compact}>\n {a.label}\n </ActionButton>\n )\n )}\n </div>\n ) : null}\n </>\n )\n}\n\nfunction ActionLink(props: OptionalLinkProps & { compact?: boolean }) {\n const ref = React.useRef<HTMLAnchorElement>(null)\n const { isPressed, pressProps } = usePress({\n ref\n })\n const { compact, ...linkProps } = props\n return (\n <OptionalLink\n {...mergeProps(pressProps, linkProps)}\n ref={ref}\n className={twMerge(\n props.className,\n props.disabled && 'cursor-not-allowed opacity-30',\n compact\n ? 'inline-flex items-center rounded px-2 py-0.5 text-sm font-medium text-primary-700 hover:text-primary-900 hover:bg-primary-100 truncate'\n : twMerge(\n 'inline-flex items-center rounded bg-white px-2 py-1 text-sm font-semibold text-gray-900',\n 'shadow-sm ring-1 ring-black/10',\n 'hover:bg-gray-50 truncate'\n ),\n isPressed && 'scale-[.98]'\n )}\n >\n {props.children}\n </OptionalLink>\n )\n}\n\ntype ActionButtonProps = React.ComponentProps<typeof HeadlessButton> & { compact?: boolean }\n\nfunction ActionButton(props: ActionButtonProps) {\n const { className, compact, ...restProps } = props\n return (\n <HeadlessButton\n {...restProps}\n className={twMerge(\n typeof className === 'function' ? className({ isPressed: false }) : className,\n props.disabled && 'cursor-not-allowed opacity-30',\n compact\n ? 'inline-flex items-center rounded px-2 py-0.5 text-sm font-medium text-primary-700 hover:text-primary-900 hover:bg-primary-100 truncate'\n : twMerge(\n 'inline-flex items-center rounded bg-white px-2 py-1 text-sm font-semibold text-gray-900',\n 'shadow-sm ring-1 ring-black/10',\n 'hover:bg-gray-50 truncate data-[pressed=true]:scale-[.98]'\n )\n )}\n >\n {props.children}\n </HeadlessButton>\n )\n}\n"},{"name":"components/table/headless-templated-row-table.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { matchSorter, type MatchSorterOptions } from 'match-sorter'\n\nexport declare namespace HeadlessTemplatedRowTable {\n type RowData = Record<string, any>\n\n type Feature<\n TData extends RowData,\n FeatureState extends Record<string, any>,\n FeatureOptions extends Record<string, any>,\n FeatureExtras extends Record<string, any>\n > = (<\n TableState extends Record<string, any>,\n TableOptions extends Record<string, any>,\n TableExtras extends Record<string, any>\n >(\n table: Table<TData, TableState, TableOptions, TableExtras>\n ) => void) & {\n ['~feature']: {\n state: FeatureState\n options: FeatureOptions\n extras: FeatureExtras\n }\n }\n\n type FeatureBase<TData extends RowData> = Feature<TData, {}, {}, {}>\n\n type ExtractFeatureChanges<F extends FeatureBase<RowData>> =\n F extends Feature<infer FTData, infer FState, infer FOptions, infer FExtras>\n ? {\n TData: FTData\n State: FState\n Options: FOptions\n Extras: FExtras\n }\n : never\n\n type ExtractAllFeatureChanges<Features extends FeatureBase<RowData>[]> = Features extends [\n infer F extends FeatureBase<RowData>,\n ...infer Rest extends FeatureBase<RowData>[]\n ]\n ? ExtractFeatureChanges<F> & ExtractAllFeatureChanges<Rest>\n : {\n TData: RowData\n State: {}\n Options: {}\n Extras: {}\n }\n\n type ApplyFeature<T extends TableBase<RowData>, F extends FeatureBase<RowData>> =\n T extends Table<infer TData, infer State, infer Options, infer Extras>\n ? F extends Feature<infer FTData, infer FState, infer FOptions, infer FExtras>\n ? Table<TData & FTData, State & FState, Options & FOptions, Extras & FExtras>\n : never\n : never\n\n type ApplyFeatures<T extends TableBase<RowData>, Features extends FeatureBase<RowData>[]> = Features extends [\n infer F extends FeatureBase<RowData>,\n ...infer Rest extends FeatureBase<RowData>[]\n ]\n ? ApplyFeatures<ApplyFeature<T, F>, Rest>\n : T\n\n type TableOptions<TData extends RowData, Features extends FeatureBase<TData>[]> = {\n rows: TData[]\n features: [...Features]\n initialState?: Partial<ExtractAllFeatureChanges<Features>['State']>\n state?: Partial<ExtractAllFeatureChanges<Features>['State']>\n } & ExtractAllFeatureChanges<Features>['Options']\n\n type Table<\n TData extends RowData,\n State extends Record<string, any>,\n Options extends Record<string, any>,\n Extras extends Record<string, any>\n > = {\n getRows(): TData[]\n setRows(rows: TData[]): void\n getOptions(): Options & { state?: Partial<State>; initialState?: Partial<State> }\n setOptions(options: Options): void\n state: State\n setState(update: State | ((previous: State) => State)): void\n subscribe: (onStoreChange: () => void) => () => void\n } & Extras\n\n type TableBase<TData extends RowData> = Table<TData, {}, {}, {}>\n\n type ReactTable<\n TData extends RowData,\n Features extends FeatureBase<TData>[] = []\n > = HeadlessTemplatedRowTable.ApplyFeatures<HeadlessTemplatedRowTable.TableBase<TData>, Features>\n\n type FilterPredicate<\n TData extends RowData,\n Name extends string = string,\n Value = Name extends keyof TData ? TData[Name] : any\n > = (row: TData, value: Value, name: string) => boolean\n\n type FilterConfig<TData extends RowData, FC extends Record<string, any> = Record<string, any>> = {\n [K in keyof FC & string]: FilterPredicate<TData, K>\n }\n\n type ExtractFilterValue<C extends FilterPredicate<any, any, any>> = Parameters<C>[1]\n\n type FilterState<TData extends RowData, Config extends FilterConfig<TData>> = {\n [key in keyof Config]: ExtractFilterValue<Config[key]> | null | undefined\n }\n\n type SortComparator<TData extends RowData, Name extends string = string> = (\n row1: TData,\n row2: TData,\n name: string\n ) => number\n\n type SortConfig<TData extends RowData, SC extends Record<string, any> = Record<string, any>> = {\n [K in keyof SC & string]: SortComparator<TData, K>\n }\n\n type SortState<Config extends Record<string, SortComparator<any, any>>> = (keyof Config & string) | null\n\n type PaginationState = { limit: number; skip: number }\n\n type FilterFeature<TData extends RowData, Config extends FilterConfig<TData>> = Feature<\n TData,\n { filters: FilterState<TData, Config> },\n {\n manualFiltering?: boolean\n onFiltersChange?: (\n val: FilterState<TData, Config> | ((prev: FilterState<TData, Config>) => FilterState<TData, Config>)\n ) => void\n },\n {\n getFilterConfig(): { [key in keyof Config]: ExtractFilterValue<Config[key]> }\n setFilters: (\n val: FilterState<TData, Config> | ((prev: FilterState<TData, Config>) => FilterState<TData, Config>)\n ) => void\n }\n >\n\n type SearchFeature<TData extends RowData> = Feature<\n TData,\n { search: string },\n {\n manualSearch?: boolean\n onSearchChange?: (val: string | ((prev: string) => string)) => void\n },\n {\n setSearch: (val: string | ((prev: string) => string)) => void\n }\n >\n\n type SortFeature<TData extends RowData, Config extends SortConfig<TData>> = Feature<\n TData,\n { sort: SortState<Config> },\n {\n manualSort?: boolean\n onSortChange?: (val: SortState<Config> | ((prev: SortState<Config>) => SortState<Config>)) => void\n },\n {\n getSortConfig(): { [key in keyof Config]: null }\n setSort: (val: SortState<Config> | ((prev: SortState<Config>) => SortState<Config>)) => void\n }\n >\n\n type PaginationFeature<TData extends RowData> = Feature<\n TData,\n { pagination: PaginationState },\n {\n manualPagination?: boolean\n onPaginationChange?: (val: PaginationState | ((prev: PaginationState) => PaginationState)) => void\n },\n {\n setPagination: (val: PaginationState | ((prev: PaginationState) => PaginationState)) => void\n getPrePaginationRows(): TData[]\n }\n >\n\n type RowSelectionFeature<TData extends RowData> = Feature<\n TData,\n { rowSelection: Set<string> },\n {\n onRowSelectionChange?: (val: Set<string>) => void\n },\n {\n toggleRowSelection: (row: TData) => void\n toggleAllRowSelection: () => void\n isRowSelected: (row: TData) => boolean\n clearRowSelection: () => void\n }\n >\n\n type RowCountFeature<TData extends RowData> = Feature<TData, {}, { rowCount?: number }, { getRowCount(): number }>\n}\n\nexport const HeadlessTemplatedRowTable = {\n createTable,\n filtering,\n search,\n sort,\n pagination,\n rowSelection,\n rowCount\n}\n\n// ============================================================\n// createTable\n// ============================================================\n\nfunction createTable<\n TData extends HeadlessTemplatedRowTable.RowData,\n Features extends HeadlessTemplatedRowTable.FeatureBase<TData>[]\n>(\n initialState: HeadlessTemplatedRowTable.TableOptions<TData, Features>\n): HeadlessTemplatedRowTable.ApplyFeatures<HeadlessTemplatedRowTable.TableBase<TData>, Features> {\n let rows = initialState.rows\n let { rows: _rows, features: _features, ...options } = initialState\n\n const getRows = () => rows\n const getOptions = () => options\n\n function setRows(updatedRows: TData[]) {\n if (rows !== updatedRows) {\n rows = updatedRows\n }\n }\n function setOptions(updatedOptions: Omit<HeadlessTemplatedRowTable.TableOptions<TData, Features>, 'rows' | 'features'>) {\n options = { ...options, ...updatedOptions }\n if (updatedOptions.state) {\n table.state = { ...table.state, ...updatedOptions.state }\n }\n subscription.notify()\n }\n\n const subscription = createSubscription()\n let flushing = false\n let pendingNotify = false\n\n function setState<S>(val: S | ((previous: S) => S)) {\n if (typeof val === 'function') {\n // @ts-expect-error\n table.state = val(table.state)\n } else {\n // @ts-expect-error\n table.state = val\n }\n if (flushing) {\n pendingNotify = true\n return\n }\n flushing = true\n subscription.notify()\n let flushCount = 0\n while (pendingNotify) {\n if (++flushCount > 100) {\n console.warn('Table setState: exceeded 100 flush iterations — possible infinite notification loop')\n pendingNotify = false\n break\n }\n pendingNotify = false\n subscription.notify()\n }\n\n flushing = false\n }\n\n const table: HeadlessTemplatedRowTable.TableBase<TData> = {\n getRows,\n setRows,\n getOptions,\n setOptions,\n state: {},\n setState,\n subscribe: subscription.subscribe\n }\n\n initialState.features.forEach((feature) => feature(table))\n\n // @ts-expect-error\n return table\n}\n\n// ============================================================\n// Client Filtering\n// ============================================================\n\ntype FilterFeature<\n TData extends HeadlessTemplatedRowTable.RowData,\n Config extends HeadlessTemplatedRowTable.FilterConfig<TData>\n> = HeadlessTemplatedRowTable.FilterFeature<TData, Config>\n\nfunction filtering<\n TData extends HeadlessTemplatedRowTable.RowData,\n Config extends HeadlessTemplatedRowTable.FilterConfig<TData>\n>(filterConfig: Config): FilterFeature<TData, Config> {\n const filterKeys = Object.keys(filterConfig) as (keyof Config extends string ? keyof Config : never)[]\n const defaultFilterState = Object.fromEntries(filterKeys.map((key) => [key, null]))\n const tableFilterConfig = defaultFilterState as { [key in keyof Config]: null }\n\n return defineFeature<TData, FilterFeature<TData, Config>>((table) => {\n table.state.filters = {\n ...defaultFilterState,\n ...table.getOptions().initialState?.['filters']\n } as HeadlessTemplatedRowTable.FilterState<TData, Config>\n\n const prevGetFilterConfig = table['getFilterConfig']\n table.getFilterConfig = () => ({ ...(prevGetFilterConfig?.() ?? {}), ...tableFilterConfig })\n\n table.setFilters = (val) => {\n const onFiltersChange = table.getOptions().onFiltersChange\n if (onFiltersChange) onFiltersChange(val)\n else\n table.setState((prev) => ({\n ...prev,\n filters: typeof val === 'function' ? val(prev.filters) : val\n }))\n }\n\n const applyFilters = memo((rows: TData[], filters: HeadlessTemplatedRowTable.FilterState<TData, Config>) =>\n rows.filter((row) =>\n filterKeys.every((f) => {\n const value = filters[f]\n if (value == null) return true\n return filterConfig[f]!(row, value, f)\n })\n )\n )\n\n const originalGetRows = table.getRows\n table.getRows = () => {\n if (table.getOptions().manualFiltering) return originalGetRows()\n return applyFilters(originalGetRows(), table.state.filters)\n }\n })\n}\n\n// ============================================================\n// Client Search\n// ============================================================\n\ntype SearchFeature<TData extends HeadlessTemplatedRowTable.RowData> = HeadlessTemplatedRowTable.SearchFeature<TData>\n\nfunction search<TData extends HeadlessTemplatedRowTable.RowData>(options: MatchSorterOptions<TData>): SearchFeature<TData> {\n return defineFeature<TData, SearchFeature<TData>>((table) => {\n const applySearch = memo((rows: TData[], query: string) => {\n return query ? query.split(' ').reduce((results, term) => matchSorter(results, term, options), rows) : rows\n })\n\n table.state.search = (table.getOptions().initialState?.['search'] as string) ?? ''\n\n table.setSearch = (val) => {\n const onSearchChange = table.getOptions().onSearchChange\n if (onSearchChange) onSearchChange(val)\n else table.setState((prev) => ({ ...prev, search: typeof val === 'function' ? val(prev.search) : val }))\n }\n\n const originalGetRows = table.getRows\n table.getRows = () => {\n if (table.getOptions().manualSearch) return originalGetRows()\n return applySearch(originalGetRows(), table.state.search)\n }\n })\n}\n\n// ============================================================\n// Client Sort\n// ============================================================\n\ntype SortFeature<\n TData extends HeadlessTemplatedRowTable.RowData,\n Config extends HeadlessTemplatedRowTable.SortConfig<TData>\n> = HeadlessTemplatedRowTable.SortFeature<TData, Config>\n\nfunction sort<TData extends HeadlessTemplatedRowTable.RowData, Config extends HeadlessTemplatedRowTable.SortConfig<TData>>(\n config: Config,\n defaultSort?: HeadlessTemplatedRowTable.SortState<Config>\n): SortFeature<TData, Config> {\n const tableSortConfig = Object.fromEntries(Object.keys(config).map((f) => [f, null])) as {\n [key in keyof Config]: null\n }\n\n return defineFeature<TData, SortFeature<TData, Config>>((table) => {\n table.state.sort =\n (table.getOptions().initialState?.['sort'] as HeadlessTemplatedRowTable.SortState<Config>) ?? defaultSort ?? null\n\n const prevGetSortConfig = table['getSortConfig']\n table.getSortConfig = () => ({ ...(prevGetSortConfig?.() ?? {}), ...tableSortConfig })\n\n table.setSort = (val) => {\n const onSortChange = table.getOptions().onSortChange\n if (onSortChange) onSortChange(val)\n else table.setState((prev) => ({ ...prev, sort: typeof val === 'function' ? val(prev.sort) : val }))\n }\n\n const applySort = memo((rows: TData[], sort: HeadlessTemplatedRowTable.SortState<Config>) =>\n sort && config[sort] ? [...rows].sort((a, b) => config[sort]!(a, b, sort)) : rows\n )\n\n const originalGetRows = table.getRows\n table.getRows = () => {\n if (table.getOptions().manualSort) return originalGetRows()\n return applySort(originalGetRows(), table.state.sort)\n }\n })\n}\n\n// ============================================================\n// Client Pagination\n// ============================================================\n\ntype PaginationFeature<TData extends HeadlessTemplatedRowTable.RowData> = HeadlessTemplatedRowTable.PaginationFeature<TData>\n\nfunction pagination<TData extends HeadlessTemplatedRowTable.RowData>(defaultLimit: number): PaginationFeature<TData> {\n return defineFeature<TData, PaginationFeature<TData>>((table) => {\n table.state.pagination = (table.getOptions().initialState?.[\n 'pagination'\n ] as HeadlessTemplatedRowTable.PaginationState) ?? {\n limit: defaultLimit,\n skip: 0\n }\n\n table.setPagination = (val) => {\n const onPaginationChange = table.getOptions().onPaginationChange\n if (onPaginationChange) onPaginationChange(val)\n else table.setState((prev) => ({ ...prev, pagination: typeof val === 'function' ? val(prev.pagination) : val }))\n }\n\n const originalGetRows = table.getRows\n table.getPrePaginationRows = originalGetRows\n table.getRows = () => {\n if (table.getOptions().manualPagination) return originalGetRows()\n const { skip, limit } = table.state.pagination\n return originalGetRows().slice(skip, skip + limit)\n }\n })\n}\n\n// ============================================================\n// Client Row Selection\n// ============================================================\n\ntype RowSelectionFeature<TData extends HeadlessTemplatedRowTable.RowData> =\n HeadlessTemplatedRowTable.RowSelectionFeature<TData>\n\nfunction rowSelection<TData extends HeadlessTemplatedRowTable.RowData>(\n getRowId: (row: TData) => string\n): RowSelectionFeature<TData> {\n return defineFeature<TData, RowSelectionFeature<TData>>((table) => {\n table.state.rowSelection = (table.getOptions().initialState?.['rowSelection'] as Set<string>) ?? new Set<string>()\n\n const getSelection = () => table.state.rowSelection\n const setSelection = (next: Set<string>) => {\n const onRowSelectionChange = table.getOptions().onRowSelectionChange\n if (onRowSelectionChange) onRowSelectionChange(next)\n else table.setState((prev) => ({ ...prev, rowSelection: next }))\n }\n\n table.toggleRowSelection = (row: TData) => {\n const id = getRowId(row)\n const next = new Set(getSelection())\n if (next.has(id)) next.delete(id)\n else next.add(id)\n setSelection(next)\n }\n\n table.toggleAllRowSelection = () => {\n const rows = table.getRows()\n const rowIds = new Set(rows.map(getRowId))\n const allSelected = rowIds.size > 0 && [...rowIds].every((id) => getSelection().has(id))\n setSelection(allSelected ? new Set<string>() : rowIds)\n }\n\n table.isRowSelected = (row: TData) => {\n return getSelection().has(getRowId(row))\n }\n\n table.clearRowSelection = () => {\n setSelection(new Set<string>())\n }\n })\n}\n\n// ============================================================\n// Row Count\n// ============================================================\n\ntype RowCountFeature<TData extends HeadlessTemplatedRowTable.RowData> = HeadlessTemplatedRowTable.RowCountFeature<TData>\n\nfunction rowCount<TData extends HeadlessTemplatedRowTable.RowData>(): RowCountFeature<TData> {\n return defineFeature<TData, RowCountFeature<TData>>((table) => {\n table.getRowCount = () => {\n return (\n table.getOptions().rowCount ??\n ('getPrePaginationRows' in table ? table.getPrePaginationRows().length : table.getRows().length)\n )\n }\n })\n}\n\n// ============================================================\n// Helpers\n// ============================================================\n\nfunction memo<TArgs extends any[], TResult>(fn: (...args: TArgs) => TResult): (...args: TArgs) => TResult {\n let lastArgs: TArgs | undefined\n let lastResult: TResult\n\n return (...args: TArgs) => {\n if (lastArgs && args.length === lastArgs.length && args.every((arg, i) => arg === lastArgs![i])) {\n return lastResult\n }\n lastArgs = args\n lastResult = fn(...args)\n return lastResult\n }\n}\n\nfunction createSubscription(callback?: () => void) {\n type Subscriber = () => void\n const subscribers = new Set<Subscriber>(callback ? [callback] : [])\n function subscribe(onStoreChange: () => void) {\n subscribers.add(onStoreChange)\n function unsubscribe() {\n subscribers.delete(onStoreChange)\n }\n\n return unsubscribe\n }\n function notify() {\n subscribers.forEach((s) => s())\n }\n return { subscribe, notify }\n}\n\nfunction defineFeature<\n TData extends HeadlessTemplatedRowTable.RowData,\n F extends HeadlessTemplatedRowTable.Feature<TData, {}, {}, {}>\n>(\n callback: <\n TableState extends Record<string, any>,\n TableOptions extends Record<string, any>,\n TableExtras extends Record<string, any>\n >(\n table: HeadlessTemplatedRowTable.ApplyFeature<\n HeadlessTemplatedRowTable.Table<TData, TableState, TableOptions, TableExtras>,\n F\n >\n ) => void\n): F {\n // @ts-expect-error\n return (table) => callback(table)\n}\n"},{"name":"components/table/use-client-table.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport React from 'react'\nimport { HeadlessTemplatedRowTable } from './headless-templated-row-table'\nimport type { MatchSorterOptions } from 'match-sorter'\n\ntype ClientFeatures<\n TData extends HeadlessTemplatedRowTable.RowData,\n FC extends HeadlessTemplatedRowTable.FilterConfig<TData> = {},\n SC extends HeadlessTemplatedRowTable.SortConfig<TData> = {}\n> = [\n ReturnType<typeof HeadlessTemplatedRowTable.rowCount<TData>>,\n ReturnType<typeof HeadlessTemplatedRowTable.filtering<TData, HeadlessTemplatedRowTable.FilterConfig<TData, FC>>>,\n ReturnType<typeof HeadlessTemplatedRowTable.search<TData>>,\n ReturnType<typeof HeadlessTemplatedRowTable.sort<TData, SC>>,\n ReturnType<typeof HeadlessTemplatedRowTable.rowSelection<TData>>,\n ReturnType<typeof HeadlessTemplatedRowTable.pagination<TData>>\n]\n\nexport function useClientTable<\n TData extends HeadlessTemplatedRowTable.RowData,\n FC extends HeadlessTemplatedRowTable.FilterConfig<NoInfer<TData>> = {},\n SC extends HeadlessTemplatedRowTable.SortConfig<NoInfer<TData>> = {}\n>({\n rows: rowsInput,\n filters,\n search,\n sorts,\n getRowId,\n defaultPageSize,\n onFiltersChange,\n onSearchChange,\n onSortChange,\n onPaginationChange,\n onRowSelectionChange,\n manualFiltering,\n manualSearch,\n manualSort,\n manualPagination\n}: {\n rows: TData[]\n filters?: FC & HeadlessTemplatedRowTable.FilterConfig<NoInfer<TData>, FC>\n search?: MatchSorterOptions<TData>\n sorts?: SC & HeadlessTemplatedRowTable.SortConfig<NoInfer<TData>, SC>\n getRowId: (row: TData) => string\n defaultPageSize?: number\n manualFiltering?: boolean\n onFiltersChange?: (\n val:\n | HeadlessTemplatedRowTable.FilterState<TData, FC>\n | ((prev: HeadlessTemplatedRowTable.FilterState<TData, FC>) => HeadlessTemplatedRowTable.FilterState<TData, FC>)\n ) => void\n manualSearch?: boolean\n onSearchChange?: (val: string | ((prev: string) => string)) => void\n manualSort?: boolean\n onSortChange?: (\n val:\n | HeadlessTemplatedRowTable.SortState<SC>\n | ((prev: HeadlessTemplatedRowTable.SortState<SC>) => HeadlessTemplatedRowTable.SortState<SC>)\n ) => void\n manualPagination?: boolean\n onPaginationChange?: (val: any) => void\n onRowSelectionChange?: (val: Set<string>) => void\n}): HeadlessTemplatedRowTable.ReactTable<TData, ClientFeatures<TData, FC, SC>> {\n const tableRef = React.useRef(null as HeadlessTemplatedRowTable.ReactTable<TData, ClientFeatures<TData, FC, SC>> | null)\n\n const options = React.useMemo(() => {\n const applyState = (key: string, val: any) => {\n tableRef.current!.setState((prev: any) => ({\n ...prev,\n [key]: typeof val === 'function' ? val(prev[key]) : val\n }))\n }\n const resetDependentState = (resetPagination: boolean) => {\n tableRef.current!.setState((prev: any) => {\n const resetPag = resetPagination && !manualPagination && prev.pagination?.skip !== 0\n const resetSel = prev.rowSelection?.size > 0\n if (!resetPag && !resetSel) return prev\n return {\n ...prev,\n ...(resetPag ? { pagination: { ...prev.pagination, skip: 0 } } : {}),\n ...(resetSel ? { rowSelection: new Set() } : {})\n }\n })\n }\n\n return {\n onFiltersChange: (val: any) => {\n if (onFiltersChange) onFiltersChange(val)\n else applyState('filters', val)\n resetDependentState(true)\n },\n onSearchChange: (val: any) => {\n if (onSearchChange) onSearchChange(val)\n else applyState('search', val)\n resetDependentState(true)\n },\n onSortChange: (val: any) => {\n if (onSortChange) onSortChange(val)\n else applyState('sort', val)\n resetDependentState(true)\n },\n onPaginationChange: (val: any) => {\n if (onPaginationChange) onPaginationChange(val)\n else applyState('pagination', val)\n resetDependentState(false)\n },\n onRowSelectionChange,\n manualFiltering,\n manualSearch,\n manualSort,\n manualPagination\n }\n }, [\n onFiltersChange,\n onSearchChange,\n onSortChange,\n onPaginationChange,\n onRowSelectionChange,\n manualFiltering,\n manualSearch,\n manualSort,\n manualPagination\n ])\n\n if (!tableRef.current) {\n tableRef.current = HeadlessTemplatedRowTable.createTable({\n features: [\n HeadlessTemplatedRowTable.filtering<TData, HeadlessTemplatedRowTable.FilterConfig<TData, FC>>(\n (filters ?? {}) as HeadlessTemplatedRowTable.FilterConfig<TData, FC>\n ),\n HeadlessTemplatedRowTable.search<TData>(search ?? {}),\n HeadlessTemplatedRowTable.sort<TData, SC>((sorts ?? {}) as SC),\n HeadlessTemplatedRowTable.rowCount(),\n HeadlessTemplatedRowTable.rowSelection<TData>(getRowId),\n HeadlessTemplatedRowTable.pagination<TData>(defaultPageSize ?? 10)\n ],\n rows: rowsInput,\n ...options\n }) as HeadlessTemplatedRowTable.ReactTable<TData, ClientFeatures<TData, FC, SC>>\n }\n const table = tableRef.current\n\n const state = React.useSyncExternalStore(\n table.subscribe,\n () => table.state,\n () => table.state\n )\n table.state = state\n\n React.useEffect(() => {\n table.setRows(rowsInput)\n }, [rowsInput])\n React.useEffect(() => {\n table.setOptions(options)\n }, [options])\n\n return table\n}\n"},{"name":"components/table/use-server-table.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport React from 'react'\nimport { useSearchParams as useSearchParamsRR } from 'react-router'\nimport { HeadlessTemplatedRowTable } from './headless-templated-row-table'\nimport { SearchParams } from '../../utils/use-query-state'\nimport { type ServerTable } from './server-table'\n\ntype ServerFilterConfig<TData extends HeadlessTemplatedRowTable.RowData, FC extends Record<string, any>> = {\n [K in keyof FC & string]: HeadlessTemplatedRowTable.FilterPredicate<TData, K, NonNullable<FC[K]>>\n}\n\ntype ServerSortConfig<TData extends HeadlessTemplatedRowTable.RowData, SortKeys extends string> = Record<\n SortKeys,\n HeadlessTemplatedRowTable.SortComparator<TData>\n>\n\ntype ServerFeatures<\n TData extends HeadlessTemplatedRowTable.RowData,\n FC extends Record<string, any>,\n SC extends Record<string, any>\n> = [\n ReturnType<typeof HeadlessTemplatedRowTable.rowCount<TData>>,\n ReturnType<typeof HeadlessTemplatedRowTable.filtering<TData, ServerFilterConfig<TData, FC>>>,\n ReturnType<typeof HeadlessTemplatedRowTable.sort<TData, ServerSortConfig<TData, keyof SC & string>>>,\n ReturnType<typeof HeadlessTemplatedRowTable.search<TData>>,\n ReturnType<typeof HeadlessTemplatedRowTable.rowSelection<TData>>,\n ReturnType<typeof HeadlessTemplatedRowTable.pagination<TData>>\n]\n\nconst noopSearchCodec = SearchParams.codecs.string().default('')\nconst noopPaginationLoader = SearchParams.createLoader({\n limit: SearchParams.codecs.int().default(0),\n skip: SearchParams.codecs.int().default(0)\n})\n\nexport function useServerTable<\n TData extends HeadlessTemplatedRowTable.RowData,\n FC extends Record<string, any> = {},\n SC extends Record<string, any> = {}\n>(\n serverTable: ServerTable.ServerTable<TData, FC, SC>,\n _options: {\n getRowId: (row: TData) => string\n }\n): HeadlessTemplatedRowTable.ReactTable<TData, ServerFeatures<TData, FC, SC>> {\n type SortKey = keyof SC & string\n\n const filterCodec = React.useMemo(\n () => SearchParams.deserializeLoader(serverTable.filters),\n [serverTable.filters]\n ) as SearchParams.QueryStateLoader<FC>\n const sortCodec = React.useMemo(() => SearchParams.deserializeCodec(serverTable.sorts), [serverTable.sorts])\n const searchCodec = React.useMemo(\n () => (serverTable.search ? SearchParams.deserializeCodec(serverTable.search) : noopSearchCodec),\n [serverTable.search]\n )\n const paginationCodec = React.useMemo(\n () => (serverTable.pagination ? SearchParams.deserializeLoader(serverTable.pagination) : noopPaginationLoader),\n [serverTable.pagination]\n )\n\n const [filters] = SearchParams.useSearchParams(filterCodec)\n const [sort] = SearchParams.useState('sort', sortCodec)\n const sortValues = serverTable.sorts.values\n const [search] = SearchParams.useState('search', searchCodec)\n const [pagination, setPagination] = SearchParams.useSearchParams(paginationCodec)\n const [, setURLParams] = useSearchParamsRR()\n\n // Build noop configs for features (manual mode — predicates/comparators won't be called)\n const filterConfig = React.useMemo(\n () => Object.fromEntries(Object.keys(filterCodec ?? {}).map((key) => [key, () => true])),\n [filterCodec]\n ) as unknown as ServerFilterConfig<TData, FC>\n const sortConfig = React.useMemo(\n () => Object.fromEntries(sortValues.map((v) => [v, () => 0])),\n [sortValues]\n ) as unknown as ServerSortConfig<TData, SortKey>\n\n const state = React.useMemo(\n () => ({\n filters,\n sort: sort as SortKey | null,\n search: search ?? '',\n pagination: { limit: pagination.limit, skip: pagination.skip }\n }),\n [filters, sort, search, pagination]\n )\n\n const options = React.useMemo(\n () => ({\n manualFiltering: true as const,\n manualSearch: true as const,\n manualSort: true as const,\n manualPagination: true as const,\n onFiltersChange: (v: any) => {\n setURLParams(\n (prev) => {\n const next = new URLSearchParams(prev)\n const oldFilters = Object.fromEntries(\n Object.entries(filterCodec).map(([name, codec]: [string, any]) => {\n try {\n return [name, codec.decode(next.get(name)) ?? codec._def?.defaultValue ?? null]\n } catch {\n return [name, codec._def?.defaultValue ?? null]\n }\n })\n )\n const newFilters = typeof v === 'function' ? v(oldFilters) : v\n for (const [name, codec] of Object.entries(filterCodec) as [string, any][]) {\n const encoded = codec.encode(newFilters[name])\n if (\n encoded === null ||\n (codec._def?.defaultValue != null &&\n codec._def?.clearOnDefault &&\n encoded === codec.encode(codec._def?.defaultValue))\n ) {\n next.delete(name)\n } else {\n next.set(name, encoded)\n }\n }\n next.delete('skip')\n return next\n },\n { preventScrollReset: true, replace: true }\n )\n table.clearRowSelection?.()\n },\n onSortChange: (v: any) => {\n setURLParams(\n (prev) => {\n const next = new URLSearchParams(prev)\n const newSort = typeof v === 'function' ? v(sortCodec.decode(next.get('sort'))) : v\n const encoded = sortCodec.encode(newSort)\n if (encoded === null) next.delete('sort')\n else next.set('sort', encoded)\n next.delete('skip')\n return next\n },\n { preventScrollReset: true, replace: true }\n )\n table.clearRowSelection?.()\n },\n onSearchChange: (v: any) => {\n setURLParams(\n (prev) => {\n const next = new URLSearchParams(prev)\n const newSearch = typeof v === 'function' ? v(next.get('search') ?? '') : v\n if (newSearch) next.set('search', newSearch)\n else next.delete('search')\n next.delete('skip')\n return next\n },\n { preventScrollReset: true, replace: true }\n )\n table.clearRowSelection?.()\n },\n onPaginationChange: (v: any) => setPagination(v, { preventScrollReset: true, replace: true }),\n rowCount: serverTable.totalRows\n }),\n [filterCodec, sortCodec, setURLParams, setPagination, serverTable.totalRows]\n )\n\n const { current: table } = React.useRef(\n HeadlessTemplatedRowTable.createTable({\n features: [\n HeadlessTemplatedRowTable.rowCount(),\n HeadlessTemplatedRowTable.filtering<TData, ServerFilterConfig<TData, FC>>(filterConfig),\n HeadlessTemplatedRowTable.sort<TData, ServerSortConfig<TData, SortKey>>(sortConfig),\n HeadlessTemplatedRowTable.search<TData>({} as any),\n HeadlessTemplatedRowTable.rowSelection<TData>(_options.getRowId),\n HeadlessTemplatedRowTable.pagination<TData>(pagination.limit ?? 25)\n ],\n rows: serverTable.rows,\n initialState: state,\n ...options\n })\n )\n\n table.state = React.useSyncExternalStore(\n table.subscribe,\n () => table.state,\n () => table.state\n )\n\n table.setRows(serverTable.rows)\n\n React.useEffect(() => {\n table.setOptions({ state, ...options })\n }, [state, options])\n\n return table\n}\n"},{"name":"components/table/server-table.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { matchSorter, type MatchSorterOptions } from 'match-sorter'\nimport { SearchParams } from '../../utils/use-query-state'\nimport type { SearchParams as SearchParamsTypes } from '../../utils/use-query-state'\nimport type { HeadlessTemplatedRowTable } from './headless-templated-row-table'\n\nexport declare namespace ServerTable {\n type RowData = HeadlessTemplatedRowTable.RowData\n\n type ServerTableOptions<TData extends RowData, FC extends Record<string, any>, SC extends Record<string, any>> = {\n rows: TData[]\n url: string | URL\n filters?: FilterFieldMap<TData, FC>\n sorts?: HeadlessTemplatedRowTable.SortConfig<TData, SC>\n defaultSort?: HeadlessTemplatedRowTable.SortState<SC>\n search?: MatchSorterOptions<TData>\n pagination?: number\n }\n\n type ServerTable<TData extends RowData, FC extends Record<string, any>, SC extends Record<string, any>> = {\n rows: TData[]\n totalRows: number\n filters: SerializedFilterCodecs<FilterFieldMap<TData, FC>>\n sorts: SerializedSortCodec<SC>\n search: SearchParamsTypes.CodecDef<string | null> | null\n pagination: {\n limit: SearchParamsTypes.CodecDef<number | null>\n skip: SearchParamsTypes.CodecDef<number | null>\n } | null\n }\n\n // ---- Filtering ----\n\n type FilterField<TData extends RowData, Name extends string = string, Value = any> = {\n filter: HeadlessTemplatedRowTable.FilterPredicate<TData, Name, NonNullable<Value>>\n codec: SearchParamsTypes.Codec<SearchParamsTypes.CodecDef<Value>>\n }\n\n type FilterFieldMap<TData extends RowData, FC extends Record<string, any> = Record<string, any>> = {\n [K in keyof FC & string]: FilterField<TData, K, FC[K]>\n }\n\n type ExtractFilterValue<C extends FilterField<any, any, any>> = Parameters<C['filter']>[1]\n\n type FilterValues<TData extends RowData, Def extends FilterFieldMap<TData>> = {\n [key in keyof Def]: ExtractFilterValue<Def[key]> | null | undefined\n }\n\n type SerializedFilterCodecs<Def extends FilterFieldMap<any>> = {\n [K in keyof Def & string]: SearchParamsTypes.SerializedCodec<Def[K]['codec']>\n }\n\n // ---- Sorting ----\n\n type SortCodec<SC extends Record<string, any>> = SearchParamsTypes.Codec<\n SearchParamsTypes.CodecDef<(keyof SC & string) | null>\n >\n\n type SerializedSortCodec<SC extends Record<string, any>> = SearchParamsTypes.SerializedCodec<SortCodec<SC>> & {\n values: readonly (keyof SC & string)[]\n }\n}\n\nexport function createServerTable<\n TData extends ServerTable.RowData,\n FC extends Record<string, any> = {},\n SC extends Record<string, any> = {}\n>(config: ServerTable.ServerTableOptions<TData, FC, SC>): ServerTable.ServerTable<TData, FC, SC> {\n type Def = ServerTable.FilterFieldMap<TData, FC>\n let rows = config.rows\n\n // Build a unified loader from all feature codecs and read state from URL\n const filterDef = config.filters\n const filterKeys = filterDef ? (Object.keys(filterDef) as (keyof FC & string)[]) : []\n const sortKeys = config.sorts ? (Object.keys(config.sorts) as (keyof SC & string)[]) : []\n\n // Build codecs once\n const sortCodec = config.sorts\n ? SearchParams.codecs.enum(sortKeys).default(config.defaultSort ?? null)\n : SearchParams.codecs.enum([] as string[])\n const searchCodec = config.search ? SearchParams.codecs.string().default('') : null\n const limitCodec = config.pagination != null ? SearchParams.codecs.int().default(config.pagination) : null\n const skipCodec = config.pagination != null ? SearchParams.codecs.int().default(0) : null\n\n const loader = {\n ...Object.fromEntries(filterKeys.map((key) => [key, filterDef![key]!.codec])),\n ...(config.sorts ? { sort: sortCodec } : {}),\n ...(searchCodec ? { search: searchCodec } : {}),\n ...(limitCodec && skipCodec ? { limit: limitCodec, skip: skipCodec } : {})\n }\n\n const params = SearchParams.getSearchParamsState(config.url, loader)\n\n // Filtering\n const filterCodecs = filterDef\n ? (Object.fromEntries(\n filterKeys.map((key) => [key, SearchParams.serializeCodec(filterDef[key]!.codec)])\n ) as ServerTable.SerializedFilterCodecs<Def>)\n : ({} as ServerTable.SerializedFilterCodecs<Def>)\n\n rows = rows.filter((row) =>\n filterKeys.every((f) => {\n const value = (params as Record<string, any>)[f]\n if (value == null) return true\n return filterDef![f]!.filter(row, value, f)\n })\n )\n\n // Search\n if (config.search && searchCodec) {\n const query = (params as any).search as string\n if (query) {\n rows = query.split(' ').reduce((results, term) => matchSorter(results, term, config.search!), rows)\n }\n }\n\n // Sort\n if (config.sorts) {\n const sortKey = ((params as any).sort as string) || null\n if (sortKey && config.sorts[sortKey as keyof SC & string]) {\n rows = [...rows].sort((a, b) => config.sorts![sortKey as keyof SC & string]!(a, b, sortKey))\n }\n }\n\n // Pagination\n const totalRows = rows.length\n if (config.pagination != null) {\n const limit = ((params as any).limit as number) ?? config.pagination\n const skip = ((params as any).skip as number) ?? 0\n rows = rows.slice(skip, skip + limit)\n }\n\n return {\n rows,\n totalRows,\n filters: filterCodecs,\n sorts: SearchParams.serializeCodec(sortCodec) as ServerTable.SerializedSortCodec<SC>,\n search: searchCodec ? SearchParams.serializeCodec(searchCodec) : null,\n pagination:\n limitCodec && skipCodec\n ? { limit: SearchParams.serializeCodec(limitCodec), skip: SearchParams.serializeCodec(skipCodec) }\n : null\n }\n}\n"},{"name":"components/table/table-container.type-test.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport type { StandardTable, TableLocalization, TableFilterLocalization } from './table-types'\nimport type { Iso } from 'iso-fns'\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\ntype Expect<T extends true> = T\ntype Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false\n\n// =============================================================================\n// Setup — concrete filter/sort configs\n// =============================================================================\n\ntype StatusValue = 'active' | 'inactive'\ntype DateRangeValue = { startDate: Iso.Date; endDate: Iso.Date }\n\ntype FC = { status: StatusValue; dateRange: DateRangeValue }\ntype SC = { newest: null; oldest: null }\n\ntype MyTable = StandardTable<FC, SC>\ntype MyLocalization = TableLocalization<FC, SC>\n\n// =============================================================================\n// TableFilterLocalization discriminates on value type\n// =============================================================================\n\n// String union → DefaultTableFilter (has options array with typed values, no `type` discriminant)\nconst _selectFilter: TableFilterLocalization<StatusValue> = {\n label: 'Status',\n options: [{ label: 'Active', value: 'active' }, { label: 'Inactive', value: 'inactive' }]\n}\n\n// Date range → DateRangeTableFilter (type: 'date-range', no value-typed options)\nconst _dateFilter: TableFilterLocalization<DateRangeValue> = {\n label: 'Date range',\n type: 'date-range'\n}\n\n// date-range value rejects DefaultTableFilter form\nconst _dateFilterRejectsSelect: TableFilterLocalization<DateRangeValue> = {\n label: 'Date range',\n type: 'date-range',\n // @ts-expect-error — DefaultTableFilter 'options' shape is not valid for date-range\n options: [{ label: 'Last 7 days', value: 'last7' }]\n}\n\n// string-union value rejects DateRangeTableFilter form\nconst _selectFilterRejectsDateRange: TableFilterLocalization<StatusValue> = {\n label: 'Status',\n // @ts-expect-error — type: 'date-range' is not valid for a select filter\n type: 'date-range'\n}\n\n// =============================================================================\n// TableLocalization requires the exact filter + sort keys\n// =============================================================================\n\ntype _FiltersHasKeys = Expect<Equal<keyof MyLocalization['filters'], 'status' | 'dateRange'>>\ntype _SortsHasKeys = Expect<Equal<keyof MyLocalization['sorts'], 'newest' | 'oldest'>>\ntype _SortShape = Expect<Equal<MyLocalization['sorts']['newest'], { label: string }>>\n\n// =============================================================================\n// Full valid localization object\n// =============================================================================\n\nconst _valid: MyLocalization = {\n filters: {\n status: {\n label: 'Status',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Inactive', value: 'inactive' }\n ]\n },\n dateRange: {\n label: 'Date range',\n type: 'date-range'\n }\n },\n sorts: {\n newest: { label: 'Newest' },\n oldest: { label: 'Oldest' }\n },\n searchPlaceholder: 'Search...'\n}\n\n// =============================================================================\n// Negative cases\n// =============================================================================\n\ndeclare function checkLocalization(l: MyLocalization): void\n\n// Missing a required filter key\ncheckLocalization({\n // @ts-expect-error — 'dateRange' is missing from filters\n filters: {\n status: { label: 'Status', options: [{ label: 'Active', value: 'active' }] }\n },\n sorts: { newest: { label: 'Newest' }, oldest: { label: 'Oldest' } }\n})\n\n// Missing a required sort key\ncheckLocalization({\n filters: {\n status: { label: 'Status', options: [{ label: 'Active', value: 'active' }] },\n dateRange: { label: 'Date range', type: 'date-range' }\n },\n // @ts-expect-error — 'oldest' is missing from sorts\n sorts: { newest: { label: 'Newest' } }\n})\n\n// Wrong option value type — status options must use StatusValue\ncheckLocalization({\n filters: {\n status: {\n label: 'Status',\n // @ts-expect-error — 'unknown' is not assignable to StatusValue\n options: [{ label: 'Unknown', value: 'unknown' }]\n },\n dateRange: { label: 'Date range', type: 'date-range' }\n },\n sorts: { newest: { label: 'Newest' }, oldest: { label: 'Oldest' } }\n})\n\n// Wrong filter form — dateRange requires DateRangeTableFilter, not DefaultTableFilter\ncheckLocalization({\n filters: {\n status: { label: 'Status', options: [{ label: 'Active', value: 'active' }] },\n dateRange: {\n label: 'Date range',\n // @ts-expect-error — select-style options array is not valid for date-range filter\n options: [{ label: 'Last 7 days', value: 'last7' }]\n }\n },\n sorts: { newest: { label: 'Newest' }, oldest: { label: 'Oldest' } }\n})\n\n// =============================================================================\n// TableContainer generic inference — localization is derived from table type\n// =============================================================================\n\ntype InferredLocalization<T extends StandardTable> = TableLocalization<\n ReturnType<T['getFilterConfig']>,\n ReturnType<T['getSortConfig']>\n>\n\n// Inferred localization from MyTable matches the manually specified one\ntype _Inferred = Expect<Equal<InferredLocalization<MyTable>, MyLocalization>>\n\n// A table with no filters/sorts infers empty objects\ntype EmptyTable = StandardTable<{}, {}>\ntype EmptyLocalization = InferredLocalization<EmptyTable>\ntype _EmptyFilters = Expect<Equal<EmptyLocalization['filters'], {}>>\ntype _EmptySorts = Expect<Equal<EmptyLocalization['sorts'], {}>>\n"},{"name":"components/table/use-client-table.type-test.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { useClientTable } from './use-client-table'\nimport type { TableLocalization } from './table-types'\nimport type { Iso } from 'iso-fns'\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\ntype Expect<T extends true> = T\ntype Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false\n\n// =============================================================================\n// Setup\n// =============================================================================\n\ntype Row = {\n id: string\n status: 'active' | 'inactive'\n name: string\n createdAt: string\n}\n\n// =============================================================================\n// Filter and sort config inference\n// =============================================================================\n\nfunction testFilterAndSortInference() {\n const table = useClientTable({\n rows: [] as Row[],\n getRowId: (row) => row.id,\n filters: {\n status: (row, value) => row.status === value\n },\n sorts: {\n newest: (a, b) => a.createdAt.localeCompare(b.createdAt),\n oldest: (a, b) => b.createdAt.localeCompare(a.createdAt)\n }\n })\n\n // getFilterConfig() keys match the provided filter names\n type _FilterKeys = Expect<Equal<keyof ReturnType<typeof table.getFilterConfig>, 'status'>>\n\n // getSortConfig() keys match the provided sort names\n type _SortKeys = Expect<Equal<keyof ReturnType<typeof table.getSortConfig>, 'newest' | 'oldest'>>\n\n // state.sort is the union of sort keys | null\n type _SortState = Expect<Equal<typeof table.state.sort, 'newest' | 'oldest' | null>>\n\n // state.filters keys match filter names\n type _FilterStateKeys = Expect<Equal<keyof typeof table.state.filters, 'status'>>\n}\n\n// =============================================================================\n// Localization compatibility — select filter\n// =============================================================================\n\nfunction testLocalizationWithSelectFilter() {\n const table = useClientTable({\n rows: [] as Row[],\n getRowId: (row) => row.id,\n filters: {\n status: (row, value) => row.status === value\n },\n sorts: {\n newest: (a, b) => 0,\n oldest: (a, b) => 0\n }\n })\n\n type Loc = TableLocalization<\n ReturnType<typeof table.getFilterConfig>,\n ReturnType<typeof table.getSortConfig>\n >\n\n type _LocFilterKeys = Expect<Equal<keyof Loc['filters'], 'status'>>\n type _LocSortKeys = Expect<Equal<keyof Loc['sorts'], 'newest' | 'oldest'>>\n\n // Valid localization with typed option values\n const _loc: Loc = {\n filters: {\n status: {\n label: 'Status',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Inactive', value: 'inactive' }\n ]\n }\n },\n sorts: {\n newest: { label: 'Newest' },\n oldest: { label: 'Oldest' }\n }\n }\n}\n\n// =============================================================================\n// Localization compatibility — date range filter\n// =============================================================================\n\nfunction testLocalizationWithDateRangeFilter() {\n type RowWithDate = {\n id: string\n dateRange: { startDate: Iso.Date; endDate: Iso.Date }\n }\n\n const table = useClientTable({\n rows: [] as RowWithDate[],\n getRowId: (row) => row.id,\n filters: {\n dateRange: (row, value) => true\n }\n })\n\n type Loc = TableLocalization<\n ReturnType<typeof table.getFilterConfig>,\n ReturnType<typeof table.getSortConfig>\n >\n\n // Valid: date-range filter accepts DateRangeTableFilter form\n const _loc: Loc = {\n filters: {\n dateRange: { label: 'Date Range', type: 'date-range' }\n },\n sorts: {}\n }\n\n // Invalid: date-range filter rejects select-style options\n const _badLoc: Loc = {\n filters: {\n dateRange: {\n label: 'Date Range',\n type: 'date-range',\n // @ts-expect-error — select-style options array is not valid for date-range\n options: [{ label: 'X', value: 'x' }]\n }\n },\n sorts: {}\n }\n}\n\n// =============================================================================\n// Filter predicate contextual typing — row and value are well typed\n// =============================================================================\n\nfunction testFilterPredicateContextualTyping() {\n useClientTable({\n rows: [] as Row[],\n getRowId: (row) => row.id,\n filters: {\n // @ts-expect-error — 'nonExistent' doesn't exist on Row\n status: (row, value) => row.nonExistent === value\n }\n })\n\n useClientTable({\n rows: [] as Row[],\n getRowId: (row) => row.id,\n filters: {\n // value is Row['status'], comparing to a number should error\n // @ts-expect-error — Row['status'] is not comparable to number\n status: (row, value) => value === 123\n }\n })\n}\n\n// =============================================================================\n// Sort comparator contextual typing — row is well typed\n// =============================================================================\n\nfunction testSortComparatorContextualTyping() {\n useClientTable({\n rows: [] as Row[],\n getRowId: (row) => row.id,\n sorts: {\n // @ts-expect-error — 'nonExistent' doesn't exist on Row\n byName: (a, b) => a.nonExistent.localeCompare(b.name)\n }\n })\n}\n\n// =============================================================================\n// Negative: wrong localization option value\n// =============================================================================\n\ndeclare function checkLocalization(l: TableLocalization<{ status: 'active' | 'inactive' }, {}>): void\n\nfunction testWrongLocalizationOptionValue() {\n checkLocalization({\n filters: {\n status: {\n label: 'Status',\n // @ts-expect-error — 'unknown' is not assignable to Row['status']\n options: [{ label: 'Unknown', value: 'unknown' }]\n }\n },\n sorts: {}\n })\n}\n\n// =============================================================================\n// No filters/sorts → empty localization\n// =============================================================================\n\nfunction testNoFiltersOrSorts() {\n const table = useClientTable({\n rows: [] as Row[],\n getRowId: (row) => row.id\n })\n\n type Loc = TableLocalization<\n ReturnType<typeof table.getFilterConfig>,\n ReturnType<typeof table.getSortConfig>\n >\n\n type _EmptyFilters = Expect<Equal<Loc['filters'], {}>>\n type _EmptySorts = Expect<Equal<Loc['sorts'], {}>>\n}\n"},{"name":"components/table/use-server-table.type-test.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { useServerTable } from './use-server-table'\nimport type { ServerTable } from './server-table'\nimport type { TableLocalization } from './table-types'\nimport type { Iso } from 'iso-fns'\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\ntype Expect<T extends true> = T\ntype Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false\n\n// =============================================================================\n// Setup\n// =============================================================================\n\ntype Row = {\n id: string\n status: 'active' | 'inactive'\n name: string\n createdAt: string\n}\n\n// =============================================================================\n// Filter and sort config inference\n// =============================================================================\n\nfunction testFilterAndSortInference() {\n const serverTable = null as unknown as ServerTable.ServerTable<Row, { status: 'active' | 'inactive' }, { newest: true; oldest: true }>\n\n const table = useServerTable(serverTable, { getRowId: (row) => row.id })\n\n // getFilterConfig() keys match the provided filter names\n type _FilterKeys = Expect<Equal<keyof ReturnType<typeof table.getFilterConfig>, 'status'>>\n\n // getSortConfig() keys match the provided sort names\n type _SortKeys = Expect<Equal<keyof ReturnType<typeof table.getSortConfig>, 'newest' | 'oldest'>>\n\n // state.sort is the union of sort keys | null\n type _SortState = Expect<Equal<typeof table.state.sort, 'newest' | 'oldest' | null>>\n\n // state.filters keys match filter names\n type _FilterStateKeys = Expect<Equal<keyof typeof table.state.filters, 'status'>>\n}\n\n// =============================================================================\n// Localization compatibility — select filter\n// =============================================================================\n\nfunction testLocalizationWithSelectFilter() {\n const serverTable = null as unknown as ServerTable.ServerTable<Row, { status: 'active' | 'inactive' }, { newest: true; oldest: true }>\n\n const table = useServerTable(serverTable, { getRowId: (row) => row.id })\n\n type Loc = TableLocalization<\n ReturnType<typeof table.getFilterConfig>,\n ReturnType<typeof table.getSortConfig>\n >\n\n type _LocFilterKeys = Expect<Equal<keyof Loc['filters'], 'status'>>\n type _LocSortKeys = Expect<Equal<keyof Loc['sorts'], 'newest' | 'oldest'>>\n\n // Valid localization with typed option values\n const _loc: Loc = {\n filters: {\n status: {\n label: 'Status',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Inactive', value: 'inactive' }\n ]\n }\n },\n sorts: {\n newest: { label: 'Newest' },\n oldest: { label: 'Oldest' }\n }\n }\n}\n\n// =============================================================================\n// Localization compatibility — date range filter\n// =============================================================================\n\nfunction testLocalizationWithDateRangeFilter() {\n type RowWithDate = {\n id: string\n dateRange: { startDate: Iso.Date; endDate: Iso.Date }\n }\n\n const serverTable = null as unknown as ServerTable.ServerTable<\n RowWithDate,\n { dateRange: { startDate: Iso.Date; endDate: Iso.Date } },\n {}\n >\n\n const table = useServerTable(serverTable, { getRowId: (row) => row.id })\n\n type Loc = TableLocalization<\n ReturnType<typeof table.getFilterConfig>,\n ReturnType<typeof table.getSortConfig>\n >\n\n // Valid: date-range filter accepts DateRangeTableFilter form\n const _loc: Loc = {\n filters: {\n dateRange: { label: 'Date Range', type: 'date-range' }\n },\n sorts: {}\n }\n\n // Invalid: date-range filter rejects select-style options\n const _badLoc: Loc = {\n filters: {\n dateRange: {\n label: 'Date Range',\n type: 'date-range',\n // @ts-expect-error — select-style options array is not valid for date-range\n options: [{ label: 'X', value: 'x' }]\n }\n },\n sorts: {}\n }\n}\n\n// =============================================================================\n// Negative: wrong localization option value\n// =============================================================================\n\ndeclare function checkLocalization(l: TableLocalization<{ status: 'active' | 'inactive' }, {}>): void\n\nfunction testWrongLocalizationOptionValue() {\n checkLocalization({\n filters: {\n status: {\n label: 'Status',\n // @ts-expect-error — 'unknown' is not assignable to 'active' | 'inactive'\n options: [{ label: 'Unknown', value: 'unknown' }]\n }\n },\n sorts: {}\n })\n}\n\n// =============================================================================\n// No filters/sorts → empty localization\n// =============================================================================\n\nfunction testNoFiltersOrSorts() {\n const serverTable = null as unknown as ServerTable.ServerTable<Row, {}, {}>\n\n const table = useServerTable(serverTable, { getRowId: (row) => row.id })\n\n type Loc = TableLocalization<\n ReturnType<typeof table.getFilterConfig>,\n ReturnType<typeof table.getSortConfig>\n >\n\n type _EmptyFilters = Expect<Equal<Loc['filters'], {}>>\n type _EmptySorts = Expect<Equal<Loc['sorts'], {}>>\n}\n\n// =============================================================================\n// State shape — search and pagination\n// =============================================================================\n\nfunction testStateShape() {\n const serverTable = null as unknown as ServerTable.ServerTable<Row, { status: 'active' | 'inactive' }, { newest: true }>\n\n const table = useServerTable(serverTable, { getRowId: (row) => row.id })\n\n type _Search = Expect<Equal<typeof table.state.search, string>>\n type _Pagination = Expect<Equal<typeof table.state.pagination, { limit: number; skip: number }>>\n}\n"},{"name":"components/table/table-filters/select-filter.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { twMerge } from 'tailwind-merge'\nimport { Icon } from '../../icon'\nimport type { ReactNode } from 'react'\nimport type { StandardTable } from '../table-types'\nimport { Button, ComboBox, Input, ListBox, ListBoxItem, Popover, Select } from 'react-aria-components'\nimport React from 'react'\n\ninterface TableSelectFilterProps {\n table: StandardTable\n label: string\n name: string\n options: { label: string; value: any; className?: string; icon?: ReactNode }[]\n value: any\n autoComplete?: boolean\n}\n\nexport function TableSelectFilter({ table, label, name, options, value, autoComplete }: TableSelectFilterProps) {\n const setFilter = (newValue: any) => {\n const toggledValue = newValue === value ? null : newValue\n table.setFilters((prev: Record<string, any>) => ({ ...prev, [name]: toggledValue }))\n }\n\n if (autoComplete) {\n return <AutoCompleteFilter label={label} options={options} value={value} onSelect={setFilter} />\n }\n\n return <DropdownFilter label={label} options={options} value={value} onSelect={setFilter} />\n}\n\nfunction DropdownFilter({\n label,\n options,\n value,\n onSelect\n}: {\n label: string\n options: TableSelectFilterProps['options']\n value: any\n onSelect: (value: any) => void\n}) {\n return (\n <Select\n aria-label={label}\n selectedKey={value != null ? String(value) : null}\n onSelectionChange={(key) => {\n if (key === '__clear__') return onSelect(null)\n const option = options.find((o) => String(o.value) === String(key))\n onSelect(option?.value ?? null)\n }}\n >\n <Button\n className={twMerge(\n 'text-sm px-2 h-full flex text-gray-500 focus:outline-none whitespace-pre',\n 'rounded-full ring-1 ring-black/10 data-[pressed]:scale-[.98]',\n 'my-1',\n value != null ? 'text-blue-600 font-medium' : ''\n )}\n >\n {label}\n <Icon name=\"chevron-down\" className=\"ml-1 h-5 w-5\" />\n </Button>\n <Popover offset={4} className=\"w-56 rounded-md shadow-lg ring-1 ring-black/10 bg-white z-20\">\n <ListBox className=\"outline-none max-h-60 overflow-y-auto py-1\">\n {value != null && (\n <ListBoxItem\n id=\"__clear__\"\n textValue=\"Clear\"\n className={({ isFocused }) =>\n twMerge(\n 'text-sm px-4 py-2 cursor-pointer outline-none border-b border-gray-100',\n isFocused ? 'text-white bg-primary-500' : 'text-gray-500'\n )\n }\n >\n Clear filter\n </ListBoxItem>\n )}\n {options.map((o) => (\n <FilterOption key={o.label} option={o} selected={value === o.value} />\n ))}\n </ListBox>\n </Popover>\n </Select>\n )\n}\n\nexport function FilterOption({\n option,\n selected\n}: {\n option: { label: string; value: any; className?: string; icon?: ReactNode }\n selected: boolean\n}) {\n return (\n <ListBoxItem\n id={String(option.value)}\n textValue={option.label}\n className={({ isFocused }) =>\n twMerge(\n 'group items-center text-sm px-4 py-2 relative flex justify-between w-full cursor-pointer outline-none',\n isFocused ? 'text-white bg-primary-500' : 'text-gray-900',\n option.className\n )\n }\n >\n {({ isFocused }) => (\n <>\n <div\n className={twMerge(\n selected ? 'font-semibold' : 'font-normal',\n option.icon ? 'flex gap-1 items-center' : 'block'\n )}\n >\n {option.icon}\n {option.label}\n </div>\n {selected ? (\n <div className={twMerge(isFocused ? 'text-white' : 'text-primary-500', 'flex items-center')}>\n <Icon name=\"check\" className=\"h-5 w-5\" aria-hidden=\"true\" />\n </div>\n ) : null}\n </>\n )}\n </ListBoxItem>\n )\n}\n\ninterface AutoCompleteFilterProps {\n label: string\n options: { label: string; value: any; className?: string; icon?: ReactNode }[]\n value: any\n onSelect: (value: any) => void\n}\n\nfunction AutoCompleteFilter({ label, options, value, onSelect }: AutoCompleteFilterProps) {\n const [query, setQuery] = React.useState('')\n\n const filteredOptions =\n query === ''\n ? options\n : options.filter((o) => {\n const q = query.toLowerCase()\n return o.label.toLowerCase().includes(q) || String(o.value).toLowerCase().includes(q)\n })\n\n return (\n <ComboBox\n aria-label={label}\n inputValue={query}\n onInputChange={setQuery}\n selectedKey={value != null ? String(value) : null}\n onSelectionChange={(key) => {\n if (key === null) return\n if (key === '__clear__') {\n onSelect(null)\n setQuery('')\n return\n }\n const option = options.find((o) => String(o.value) === String(key))\n onSelect(option?.value ?? null)\n setQuery('')\n }}\n menuTrigger=\"focus\"\n >\n <div className=\"relative inline-block text-left\">\n <Button\n className={twMerge(\n 'text-sm px-2 h-full flex text-gray-500 focus:outline-none whitespace-pre',\n 'rounded-full ring-1 ring-black/10 data-[pressed]:scale-[.98]',\n 'my-1',\n value != null ? 'text-blue-600 font-medium' : ''\n )}\n >\n {label}\n <Icon name=\"chevron-down\" className=\"ml-1 h-5 w-5\" />\n </Button>\n </div>\n <Popover offset={4} className=\"w-56 rounded-md shadow-lg ring-1 ring-black/10 bg-white z-20\">\n <div className=\"flex p-2\">\n <Input\n className={twMerge(\n 'text-sm block rounded-md focus:ring-0 border-0 focus:outline-none px-0 py-0 w-0 grow min-w-[30px]',\n 'py-1 px-1'\n )}\n placeholder=\"Filter...\"\n autoFocus\n />\n </div>\n <ListBox className=\"outline-none max-h-60 overflow-y-auto py-1\">\n {value != null && (\n <ListBoxItem\n id=\"__clear__\"\n textValue=\"Clear\"\n className={({ isFocused }) =>\n twMerge(\n 'text-sm px-4 py-2 cursor-pointer outline-none border-b border-gray-100',\n isFocused ? 'text-white bg-primary-500' : 'text-gray-500'\n )\n }\n >\n Clear filter\n </ListBoxItem>\n )}\n {filteredOptions.map((o) => (\n <FilterOption key={o.label} option={o} selected={value === o.value} />\n ))}\n </ListBox>\n </Popover>\n </ComboBox>\n )\n}\n"},{"name":"components/table/table-filters/sort.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { twMerge } from 'tailwind-merge'\nimport { Icon } from '../../icon'\nimport React from 'react'\nimport type { StandardTable } from '../table-types'\nimport { Button, ListBox, ListBoxItem, Popover, Select } from 'react-aria-components'\n\nexport interface TableSelectSortProps {\n table: StandardTable\n value: string | null\n options: { value: string; label: string }[]\n search: string | null\n}\n\nexport function TableSelectSort({ table, value, options, search }: TableSelectSortProps) {\n const optionsWithRelevant = React.useMemo(() => {\n return search ? [{ label: 'Most Relevant', value: 'mostRelevant' }, ...options] : options\n }, [options, search])\n\n return (\n <Select\n aria-label=\"Sort\"\n selectedKey={value ?? null}\n onSelectionChange={(key) => {\n table.setSort(key === '__clear__' ? null : key ? String(key) : null)\n }}\n >\n <Button\n className={twMerge(\n 'text-sm px-2 h-full flex text-gray-500',\n 'rounded-full ring-1 ring-black/10 data-[pressed]:scale-[.98]',\n 'my-1'\n )}\n >\n Sort\n <Icon name=\"chevron-down\" className=\"ml-1 h-5 w-5\" />\n </Button>\n <Popover offset={4} className=\"w-56 rounded-md shadow-lg ring-1 ring-black/10 bg-white z-20\">\n <ListBox className=\"outline-none max-h-60 overflow-y-auto py-1\">\n {value != null && (\n <ListBoxItem\n key=\"__clear__\"\n id=\"__clear__\"\n textValue=\"Clear\"\n className={({ isFocused }) =>\n twMerge(\n 'text-sm px-4 py-2 cursor-pointer outline-none border-b border-gray-100',\n isFocused ? 'text-white bg-primary-500' : 'text-gray-500'\n )\n }\n >\n Clear sort\n </ListBoxItem>\n )}\n {optionsWithRelevant.map((o) => {\n const selected = value === o.value\n return (\n <ListBoxItem\n key={o.value}\n id={o.value}\n textValue={o.label}\n className={({ isFocused }) =>\n twMerge(\n 'group items-center text-sm px-4 py-2 relative flex justify-between w-full cursor-pointer outline-none',\n isFocused ? 'text-white bg-primary-500' : 'text-gray-900'\n )\n }\n >\n {({ isFocused }) => (\n <>\n <span className={twMerge(selected ? 'font-semibold' : 'font-normal', 'block')}>{o.label}</span>\n {selected ? (\n <span className={twMerge(isFocused ? 'text-white' : 'text-primary-500', 'flex items-center')}>\n <Icon name=\"check\" className=\"h-5 w-5\" aria-hidden=\"true\" />\n </span>\n ) : null}\n </>\n )}\n </ListBoxItem>\n )\n })}\n </ListBox>\n </Popover>\n </Select>\n )\n}\n"},{"name":"components/table/table-filters/date-range-filter.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { twMerge } from 'tailwind-merge'\nimport React from 'react'\nimport { dateFns, type Iso } from 'iso-fns'\nimport { Button } from '../../button'\nimport type { StandardTable } from '../table-types'\nimport { Button as AriaButton, Dialog, DialogTrigger, Popover } from 'react-aria-components'\nimport { Icon } from '../../icon'\nimport { DateRangePicker, type DateRangeRootAdditionalProps } from '../../date-range'\n\nexport function TableDateRangeFilter({\n table,\n label,\n value,\n name,\n options\n}: {\n table: StandardTable\n label: string\n name: string\n value: null | { startDate: Iso.Date; endDate: Iso.Date }\n options?: Partial<\n Pick<DateRangeRootAdditionalProps, 'shouldDisableDate' | 'shouldDisableEndDate' | 'shouldDisableStartDate'>\n > & {\n quickPresets?: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]\n }\n}) {\n const [startDate, endDate] = value ? [value.startDate, value.endDate] : [null, null]\n\n return (\n <DialogTrigger>\n <AriaButton\n className={twMerge(\n 'text-sm px-2 h-full flex text-gray-500 focus:outline-none whitespace-pre',\n 'rounded-full border data-[pressed]:scale-[.98]',\n 'md:my-3',\n value ? 'text-primary-400 font-semibold' : ''\n )}\n >\n {label} {startDate && endDate ? getTableDateRangeFilterValue({ startDate, endDate }, options?.quickPresets) : ''}\n <Icon name=\"chevron-down\" className=\"ml-1 h-5 w-5\" />\n </AriaButton>\n <Popover offset={4} className=\"z-20 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 bg-white\">\n <Dialog className=\"outline-none\">\n {({ close }) => (\n <TableDateRangeFilterInner\n table={table}\n filterName={name}\n closePopover={close}\n defaultStart={startDate}\n defaultEnd={endDate}\n shouldDisableDate={options?.shouldDisableDate ?? (() => false)}\n shouldDisableEndDate={options?.shouldDisableEndDate ?? (() => false)}\n shouldDisableStartDate={options?.shouldDisableStartDate ?? (() => false)}\n quickPresets={options?.quickPresets}\n />\n )}\n </Dialog>\n </Popover>\n </DialogTrigger>\n )\n}\n\nfunction TableDateRangeFilterInner({\n table,\n filterName,\n closePopover,\n defaultStart,\n defaultEnd,\n shouldDisableDate,\n shouldDisableEndDate,\n shouldDisableStartDate,\n quickPresets\n}: {\n table: StandardTable\n filterName: string\n closePopover(): void\n quickPresets?: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]\n} & DateRangeRootAdditionalProps) {\n const [start, setStart] = React.useState<DateRangeRootAdditionalProps['defaultStart']>(defaultStart)\n const [end, setEnd] = React.useState<DateRangeRootAdditionalProps['defaultEnd']>(defaultEnd)\n const [currentField, setCurrentField] = React.useState<'start' | 'end' | null>(null)\n\n const isPresetAvailable = !!quickPresets?.length\n\n const applyFilter = (dateRange: { startDate: Iso.Date; endDate: Iso.Date }) => {\n table.setFilters((prev: Record<string, any>) => ({ ...prev, [filterName]: dateRange }))\n }\n\n const onQuickPresetClick = (presetValue: { startDate: Iso.Date; endDate: Iso.Date }) => {\n setCurrentField(null)\n closePopover()\n applyFilter(presetValue)\n }\n\n // Note: DateRangePicker and its useDateRangePicker hook need to be adapted\n // from headless-primitives. For now we pass through the same shape.\n const pickerControls = {\n start: start ?? null,\n end: end ?? null,\n onStartChange(v: Iso.Date | null, f: 'start' | 'end' | null) {\n setStart(v)\n setCurrentField(f)\n },\n onEndChange(v: Iso.Date | null, f: 'start' | 'end' | null) {\n setEnd(v)\n setCurrentField(f)\n },\n currentField,\n shouldDisableDate,\n shouldDisableEndDate,\n shouldDisableStartDate\n }\n\n return (\n <div className=\"flex bg-white overflow-auto rounded-md text-base sm:text-sm\">\n {isPresetAvailable ? (\n <QuickPresetPanel\n start={start}\n end={end}\n containerClassName=\"border-solid border-r border-gray-200 py-2 overflow-y-auto sm:min-w-[200px] hidden sm:block\"\n className={twMerge('px-4 py-2 w-full text-left')}\n quickPresets={quickPresets}\n onQuickPresetClick={onQuickPresetClick}\n />\n ) : null}\n <div>\n <div className=\"px-3 py-3\">\n <DateRangePicker {...pickerControls} />\n </div>\n {isPresetAvailable ? (\n <QuickPresetPanel\n start={start}\n end={end}\n containerClassName=\"border-solid border-y border-gray-200 p-2 flex gap-1 flex-wrap sm:hidden\"\n className={twMerge('px-3 py-1 rounded-full border')}\n quickPresets={quickPresets}\n onQuickPresetClick={onQuickPresetClick}\n />\n ) : null}\n <div className=\"p-2 float-right flex gap-2\">\n <Button variant=\"soft\" onClick={closePopover}>\n Cancel\n </Button>\n <Button\n onClick={() => {\n closePopover()\n if (start && end && start <= end) {\n applyFilter({ startDate: start as Iso.Date, endDate: end as Iso.Date })\n }\n }}\n >\n Apply\n </Button>\n </div>\n </div>\n </div>\n )\n}\n\nfunction QuickPresetPanel({\n start,\n end,\n quickPresets,\n onQuickPresetClick,\n className,\n containerClassName\n}: {\n start?: string | null\n end?: string | null\n quickPresets: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]\n onQuickPresetClick: (value: { startDate: Iso.Date; endDate: Iso.Date }) => void\n className?: string\n containerClassName?: string\n}) {\n return (\n <div className={containerClassName}>\n {quickPresets.map(({ label, value }) => {\n const [presetStartDate, presetEndDate] = [value.startDate, value.endDate]\n return (\n <button\n key={label}\n className={twMerge(\n 'text-sm cursor-pointer',\n className,\n 'text-gray-900 bg-white text-nowrap',\n 'hover:text-white hover:bg-primary-500',\n 'focus:text-white focus:bg-primary-500',\n presetStartDate === start && presetEndDate === end && 'bg-primary-500 text-white'\n )}\n onClick={() => onQuickPresetClick(value)}\n >\n {label}\n </button>\n )\n })}\n </div>\n )\n}\n\nexport function getTableDateRangeFilterValue(\n value: { startDate: Iso.Date; endDate: Iso.Date },\n quickPreset?: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]\n) {\n if (quickPreset) {\n const preset = quickPreset.find((p) => p.value.startDate === value.startDate && p.value.endDate === value.endDate)\n if (preset) {\n return preset.label\n }\n }\n if (dateFns.getYear(value.startDate) !== dateFns.getYear(value.endDate)) {\n return getFormattedDateRange(value, 'MMM dd yyyy')\n }\n if (\n dateFns.getYear(value.startDate) === dateFns.getYear(dateFns.now()) &&\n dateFns.getYear(value.endDate) === dateFns.getYear(dateFns.now())\n ) {\n return getFormattedDateRange(value, 'MMM dd')\n }\n return getFormattedDateRange(value, 'MMM dd yyyy')\n}\n\nfunction getFormattedDateRange(value: { startDate: Iso.Date; endDate: Iso.Date }, format: string) {\n if (value.startDate === value.endDate) {\n return dateFns.format(value.startDate, format)\n }\n return `${dateFns.format(value.startDate, format)} - ${dateFns.format(value.endDate, format)}`\n}\n"},{"name":"components/optional-link.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { type LinkProps, Link } from 'react-router'\nimport React from 'react'\n\nexport type OptionalLinkProps = Omit<LinkProps, 'to'> & { to?: LinkProps['to']; disabled?: boolean }\n\nexport const OptionalLink = React.forwardRef<HTMLAnchorElement, OptionalLinkProps>(\n ({ to, onClick, disabled, ...props }, ref) => {\n if (to && !disabled) {\n return <Link to={to} onClick={onClick} {...props} ref={ref} />\n } else {\n return <span {...props} ref={ref} />\n }\n }\n)\nOptionalLink.displayName = 'OptionalLink'\n"},{"name":"components/date-range.tsx","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport * as React from 'react'\nimport {\n RangeCalendar,\n CalendarGrid,\n CalendarCell,\n Heading,\n CalendarGridHeader,\n CalendarHeaderCell,\n CalendarGridBody,\n Button,\n I18nProvider\n} from 'react-aria-components'\nimport { twMerge } from 'tailwind-merge'\nimport { parseDate, type CalendarDate, type DateValue } from '@internationalized/date'\nimport type { Iso } from 'iso-fns'\n\nexport type DateRangeRootAdditionalProps = {\n defaultStart: Iso.Date | null\n defaultEnd: Iso.Date | null\n shouldDisableDate: (date: string) => boolean\n shouldDisableStartDate: (date: string) => boolean\n shouldDisableEndDate: (date: string) => boolean\n}\n\ntype DateRangePickerProps = {\n start: Iso.Date | null\n end: Iso.Date | null\n onStartChange: (value: Iso.Date | null, field: 'start' | 'end' | null) => void\n onEndChange: (value: Iso.Date | null, field: 'start' | 'end' | null) => void\n currentField: 'start' | 'end' | null\n shouldDisableDate?: (date: string) => boolean\n shouldDisableStartDate?: (date: string) => boolean\n shouldDisableEndDate?: (date: string) => boolean\n}\n\nexport function DateRangePicker(props: DateRangePickerProps) {\n const ariaValue = React.useMemo(() => {\n if (!props.start || !props.end) return null\n try {\n return { start: parseDate(props.start), end: parseDate(props.end) }\n } catch {\n return null\n }\n }, [props.start, props.end])\n\n const handleChange = (range: { start: CalendarDate; end: CalendarDate } | null) => {\n if (!range) {\n props.onStartChange(null, null)\n props.onEndChange(null, null)\n return\n }\n const start = range.start.toString() as Iso.Date\n const end = range.end.toString() as Iso.Date\n props.onStartChange(start, 'end')\n props.onEndChange(end, null)\n }\n\n const isDateUnavailable = React.useCallback(\n (date: DateValue) => {\n const dateStr = date.toString()\n return props.shouldDisableDate?.(dateStr) ?? false\n },\n [props.shouldDisableDate]\n )\n\n return (\n <I18nProvider locale=\"en\">\n <RangeCalendar\n value={ariaValue}\n onChange={handleChange}\n isDateUnavailable={isDateUnavailable}\n visibleDuration={{ months: 2 }}\n className=\"text-sm\"\n >\n <header className=\"flex items-center justify-between mb-2 px-1\">\n <Button slot=\"previous\" className=\"p-1 hover:bg-neutral-100 rounded cursor-pointer\">\n <ChevronLeft />\n </Button>\n <Heading className=\"text-sm font-semibold text-neutral-900 flex-1 text-center\" />\n <Button slot=\"next\" className=\"p-1 hover:bg-neutral-100 rounded cursor-pointer\">\n <ChevronRight />\n </Button>\n </header>\n <div className=\"flex gap-4\">\n <CalendarGrid>\n <CalendarGridHeader>\n {(day) => (\n <CalendarHeaderCell className=\"text-xs text-neutral-500 font-medium w-8 h-8\">{day}</CalendarHeaderCell>\n )}\n </CalendarGridHeader>\n <CalendarGridBody>\n {(date) => (\n <CalendarCell\n date={date}\n className={({ isSelected, isSelectionStart, isSelectionEnd, isOutsideMonth, isDisabled, isUnavailable }) =>\n twMerge(\n 'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center',\n 'hover:bg-neutral-100',\n isOutsideMonth && 'text-neutral-300',\n (isDisabled || isUnavailable) && 'text-neutral-300 cursor-not-allowed',\n isSelected && !isSelectionStart && !isSelectionEnd && 'bg-primary-500/20',\n (isSelectionStart || isSelectionEnd) && 'bg-primary-500 text-white hover:bg-primary-500'\n )\n }\n />\n )}\n </CalendarGridBody>\n </CalendarGrid>\n <CalendarGrid offset={{ months: 1 }}>\n <CalendarGridHeader>\n {(day) => (\n <CalendarHeaderCell className=\"text-xs text-neutral-500 font-medium w-8 h-8\">{day}</CalendarHeaderCell>\n )}\n </CalendarGridHeader>\n <CalendarGridBody>\n {(date) => (\n <CalendarCell\n date={date}\n className={({ isSelected, isSelectionStart, isSelectionEnd, isOutsideMonth, isDisabled, isUnavailable }) =>\n twMerge(\n 'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center',\n 'hover:bg-neutral-100',\n isOutsideMonth && 'text-neutral-300',\n (isDisabled || isUnavailable) && 'text-neutral-300 cursor-not-allowed',\n isSelected && !isSelectionStart && !isSelectionEnd && 'bg-primary-500/20',\n (isSelectionStart || isSelectionEnd) && 'bg-primary-500 text-white hover:bg-primary-500'\n )\n }\n />\n )}\n </CalendarGridBody>\n </CalendarGrid>\n </div>\n </RangeCalendar>\n </I18nProvider>\n )\n}\n\nfunction ChevronLeft() {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" fill=\"currentColor\" className=\"w-4 h-4\">\n <path\n fillRule=\"evenodd\"\n d=\"M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z\"\n clipRule=\"evenodd\"\n />\n </svg>\n )\n}\n\nfunction ChevronRight() {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" fill=\"currentColor\" className=\"w-4 h-4\">\n <path\n fillRule=\"evenodd\"\n d=\"M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 1 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z\"\n clipRule=\"evenodd\"\n />\n </svg>\n )\n}\n"},{"name":"utils/use-query-state.ts","content":"// From @maestro-js/components\n// Install with: pnpm maestro components:add table --directory app\n\nimport { useLocation, useNavigation, useSearchParams as useSearchParamsRR } from 'react-router'\nimport { dateFns, dateTimeFns, durationFns, type Iso } from 'iso-fns'\nimport React from 'react'\nimport { useStableAccessor } from './use-stable-accessor'\n\n// ────────────────────────────────────────────────────────────────\n// Types\n// ────────────────────────────────────────────────────────────────\n\ninterface QueryStateNavigateOptions {\n replace?: boolean\n preventScrollReset?: boolean\n /** When true, updates the URL via `history.pushState`/`replaceState` without triggering a React Router navigation. */\n shallow?: boolean\n}\n\ninterface CodecDef<T> {\n name: string\n defaultValue?: T\n ['~type']: T\n /** When true, the param is removed from the URL when the value matches `defaultValue`. */\n clearOnDefault?: boolean\n}\n\ninterface Codec<Def extends CodecDef<any>> {\n _def: Def\n encode(value: Def['~type']): string | null\n decode(value: string | null): Def['~type']\n}\n\ninterface CodecConfigurator<Def extends CodecDef<any>> extends Codec<Def> {\n default(defaultValue: Def['~type']): CodecConfigurator<Def>\n clearOnDefault(clear: boolean): CodecConfigurator<Def>\n}\n\nfunction makeConfigurator<Def extends CodecDef<any>>(codec: Codec<Def>): CodecConfigurator<Def> {\n return {\n ...codec,\n default(defaultValue) {\n return makeConfigurator({ ...codec, _def: { ...codec._def, defaultValue } })\n },\n clearOnDefault(clear) {\n return makeConfigurator({ ...codec, _def: { ...codec._def, clearOnDefault: clear } })\n }\n }\n}\n\n// ────────────────────────────────────────────────────────────────\n// Codec defs\n// ────────────────────────────────────────────────────────────────\n\ninterface NoopDef extends CodecDef<string | null> {\n name: 'noop'\n ['~type']: string | null\n}\n\ninterface StringDef extends CodecDef<string | null> {\n name: 'string'\n ['~type']: string | null\n}\n\ninterface ValidatedDef<T extends string> extends CodecDef<T | null> {\n name: string\n ['~type']: T | null\n isValid: (v: string) => v is T\n}\n\ninterface RelativeDateTimeRangeDef extends CodecDef<Iso.Duration | { start: Iso.DateTime; end: Iso.DateTime } | null> {\n name: 'relativeDateTimeRange'\n ['~type']: Iso.Duration | { start: Iso.DateTime; end: Iso.DateTime } | null\n}\n\ninterface IntDef extends CodecDef<number | null> {\n name: 'int'\n ['~type']: number | null\n}\n\ninterface FloatDef extends CodecDef<number | null> {\n name: 'float'\n ['~type']: number | null\n}\n\ninterface HexDef extends CodecDef<number | null> {\n name: 'hex'\n ['~type']: number | null\n}\n\ninterface IndexDef extends CodecDef<number | null> {\n name: 'index'\n ['~type']: number | null\n}\n\ninterface BooleanDef extends CodecDef<boolean | null> {\n name: 'boolean'\n ['~type']: boolean | null\n}\n\ninterface EnumDef<T extends string> extends CodecDef<T | null> {\n name: 'enum'\n ['~type']: T | null\n values: readonly T[]\n}\n\ninterface EnumArrayDef<T extends string> extends CodecDef<T[]> {\n name: 'enumArray'\n ['~type']: T[]\n values: readonly T[]\n}\n\n// ────────────────────────────────────────────────────────────────\n// Codec factories\n// ────────────────────────────────────────────────────────────────\n\n/** Pass-through — no transformation, value is `string | null`. */\nfunction noop(def: Omit<NoopDef, '~type'>): Codec<NoopDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => value,\n decode: (value) => value\n }\n}\n\n/** String codec — pass-through with `string | null` type. */\nfunction string(def: Omit<StringDef, '~type'>): Codec<StringDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => value,\n decode: (value) => value\n }\n}\n\n/** Validates a string against a guard function. Used for date, dateTime, duration. */\nfunction validated<T extends string>(def: Omit<ValidatedDef<T>, '~type'>): Codec<ValidatedDef<T>> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => (value != null && def.isValid(value) ? value : null),\n decode: (value) => (value !== null && def.isValid(value) ? value : null)\n }\n}\n\n/**\n * Either an ISO duration (relative range) or a `start~end` pair of ISO\n * date-times (absolute range). Useful for \"last 7 days\" vs a custom window.\n */\nfunction relativeDateTimeRange(def: Omit<RelativeDateTimeRangeDef, '~type'>): Codec<RelativeDateTimeRangeDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => {\n if (value == null) return null\n if (durationFns.isValid(value)) return value\n if (typeof value === 'object' && 'start' in value && 'end' in value) {\n return dateTimeFns.isValid(value.start) && dateTimeFns.isValid(value.end) ? `${value.start}~${value.end}` : null\n }\n return null\n },\n decode: (value) => {\n if (value === null) return null\n if (durationFns.isValid(value)) return value\n const [start, end] = value.split('~')\n return dateTimeFns.isValid(start) && dateTimeFns.isValid(end) ? { start, end } : null\n }\n }\n}\n\n/** Integer (base-10). Non-integer or missing values decode to `null`. */\nfunction int(def: Omit<IntDef, '~type'>): Codec<IntDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => (value != null && Number.isInteger(value) ? value.toString() : null),\n decode: (value) => {\n if (value === null) return null\n const num = Number(value)\n return Number.isInteger(num) ? num : null\n }\n }\n}\n\n/** Finite floating-point number. `NaN` / `Infinity` decode to `null`. */\nfunction float(def: Omit<FloatDef, '~type'>): Codec<FloatDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => (value != null && Number.isFinite(value) ? value.toString() : null),\n decode: (value) => {\n if (value === null) return null\n const num = Number(value)\n return Number.isFinite(num) ? num : null\n }\n }\n}\n\n/** Hexadecimal integer (with or without `0x` prefix). Stored without prefix. */\nfunction hex(def: Omit<HexDef, '~type'>): Codec<HexDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => (typeof value === 'number' && Number.isFinite(value) ? value.toString(16) : null),\n decode: (value) => {\n if (value === null) return null\n const body = value.trim().toLowerCase().replace(/^0x/, '')\n if (body.length === 0 || /[^0-9a-f]/i.test(body)) return null\n const num = parseInt(body, 16)\n return Number.isFinite(num) ? num : null\n }\n }\n}\n\n/**\n * Zero-based index stored as a **1-based** number in the URL.\n * `?page=1` decodes to `0`, `0` encodes to `\"1\"`.\n */\nfunction index(def: Omit<IndexDef, '~type'>): Codec<IndexDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => (typeof value === 'number' && Number.isInteger(value) && value >= 0 ? (value + 1).toString() : null),\n decode: (value) => {\n if (value === null) return null\n const num = Number(value)\n return Number.isInteger(num) && num >= 1 ? num - 1 : null\n }\n }\n}\n\n/** Boolean accepting common truthy/falsy strings (`1`, `true`, `yes`, `on`, etc). Encodes as `\"1\"` / `\"0\"`. */\nfunction boolean(def: Omit<BooleanDef, '~type'>): Codec<BooleanDef> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => (value == null ? null : value ? '1' : '0'),\n decode: (value) => {\n if (value === null) return null\n const v = value.trim().toLowerCase()\n if (v === '1' || v === 'true' || v === 't' || v === 'yes' || v === 'y' || v === 'on') return true\n if (v === '0' || v === 'false' || v === 'f' || v === 'no' || v === 'n' || v === 'off') return false\n return null\n }\n }\n}\n\n/** Constrained to one of the provided `values`. Anything else decodes to `null`. */\nfunction enumCodec<T extends string>(def: Omit<EnumDef<T>, '~type'>): Codec<EnumDef<T>> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => value ?? null,\n decode: (value) => {\n if (value === null) return null\n return (def.values as readonly string[]).includes(value) ? (value as T) : null\n }\n }\n}\n\n/**\n * Comma-separated list constrained to the provided `values`.\n * Decodes to `T[]` (empty array when missing). Encodes in the canonical\n * order of `values` so the same selection always produces the same URL.\n */\nfunction enumArray<T extends string>(def: Omit<EnumArrayDef<T>, '~type'>): Codec<EnumArrayDef<T>> {\n return {\n _def: { ...def, ['~type']: null as any },\n encode: (value) => {\n if (!value.length) return null\n return value\n .sort((v1, v2) => def.values.indexOf(v1) - def.values.indexOf(v2))\n .map((v) => encodeURIComponent(v))\n .join(',')\n },\n decode: (value) => {\n if (value === null) return []\n return value\n .split(',')\n .map((v) => decodeURIComponent(v))\n .filter((v) => def.values.includes(v as T)) as T[]\n }\n }\n}\n\n// ────────────────────────────────────────────────────────────────\n// Serialize / Deserialize\n// ────────────────────────────────────────────────────────────────\n\n/** Extract the serializable def from a codec. */\nfunction serializeCodec<Def extends CodecDef<any>>(codec: Codec<Def>): Def {\n return codec._def\n}\n\ntype CodecFactory = (def: any) => Codec<any>\n\nconst defaultFactories: Record<string, CodecFactory> = {\n noop,\n string,\n date: validated,\n dateTime: validated,\n duration: validated,\n relativeDateTimeRange,\n int,\n float,\n hex,\n index,\n boolean,\n enum: enumCodec,\n enumArray\n}\n\n/** Reconstruct a codec from a serialized def. */\nfunction deserializeCodec<Def extends CodecDef<any>>(\n def: Def,\n factories: Record<string, CodecFactory> = defaultFactories\n): Codec<Def> {\n const factory = factories[def.name]\n if (!factory) throw new Error(`Unknown codec: ${def.name}`)\n return factory(def) as Codec<Def>\n}\n\ntype SerializedCodec<C extends Codec<CodecDef<any>>> = C['_def']\n\ntype SerializedLoader<L extends QueryStateLoader> = { [K in keyof L]: SerializedCodec<L[K]> }\n\n/** Serialize a loader into a plain object of defs, keyed by param name. */\nfunction serializeLoader<L extends QueryStateLoader>(loader: L): SerializedLoader<L> {\n return Object.fromEntries(\n Object.entries(loader).map(([name, codec]) => [name, serializeCodec(codec)])\n ) as SerializedLoader<L>\n}\n\n/** Reconstruct a loader from a serialized object of defs. */\nfunction deserializeLoader<L extends QueryStateLoader>(\n defs: SerializedLoader<L>,\n factories: Record<string, CodecFactory> = defaultFactories\n): L {\n return Object.fromEntries(\n Object.entries(defs).map(([name, def]) => [name, deserializeCodec(def as CodecDef<any>, factories)])\n ) as L\n}\n\n// ────────────────────────────────────────────────────────────────\n// Built-in codecs\n// ────────────────────────────────────────────────────────────────\n\n/**\n * Library of built-in codecs for common URL search-param types.\n *\n * Each codec converts between a URL string and a typed value, returning `null`\n * for missing or invalid params. Chain `.default(v)` to substitute a\n * fallback, or `.clearOnDefault(true)` to remove the param when it matches the default.\n */\nconst codecs = {\n /** Pass-through — no transformation, value is `string | null`. */\n noop: () => makeConfigurator(noop({ name: 'noop' })),\n\n /** String codec — pass-through defaulting to `''` (never null). */\n string: () => makeConfigurator(string({ name: 'string' })).default(''),\n\n /** ISO date (`YYYY-MM-DD`). Invalid strings decode to `null`. */\n date: () => makeConfigurator(validated({ name: 'date', isValid: dateFns.isValid })),\n\n /** ISO date-time (`YYYY-MM-DDTHH:mm:ss`). Invalid strings decode to `null`. */\n dateTime: () => makeConfigurator(validated({ name: 'dateTime', isValid: dateTimeFns.isValid })),\n\n /** ISO duration (`P1DT2H`, etc). Invalid strings decode to `null`. */\n duration: () => makeConfigurator(validated({ name: 'duration', isValid: durationFns.isValid })),\n\n /**\n * Either an ISO duration (relative range) or a `start~end` pair of ISO\n * date-times (absolute range). Useful for \"last 7 days\" vs a custom window.\n */\n relativeDateTimeRange: () => makeConfigurator(relativeDateTimeRange({ name: 'relativeDateTimeRange' })),\n\n /** Integer (base-10). Non-integer or missing values decode to `null`. */\n int: () => makeConfigurator(int({ name: 'int' })),\n\n /** Finite floating-point number. `NaN` / `Infinity` decode to `null`. */\n float: () => makeConfigurator(float({ name: 'float' })),\n\n /** Hexadecimal integer (with or without `0x` prefix). Stored without prefix. */\n hex: () => makeConfigurator(hex({ name: 'hex' })),\n\n /**\n * Zero-based index stored as a **1-based** number in the URL.\n * `?page=1` decodes to `0`, `0` encodes to `\"1\"`.\n */\n index: () => makeConfigurator(index({ name: 'index' })),\n\n /** Boolean accepting common truthy/falsy strings (`1`, `true`, `yes`, `on`, etc). Encodes as `\"1\"` / `\"0\"`. */\n boolean: () => makeConfigurator(boolean({ name: 'boolean' })),\n\n /** Constrained to one of the provided `values`. Anything else decodes to `null`. */\n enum: <T extends string>(values: readonly T[]) => makeConfigurator(enumCodec({ name: 'enum', values })),\n\n /**\n * Comma-separated list constrained to the provided `values`.\n * Decodes to `T[]` (empty array when missing). Encodes in the canonical\n * order of `values` so the same selection always produces the same URL.\n */\n enumArray: <T extends string>(values: readonly T[]) => makeConfigurator(enumArray({ name: 'enumArray', values }))\n}\n\n// ────────────────────────────────────────────────────────────────\n// Loader types\n// ────────────────────────────────────────────────────────────────\n\n/** A map of param names to codecs, defining the shape of search-param state. */\ntype QueryStateLoader<T extends { [key: string]: any } = { [key: string]: any }> = {\n [key in keyof T]: Codec<CodecDef<T[key]>>\n}\n\ntype ExtractTypeFromLoader<L extends QueryStateLoader> = {\n [key in keyof L]: L[key] extends Codec<infer S> ? S['~type'] : never\n}\n\n// ────────────────────────────────────────────────────────────────\n// Internal helpers\n// ────────────────────────────────────────────────────────────────\n\nfunction getQueryParam<T>(naturalValue: string | null, codec: Codec<CodecDef<T>>): T {\n try {\n const decoded = codec.decode(naturalValue)\n return (decoded ?? codec?._def?.defaultValue ?? null) as T\n } catch {\n return (codec?._def?.defaultValue ?? null) as T\n }\n}\n\nfunction setQueryParam<V>({\n searchParams,\n name,\n value,\n codec\n}: {\n searchParams: URLSearchParams\n name: string\n value: V\n codec: Codec<CodecDef<V>>\n}): void {\n const encoded = codec.encode(value)\n if (\n encoded === null ||\n (codec._def?.defaultValue != null && codec._def?.clearOnDefault && encoded === codec.encode(codec._def?.defaultValue))\n ) {\n searchParams.delete(name)\n } else {\n searchParams.set(name, encoded)\n }\n}\n\nfunction queryParamsToObject<L extends QueryStateLoader>(params: URLSearchParams, loader: L) {\n return Object.fromEntries(\n Object.entries(loader).map(([name, codec]) => [name, getQueryParam(params.get(name), codec)])\n ) as ExtractTypeFromLoader<L>\n}\n\n// ────────────────────────────────────────────────────────────────\n// Public API\n// ────────────────────────────────────────────────────────────────\n\n/**\n * Decode search params from various sources (URL string, `Request`, `URLSearchParams`, or\n * an object with a `.search` property) using a loader. Useful in loaders/actions on the server side.\n */\nfunction getSearchParamsState<L extends QueryStateLoader>(\n searchParams: URLSearchParams | string | Request | { search: string },\n loader: L\n) {\n if (typeof searchParams === 'string') {\n return queryParamsToObject(new URL(searchParams).searchParams, loader)\n } else if ('url' in searchParams) {\n return queryParamsToObject(new URL(searchParams.url).searchParams, loader)\n } else if ('search' in searchParams) {\n return queryParamsToObject(new URLSearchParams(searchParams.search), loader)\n } else {\n return queryParamsToObject(searchParams, loader)\n }\n}\n\n/**\n * React hook that binds a single search param to component state via a codec.\n * Returns `[value, setValue]` like `React.useState`. The setter accepts a value\n * or updater function and optional {@link QueryStateNavigateOptions}.\n *\n * Reads from the pending navigation location when one is in flight so the UI\n * reflects optimistic state during transitions.\n */\nfunction useState<S>(name: string, codec: Codec<CodecDef<S>>): [S, (val: S | ((prevState: S) => S), navigateOpts?: QueryStateNavigateOptions) => void] {\n const [searchParamsRaw, setSearchParams] = useSearchParamsRR()\n const navigation = useNavigation()\n\n const naturalValue = React.useMemo(\n () => (navigation.location?.search ? new URLSearchParams(navigation.location.search) : searchParamsRaw).get(name),\n [name, searchParamsRaw, navigation.location]\n )\n const value = React.useMemo(\n () => getQueryParam<S>(naturalValue, codec),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [naturalValue]\n )\n\n const setValue = React.useCallback(\n (val: S | ((prevState: S) => S), navigateOpts?: QueryStateNavigateOptions) => {\n const { replace = true, preventScrollReset = false, shallow = false } = { ...codec, ...navigateOpts }\n\n function updateParams(params: URLSearchParams) {\n const newParams = new URLSearchParams(params)\n const oldValue = getQueryParam(newParams.get(name), codec)\n const newValue = typeof val === 'function' ? (val as (prevState: S) => S)(oldValue) : val\n setQueryParam({ searchParams: newParams, name, value: newValue, codec })\n return newParams\n }\n if (shallow) {\n const params = new URLSearchParams(window.location.search)\n const newUrl = [\n [window.location.pathname, updateParams(params).toString()].filter(Boolean).join('?'),\n window.location.hash\n ].join('#')\n if (replace) {\n window.history.replaceState(null, '', newUrl)\n } else {\n window.history.pushState(null, '', newUrl)\n }\n } else {\n setSearchParams(updateParams, { replace, preventScrollReset })\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [name, setSearchParams]\n )\n\n return [value, setValue] as const\n}\n\n/**\n * React hook that decodes *all* search params defined by a loader into a typed object.\n * Read-only — for read/write access to individual params, use {@link useState}.\n *\n * Reads from the pending navigation location when one is in flight.\n */\nfunction useSearchParams<L extends QueryStateLoader>(\n loader: L\n): [\n state: ExtractTypeFromLoader<L>,\n setState: (\n val: ExtractTypeFromLoader<L> | ((prevState: ExtractTypeFromLoader<L>) => ExtractTypeFromLoader<L>),\n navigateOpts?: QueryStateNavigateOptions\n ) => void\n] {\n const location = useLocation()\n const { location: pendingLocation } = useNavigation()\n\n const search = pendingLocation?.search ?? location.search\n\n const getLoader = useStableAccessor(loader)\n const [_, setSearchParamsRaw] = useSearchParamsRR()\n\n const setSearchParam = React.useCallback(\n (\n val: ExtractTypeFromLoader<L> | ((prevState: ExtractTypeFromLoader<L>) => ExtractTypeFromLoader<L>),\n navigateOpts?: QueryStateNavigateOptions\n ) => {\n const loader = getLoader()\n const { replace = true, preventScrollReset = false, shallow = false } = { ...navigateOpts }\n\n function updateParams(params: URLSearchParams) {\n const newParams = new URLSearchParams(params)\n const oldValue = queryParamsToObject(new URLSearchParams(search), loader)\n const newValue = typeof val === 'function' ? val(oldValue) : val\n Object.entries(loader).forEach(([name, codec]) =>\n setQueryParam({ searchParams: newParams, name, value: newValue[name], codec })\n )\n return newParams\n }\n if (shallow) {\n const params = new URLSearchParams(window.location.search)\n const newUrl = [\n [window.location.pathname, updateParams(params).toString()].filter(Boolean).join('?'),\n window.location.hash\n ].join('#')\n if (replace) {\n window.history.replaceState(null, '', newUrl)\n } else {\n window.history.pushState(null, '', newUrl)\n }\n } else {\n setSearchParamsRaw(updateParams, { replace, preventScrollReset })\n }\n },\n [getLoader, setSearchParamsRaw]\n )\n\n const searchParams = React.useMemo(() => queryParamsToObject(new URLSearchParams(search), loader), [search, loader])\n\n return [searchParams, setSearchParam]\n}\n\n/**\n * Identity function that provides type inference for a loader definition.\n *\n * @example\n * ```ts\n * const loader = SearchParams.createLoader({\n * page: SearchParams.codecs.int.withDefaultValue(0),\n * sort: SearchParams.codecs.enum(['name', 'date'] as const)\n * })\n * ```\n */\nfunction createLoader<L extends QueryStateLoader>(loader: L): L {\n return loader\n}\n\nexport const SearchParams = {\n codecs,\n serializeCodec,\n deserializeCodec,\n serializeLoader,\n deserializeLoader,\n createLoader,\n getSearchParamsState,\n useState,\n useSearchParams\n}\n\nexport declare namespace SearchParams {\n export {\n Codec,\n CodecDef,\n QueryStateLoader,\n ExtractTypeFromLoader,\n QueryStateNavigateOptions,\n SerializedCodec,\n SerializedLoader\n }\n}\n"}]}]