@seekalfred/primitives 0.6.0 → 0.7.1
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.
- package/README.md +19 -11
- package/dist/activity-heatmap/activity-heatmap.d.ts +92 -0
- package/dist/activity-heatmap/activity-heatmap.js +699 -0
- package/dist/activity-heatmap/activity-heatmap.js.map +1 -0
- package/dist/activity-heatmap/index.d.ts +2 -0
- package/dist/activity-heatmap/index.js +2 -0
- package/dist/alert/alert.d.ts +5 -1
- package/dist/alert/alert.js +68 -7
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert-dialog/alert-dialog.d.ts +45 -18
- package/dist/alert-dialog/alert-dialog.js +128 -9
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/alert-dialog/index.d.ts +2 -2
- package/dist/alert-dialog/index.js +2 -2
- package/dist/animated-counter/animated-counter.d.ts +38 -0
- package/dist/animated-counter/animated-counter.js +230 -0
- package/dist/animated-counter/animated-counter.js.map +1 -0
- package/dist/animated-counter/index.d.ts +2 -0
- package/dist/animated-counter/index.js +2 -0
- package/dist/announcement-bar/announcement-bar.d.ts +67 -0
- package/dist/announcement-bar/announcement-bar.js +364 -0
- package/dist/announcement-bar/announcement-bar.js.map +1 -0
- package/dist/announcement-bar/index.d.ts +2 -0
- package/dist/announcement-bar/index.js +2 -0
- package/dist/badge/badge.d.ts +1 -1
- package/dist/bar-chart/bar-chart.d.ts +51 -0
- package/dist/bar-chart/bar-chart.js +565 -0
- package/dist/bar-chart/bar-chart.js.map +1 -0
- package/dist/bar-chart/index.d.ts +2 -0
- package/dist/bar-chart/index.js +2 -0
- package/dist/breadcrumb/breadcrumb.d.ts +2 -2
- package/dist/breadcrumb/breadcrumb.js +12 -3
- package/dist/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/brush-chart/brush-chart.d.ts +64 -0
- package/dist/brush-chart/brush-chart.js +1056 -0
- package/dist/brush-chart/brush-chart.js.map +1 -0
- package/dist/brush-chart/index.d.ts +2 -0
- package/dist/brush-chart/index.js +2 -0
- package/dist/button/button.d.ts +2 -2
- package/dist/button/button.js +31 -21
- package/dist/button/button.js.map +1 -1
- package/dist/button-group/button-group.js +3 -4
- package/dist/button-group/button-group.js.map +1 -1
- package/dist/card/card.js +1 -1
- package/dist/card/card.js.map +1 -1
- package/dist/card-stack/card-stack.d.ts +54 -0
- package/dist/card-stack/card-stack.js +492 -0
- package/dist/card-stack/card-stack.js.map +1 -0
- package/dist/card-stack/index.d.ts +2 -0
- package/dist/card-stack/index.js +2 -0
- package/dist/carousel/carousel.d.ts +56 -0
- package/dist/carousel/carousel.js +389 -0
- package/dist/carousel/carousel.js.map +1 -0
- package/dist/carousel/index.d.ts +2 -0
- package/dist/carousel/index.js +2 -0
- package/dist/checkbox/checkbox.js +4 -4
- package/dist/checkbox/checkbox.js.map +1 -1
- package/dist/chip-group/chip-group.d.ts +37 -0
- package/dist/chip-group/chip-group.js +148 -0
- package/dist/chip-group/chip-group.js.map +1 -0
- package/dist/chip-group/index.d.ts +2 -0
- package/dist/chip-group/index.js +2 -0
- package/dist/countdown/countdown.d.ts +45 -0
- package/dist/countdown/countdown.js +298 -0
- package/dist/countdown/countdown.js.map +1 -0
- package/dist/countdown/index.d.ts +2 -0
- package/dist/countdown/index.js +2 -0
- package/dist/date-picker/picker-parts.js +1 -1
- package/dist/date-picker/picker-parts.js.map +1 -1
- package/dist/dialog/dialog.js +1 -1
- package/dist/dialog/dialog.js.map +1 -1
- package/dist/donut-chart/donut-chart.d.ts +64 -0
- package/dist/donut-chart/donut-chart.js +576 -0
- package/dist/donut-chart/donut-chart.js.map +1 -0
- package/dist/donut-chart/index.d.ts +2 -0
- package/dist/donut-chart/index.js +2 -0
- package/dist/empty-state/empty-state.d.ts +21 -10
- package/dist/empty-state/empty-state.js +32 -26
- package/dist/empty-state/empty-state.js.map +1 -1
- package/dist/empty-state/index.d.ts +2 -2
- package/dist/empty-state/index.js +2 -2
- package/dist/file-drop-zone/file-drop-zone.js +1 -1
- package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
- package/dist/filter-bar/filter-bar.d.ts +47 -26
- package/dist/filter-bar/filter-bar.js +272 -156
- package/dist/filter-bar/filter-bar.js.map +1 -1
- package/dist/gauge/gauge.d.ts +45 -0
- package/dist/gauge/gauge.js +219 -0
- package/dist/gauge/gauge.js.map +1 -0
- package/dist/gauge/index.d.ts +2 -0
- package/dist/gauge/index.js +2 -0
- package/dist/in-view-title/in-view-title.d.ts +33 -0
- package/dist/in-view-title/in-view-title.js +119 -0
- package/dist/in-view-title/in-view-title.js.map +1 -0
- package/dist/in-view-title/index.d.ts +2 -0
- package/dist/in-view-title/index.js +2 -0
- package/dist/index.d.ts +49 -6
- package/dist/index.js +48 -8
- package/dist/lib/cn.js +11 -1
- package/dist/lib/cn.js.map +1 -1
- package/dist/lib/field-message.js +28 -10
- package/dist/lib/field-message.js.map +1 -1
- package/dist/lib/field-surface.js +9 -8
- package/dist/lib/field-surface.js.map +1 -1
- package/dist/lib/flag-emoji.js +50 -0
- package/dist/lib/flag-emoji.js.map +1 -0
- package/dist/lib/flip.js +11 -3
- package/dist/lib/flip.js.map +1 -1
- package/dist/lib/menu-highlight.js +64 -0
- package/dist/lib/menu-highlight.js.map +1 -0
- package/dist/lib/motion.js +9 -2
- package/dist/lib/motion.js.map +1 -1
- package/dist/lib/rolling-value.js +1 -1
- package/dist/lib/rolling-value.js.map +1 -1
- package/dist/lib/spring-tween.js +500 -0
- package/dist/lib/spring-tween.js.map +1 -0
- package/dist/lib/swap-text.js +3 -1
- package/dist/lib/swap-text.js.map +1 -1
- package/dist/line-chart/index.d.ts +2 -0
- package/dist/line-chart/index.js +2 -0
- package/dist/line-chart/line-chart.d.ts +76 -0
- package/dist/line-chart/line-chart.js +611 -0
- package/dist/line-chart/line-chart.js.map +1 -0
- package/dist/mention-input/index.d.ts +2 -0
- package/dist/mention-input/index.js +2 -0
- package/dist/mention-input/mention-input.d.ts +98 -0
- package/dist/mention-input/mention-input.js +576 -0
- package/dist/mention-input/mention-input.js.map +1 -0
- package/dist/menu/menu.d.ts +2 -0
- package/dist/menu/menu.js +23 -12
- package/dist/menu/menu.js.map +1 -1
- package/dist/money-input/index.d.ts +2 -0
- package/dist/money-input/index.js +2 -0
- package/dist/money-input/money-input.d.ts +65 -0
- package/dist/money-input/money-input.js +607 -0
- package/dist/money-input/money-input.js.map +1 -0
- package/dist/morph-select/index.d.ts +2 -0
- package/dist/morph-select/index.js +2 -0
- package/dist/morph-select/morph-select.d.ts +71 -0
- package/dist/morph-select/morph-select.js +693 -0
- package/dist/morph-select/morph-select.js.map +1 -0
- package/dist/multi-select/index.d.ts +2 -0
- package/dist/multi-select/index.js +2 -0
- package/dist/multi-select/multi-select.d.ts +36 -0
- package/dist/multi-select/multi-select.js +152 -0
- package/dist/multi-select/multi-select.js.map +1 -0
- package/dist/number-field/index.d.ts +2 -0
- package/dist/number-field/index.js +2 -0
- package/dist/number-field/number-field.d.ts +42 -0
- package/dist/number-field/number-field.js +199 -0
- package/dist/number-field/number-field.js.map +1 -0
- package/dist/onboarding-checklist/index.d.ts +2 -0
- package/dist/onboarding-checklist/index.js +2 -0
- package/dist/onboarding-checklist/onboarding-checklist.d.ts +55 -0
- package/dist/onboarding-checklist/onboarding-checklist.js +577 -0
- package/dist/onboarding-checklist/onboarding-checklist.js.map +1 -0
- package/dist/password-field/password-field.js +2 -2
- package/dist/password-field/password-field.js.map +1 -1
- package/dist/phone-input/index.d.ts +3 -0
- package/dist/phone-input/index.js +3 -0
- package/dist/phone-input/phone-countries.d.ts +29 -0
- package/dist/phone-input/phone-countries.js +170 -0
- package/dist/phone-input/phone-countries.js.map +1 -0
- package/dist/phone-input/phone-input.d.ts +66 -0
- package/dist/phone-input/phone-input.js +440 -0
- package/dist/phone-input/phone-input.js.map +1 -0
- package/dist/popover/popover.d.ts +12 -0
- package/dist/popover/popover.js +31 -7
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/progress.js +1 -1
- package/dist/radio-group/radio-group.js +1 -1
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/reactions/index.d.ts +2 -0
- package/dist/reactions/index.js +2 -0
- package/dist/reactions/reactions.d.ts +50 -0
- package/dist/reactions/reactions.js +315 -0
- package/dist/reactions/reactions.js.map +1 -0
- package/dist/rich-text-editor/index.d.ts +3 -0
- package/dist/rich-text-editor/index.js +2 -0
- package/dist/rich-text-editor/rich-text-editor.d.ts +70 -0
- package/dist/rich-text-editor/rich-text-editor.js +909 -0
- package/dist/rich-text-editor/rich-text-editor.js.map +1 -0
- package/dist/rich-text-editor/rich-text-engine.d.ts +16 -0
- package/dist/rich-text-editor/rich-text-engine.js +366 -0
- package/dist/rich-text-editor/rich-text-engine.js.map +1 -0
- package/dist/rich-text-editor/rich-text-html.js +174 -0
- package/dist/rich-text-editor/rich-text-html.js.map +1 -0
- package/dist/rich-text-editor/rich-text-icons.js +43 -0
- package/dist/rich-text-editor/rich-text-icons.js.map +1 -0
- package/dist/ridgeline/index.d.ts +2 -0
- package/dist/ridgeline/index.js +2 -0
- package/dist/ridgeline/ridgeline.d.ts +55 -0
- package/dist/ridgeline/ridgeline.js +701 -0
- package/dist/ridgeline/ridgeline.js.map +1 -0
- package/dist/scroll-area/index.d.ts +2 -2
- package/dist/scroll-area/index.js +2 -2
- package/dist/scroll-area/scroll-area.d.ts +68 -31
- package/dist/scroll-area/scroll-area.js +208 -59
- package/dist/scroll-area/scroll-area.js.map +1 -1
- package/dist/scrub-input/evaluate.d.ts +14 -0
- package/dist/scrub-input/evaluate.js +72 -0
- package/dist/scrub-input/evaluate.js.map +1 -0
- package/dist/scrub-input/index.d.ts +3 -0
- package/dist/scrub-input/index.js +3 -0
- package/dist/scrub-input/scrub-input.d.ts +74 -0
- package/dist/scrub-input/scrub-input.js +486 -0
- package/dist/scrub-input/scrub-input.js.map +1 -0
- package/dist/search-field/search-field.d.ts +2 -1
- package/dist/search-field/search-field.js +4 -4
- package/dist/search-field/search-field.js.map +1 -1
- package/dist/select/select.d.ts +3 -1
- package/dist/select/select.js +20 -8
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +1 -1
- package/dist/sheet/sheet.js +2 -1
- package/dist/sheet/sheet.js.map +1 -1
- package/dist/shortcut-recorder/index.d.ts +3 -0
- package/dist/shortcut-recorder/index.js +3 -0
- package/dist/shortcut-recorder/shortcut-model.d.ts +27 -0
- package/dist/shortcut-recorder/shortcut-model.js +206 -0
- package/dist/shortcut-recorder/shortcut-model.js.map +1 -0
- package/dist/shortcut-recorder/shortcut-recorder.d.ts +93 -0
- package/dist/shortcut-recorder/shortcut-recorder.js +489 -0
- package/dist/shortcut-recorder/shortcut-recorder.js.map +1 -0
- package/dist/slider/index.d.ts +2 -2
- package/dist/slider/slider.d.ts +46 -14
- package/dist/slider/slider.js +151 -67
- package/dist/slider/slider.js.map +1 -1
- package/dist/slope-chart/index.d.ts +2 -0
- package/dist/slope-chart/index.js +2 -0
- package/dist/slope-chart/slope-chart.d.ts +54 -0
- package/dist/slope-chart/slope-chart.js +568 -0
- package/dist/slope-chart/slope-chart.js.map +1 -0
- package/dist/sparkline/index.d.ts +2 -0
- package/dist/sparkline/index.js +2 -0
- package/dist/sparkline/sparkline.d.ts +46 -0
- package/dist/sparkline/sparkline.js +390 -0
- package/dist/sparkline/sparkline.js.map +1 -0
- package/dist/split-bar/index.d.ts +2 -0
- package/dist/split-bar/index.js +2 -0
- package/dist/split-bar/split-bar.d.ts +62 -0
- package/dist/split-bar/split-bar.js +513 -0
- package/dist/split-bar/split-bar.js.map +1 -0
- package/dist/split-button/split-button.d.ts +1 -1
- package/dist/split-button/split-button.js +12 -11
- package/dist/split-button/split-button.js.map +1 -1
- package/dist/stat-card/index.d.ts +2 -2
- package/dist/stat-card/index.js +2 -2
- package/dist/stat-card/stat-card.d.ts +24 -4
- package/dist/stat-card/stat-card.js +114 -17
- package/dist/stat-card/stat-card.js.map +1 -1
- package/dist/stepper/index.d.ts +2 -0
- package/dist/stepper/index.js +2 -0
- package/dist/stepper/stepper.d.ts +51 -0
- package/dist/stepper/stepper.js +215 -0
- package/dist/stepper/stepper.js.map +1 -0
- package/dist/streamgraph/index.d.ts +2 -0
- package/dist/streamgraph/index.js +2 -0
- package/dist/streamgraph/streamgraph.d.ts +70 -0
- package/dist/streamgraph/streamgraph.js +613 -0
- package/dist/streamgraph/streamgraph.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/swipe-actions/index.d.ts +2 -0
- package/dist/swipe-actions/index.js +2 -0
- package/dist/swipe-actions/swipe-actions.d.ts +67 -0
- package/dist/swipe-actions/swipe-actions.js +552 -0
- package/dist/swipe-actions/swipe-actions.js.map +1 -0
- package/dist/switch/switch.js +2 -2
- package/dist/switch/switch.js.map +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +2 -2
- package/dist/table/table.d.ts +56 -4
- package/dist/table/table.js +340 -165
- package/dist/table/table.js.map +1 -1
- package/dist/tabs/tabs.js +2 -2
- package/dist/tabs/tabs.js.map +1 -1
- package/dist/tag-input/index.d.ts +2 -0
- package/dist/tag-input/index.js +2 -0
- package/dist/tag-input/tag-input.d.ts +40 -0
- package/dist/tag-input/tag-input.js +269 -0
- package/dist/tag-input/tag-input.js.map +1 -0
- package/dist/text-morph/index.d.ts +2 -0
- package/dist/text-morph/index.js +2 -0
- package/dist/text-morph/text-morph.d.ts +23 -0
- package/dist/text-morph/text-morph.js +21 -0
- package/dist/text-morph/text-morph.js.map +1 -0
- package/dist/text-reveal/index.d.ts +2 -0
- package/dist/text-reveal/index.js +2 -0
- package/dist/text-reveal/text-reveal.d.ts +27 -0
- package/dist/text-reveal/text-reveal.js +51 -0
- package/dist/text-reveal/text-reveal.js.map +1 -0
- package/dist/text-shimmer/index.d.ts +2 -0
- package/dist/text-shimmer/index.js +2 -0
- package/dist/text-shimmer/text-shimmer.d.ts +29 -0
- package/dist/text-shimmer/text-shimmer.js +29 -0
- package/dist/text-shimmer/text-shimmer.js.map +1 -0
- package/dist/time-picker/time-picker.js +2 -2
- package/dist/time-picker/time-picker.js.map +1 -1
- package/dist/toast/index.d.ts +2 -2
- package/dist/toast/index.js +2 -2
- package/dist/toast/toast.d.ts +6 -0
- package/dist/toast/toast.js +82 -40
- package/dist/toast/toast.js.map +1 -1
- package/dist/tooltip/tooltip.d.ts +9 -2
- package/dist/tooltip/tooltip.js +7 -7
- package/dist/tooltip/tooltip.js.map +1 -1
- package/dist/treemap/index.d.ts +2 -0
- package/dist/treemap/index.js +2 -0
- package/dist/treemap/treemap.d.ts +53 -0
- package/dist/treemap/treemap.js +699 -0
- package/dist/treemap/treemap.js.map +1 -0
- package/dist/usage-meter/index.d.ts +2 -0
- package/dist/usage-meter/index.js +2 -0
- package/dist/usage-meter/usage-meter.d.ts +50 -0
- package/dist/usage-meter/usage-meter.js +440 -0
- package/dist/usage-meter/usage-meter.js.map +1 -0
- package/dist/waffle-chart/index.d.ts +2 -0
- package/dist/waffle-chart/index.js +2 -0
- package/dist/waffle-chart/waffle-chart.d.ts +57 -0
- package/dist/waffle-chart/waffle-chart.js +426 -0
- package/dist/waffle-chart/waffle-chart.js.map +1 -0
- package/package.json +165 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"morph-select.js","names":[],"sources":["../../src/morph-select/morph-select.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type FocusEvent,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\n\nimport { labelVariants } from \"../label/label.js\";\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport {\n prefersReducedMotion,\n useExitBackstop,\n useIsomorphicLayoutEffect,\n useLeavingItems,\n type Rect,\n} from \"../lib/motion.js\";\n\n/** One choice. */\nexport interface MorphSelectOption {\n value: string;\n label: string;\n /** A 20px mark shown before the label, in the list and in the trigger. An avatar or a plain icon. */\n icon?: ReactNode;\n /** Short trailing detail such as an offset or a count. Rendered with tabular numerals. */\n meta?: string;\n /** Extra words that match this option when searching. */\n keywords?: string;\n disabled?: boolean;\n}\n\n/** A labelled run of options. */\nexport interface MorphSelectGroup {\n label: string;\n options: MorphSelectOption[];\n}\n\nexport type MorphSelectItem = MorphSelectOption | MorphSelectGroup;\n\n/**\n * MorphSelect: a select whose trigger is the list. Opening grows the trigger itself\n * into the options surface (its width, height and corners spring from the closed\n * control to the open panel), a highlight glides between options, and the chosen\n * option's label lifts out of the list and settles into the trigger while the trigger\n * springs to its new width. Supports groups, type-ahead, search for long lists, and the\n * full listbox keyboard model: arrows, Home, End, PageUp, PageDown, Enter, Space,\n * Escape and Alt+ArrowUp.\n *\n * Use it for compact property pickers and form fields with up to a few hundred options.\n * Prefer a combobox when people mostly type free text, and Select when the list should\n * float over the page instead of growing from the trigger.\n *\n * It is its own listbox rather than a React Aria Select: a Select's list lives in a\n * popover, a separate surface that cannot be the trigger.\n */\nexport interface MorphSelectProps {\n /** Visible label, also the accessible name of the trigger and the list. */\n label: string;\n /** Keeps the label for assistive technology only. */\n labelHidden?: boolean;\n items: MorphSelectItem[];\n /** The chosen value. Controlled when given; `null` is nothing chosen. */\n value?: string | null;\n defaultValue?: string | null;\n onValueChange?: (value: string, option: MorphSelectOption) => void;\n /** Shown while nothing is chosen. @default \"Select\" */\n placeholder?: string;\n /** Shows a search field in place of the trigger when open. \"auto\" turns it on above eight options. @default \"auto\" */\n searchable?: boolean | \"auto\";\n searchPlaceholder?: string;\n /** Width of the open surface in px. It never gets narrower than the trigger. @default 272 */\n panelWidth?: number;\n /** Tallest the option list gets before it scrolls, in px. @default 296 */\n maxListHeight?: number;\n /** Which edge of the trigger stays put while the surface grows. @default \"start\" */\n align?: \"start\" | \"end\";\n /**\n * Control scale: the same heights as Select and Input.\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Adds a hidden input for native form submission. */\n name?: string;\n disabled?: boolean;\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\ninterface Section {\n key: string;\n label?: string;\n options: MorphSelectOption[];\n}\ninterface Change {\n value: string | null;\n dir: number;\n fly: { x: number; y: number } | null;\n}\n\nconst TYPEAHEAD_RESET = 600;\nconst PAGE = 8;\n\nconst isGroup = (item: MorphSelectItem): item is MorphSelectGroup => \"options\" in item;\n\n/** Loose options between groups gather into unlabelled sections, so the list keeps the order it was given. */\nfunction normalize(items: MorphSelectItem[]): Section[] {\n const out: Section[] = [];\n items.forEach((item, index) => {\n if (isGroup(item)) {\n out.push({ key: `group-${index}`, label: item.label, options: item.options });\n return;\n }\n const last = out[out.length - 1];\n if (last && !last.label) last.options.push(item);\n else out.push({ key: `loose-${index}`, options: [item] });\n });\n return out;\n}\n\nconst matches = (option: MorphSelectOption, needle: string) =>\n `${option.label} ${option.meta ?? \"\"} ${option.keywords ?? \"\"}`.toLowerCase().includes(needle);\n\n/** The closed trigger's width, rounded up. offsetWidth rounds to the nearest pixel, and half a pixel short is enough to ellipsize the label. */\nfunction triggerWidth(node: HTMLElement) {\n const exact = node.getBoundingClientRect().width;\n const rounded = node.offsetWidth;\n // Under a scaled ancestor the rect is scaled too, so fall back to the layout width with a pixel of slack.\n return Math.abs(exact - rounded) <= 0.5 ? Math.ceil(exact - 0.01) : rounded + 1;\n}\n\n/** The layout the closed trigger and its invisible measuring copy share, so the copy's width is the trigger's. */\nconst triggerLayout =\n \"box-border flex h-(--ms-h) items-center gap-2 ps-(--ms-pad) pe-10 font-medium whitespace-nowrap\";\n\nconst iconClass =\n \"grid size-5 flex-none place-items-center text-text-secondary [&>svg]:size-4 [&>img]:size-5 [&>img]:rounded-full [&>img]:object-cover\";\n\n/** The label a layer shows: the icon, then the text, or the placeholder. */\nfunction Value({ option, placeholder }: { option: MorphSelectOption | undefined; placeholder: string }) {\n if (!option) return <span className=\"font-normal text-muted-foreground\">{placeholder}</span>;\n return (\n <>\n {option.icon ? (\n <span className={iconClass} aria-hidden=\"true\">\n {option.icon}\n </span>\n ) : null}\n <span className=\"min-w-0 truncate\">{option.label}</span>\n </>\n );\n}\n\n/** A replaced label on its way out, popped out of flow where it stood and lifting away. */\nfunction LeavingLayer({\n id,\n rect,\n dir,\n fly,\n onDone,\n children,\n}: {\n id: number;\n rect: Rect;\n dir: number;\n fly: boolean;\n onDone: (id: number) => void;\n children: ReactNode;\n}) {\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove);\n return (\n <span\n aria-hidden=\"true\"\n data-leaving=\"\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget) remove();\n }}\n // field-swap-out lifts by 0.3em times --dir: a label that flew to the trigger lifts 0.3em, one that rolled lifts 0.45em the way the list ran.\n style={{ ...rect, \"--dir\": fly ? 1 : dir * 1.5 } as CSSProperties}\n className=\"absolute flex min-w-0 items-center gap-2 motion-safe:animate-field-swap-out motion-reduce:animate-fade-out\"\n >\n {children}\n </span>\n );\n}\n\nexport function MorphSelect({\n label,\n labelHidden = false,\n items,\n value,\n defaultValue = null,\n onValueChange,\n placeholder = \"Select\",\n searchable = \"auto\",\n searchPlaceholder,\n panelWidth = 272,\n maxListHeight = 296,\n align = \"start\",\n size = \"md\",\n name,\n disabled = false,\n className,\n}: MorphSelectProps) {\n const uid = useId();\n const labelId = `${uid}-label`;\n const listId = `${uid}-list`;\n const optionId = (index: number) => `${uid}-option-${index}`;\n\n const rootRef = useRef<HTMLDivElement>(null);\n const anchorRef = useRef<HTMLDivElement>(null);\n const shapeRef = useRef<HTMLDivElement>(null);\n const measureRef = useRef<HTMLSpanElement>(null);\n const faceRef = useRef<HTMLDivElement>(null);\n const scrollRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const slotRef = useRef<HTMLSpanElement>(null);\n const layerRef = useRef<HTMLSpanElement>(null);\n const highlightRef = useRef<HTMLSpanElement>(null);\n const searchRowRef = useRef<HTMLDivElement>(null);\n const optionRefs = useRef(new Map<string, HTMLElement>());\n\n const sections = useMemo(() => normalize(items), [items]);\n const all = useMemo(() => sections.flatMap((section) => section.options), [sections]);\n const indexOf = useMemo(() => new Map(all.map((option, index) => [option.value, index])), [all]);\n const canSearch = searchable === \"auto\" ? all.length > 8 : searchable;\n\n const [inner, setInner] = useState<string | null>(defaultValue);\n const selected = value !== undefined ? value : inner;\n const selectedOption = selected === null ? undefined : all.find((option) => option.value === selected);\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [active, setActive] = useState<string | null>(null);\n const [change, setChange] = useState<Change>({ value: null, dir: 1, fly: null });\n\n const needle = query.trim().toLowerCase();\n const visibleSections = useMemo(\n () =>\n needle\n ? sections\n .map((section) => ({\n ...section,\n options: section.options.filter((option) => matches(option, needle)),\n }))\n .filter((section) => section.options.length)\n : sections,\n [needle, sections],\n );\n const enabled = useMemo(\n () => visibleSections.flatMap((section) => section.options).filter((option) => !option.disabled),\n [visibleSections],\n );\n const current =\n active !== null && enabled.some((option) => option.value === active)\n ? active\n : (enabled[0]?.value ?? null);\n const resultCount = visibleSections.reduce((sum, section) => sum + section.options.length, 0);\n\n /* ---------------------------------------------- The shape ---------------------------------------------- */\n\n // One surface whose width and height follow the trigger and the open list. The sizes are written straight to the\n // two boxes, and the transitions in CSS do the moving, so a new label never waits on a render. Nothing animates\n // until the first measurement has landed (data-ready).\n const sizes = useRef({ trigger: 0, lid: 0, listW: 0, listH: 0 });\n const live = useRef({ open: false, anchorW: 0 });\n\n const place = useCallback(() => {\n const { trigger, lid, listW, listH } = sizes.current;\n const shape = shapeRef.current;\n const anchor = anchorRef.current;\n if (!trigger || !lid || !shape || !anchor) return;\n const isOpen = live.current.open;\n // A trigger that widens springs; one that narrows never overshoots, so it never dips under its label.\n if (trigger !== live.current.anchorW) {\n anchor.toggleAttribute(\"data-widening\", trigger > live.current.anchorW);\n live.current.anchorW = trigger;\n }\n anchor.style.width = `${trigger}px`;\n shape.style.width = `${isOpen ? Math.max(trigger, listW) : trigger}px`;\n shape.style.height = `${isOpen ? lid + listH : lid}px`;\n }, []);\n\n const read = useCallback(() => {\n const measure = measureRef.current;\n const face = faceRef.current;\n const root = rootRef.current;\n if (!measure || !face || !root) return;\n // The list is never narrower than the trigger; the width goes to CSS directly so a new label never waits on a render.\n const trigger = triggerWidth(measure);\n root.style.setProperty(\"--ms-trigger-w\", `${trigger}px`);\n sizes.current = {\n trigger,\n lid: measure.offsetHeight,\n listW: face.offsetWidth,\n listH: face.offsetHeight,\n };\n place();\n }, [place]);\n\n useIsomorphicLayoutEffect(() => {\n const measure = measureRef.current;\n const face = faceRef.current;\n const root = rootRef.current;\n if (!measure || !face || !root) return;\n read();\n // The first sizes are in place: from here on a change of size moves.\n void shapeRef.current?.offsetWidth;\n root.setAttribute(\"data-ready\", \"\");\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(read);\n observer.observe(measure);\n observer.observe(face);\n return () => observer.disconnect();\n }, [read]);\n\n useIsomorphicLayoutEffect(() => {\n if (live.current.open === open) return;\n live.current.open = open;\n // A pick changes the label and closes in the same commit, so both faces are read fresh before the shape moves.\n read();\n }, [open, read]);\n\n /* ---------------------------------------------- The highlight ---------------------------------------------- */\n\n const scrollIntent = useRef(false);\n useIsomorphicLayoutEffect(() => {\n const highlight = highlightRef.current;\n if (!highlight) return;\n const node = open && current !== null ? optionRefs.current.get(current) : undefined;\n if (!node) {\n highlight.removeAttribute(\"data-on\");\n return;\n }\n const top = node.offsetTop;\n const height = node.offsetHeight;\n // It appears where it is needed, and glides from there.\n highlight.toggleAttribute(\"data-jump\", !highlight.hasAttribute(\"data-on\") || prefersReducedMotion());\n highlight.style.translate = `0 ${top}px`;\n highlight.style.height = `${height}px`;\n highlight.setAttribute(\"data-on\", \"\");\n const scroller = scrollRef.current;\n if (scroller && scrollIntent.current) {\n scrollIntent.current = false;\n const pad = 6;\n if (top < scroller.scrollTop + pad) scroller.scrollTop = top - pad;\n else if (top + height > scroller.scrollTop + scroller.clientHeight - pad)\n scroller.scrollTop = top + height - scroller.clientHeight + pad;\n }\n }, [current, open, visibleSections]);\n\n /* ---------------------------------------------- Focus and keys ---------------------------------------------- */\n\n const showSearch = open && canSearch;\n // Written as attributes, since React 18 and 19 disagree about the `inert` prop and both agree about the attribute: the\n // closed list and the idle search row take no focus and are hidden from assistive technology; a search row in use replaces the trigger.\n useIsomorphicLayoutEffect(() => {\n triggerRef.current?.toggleAttribute(\"inert\", showSearch);\n searchRowRef.current?.toggleAttribute(\"inert\", !showSearch);\n const face = faceRef.current;\n if (!face) return;\n face.toggleAttribute(\"inert\", !open);\n if (open) face.removeAttribute(\"aria-hidden\");\n else face.setAttribute(\"aria-hidden\", \"true\");\n }, [open, showSearch]);\n\n // Focus lands in the lid: the search field when there is one, otherwise the trigger that keeps the active option.\n const pendingFocus = useRef<\"trigger\" | \"input\" | null>(null);\n useIsomorphicLayoutEffect(() => {\n const target = pendingFocus.current;\n pendingFocus.current = null;\n if (target === \"input\") inputRef.current?.focus({ preventScroll: true });\n if (target === \"trigger\") triggerRef.current?.focus({ preventScroll: true });\n }, [open]);\n\n const typeahead = useRef({ buffer: \"\", at: 0 });\n /** Space opens and picks on keydown; the click a button fires on the matching keyup must not undo it. */\n const suppressClick = useRef(false);\n const [announcement, setAnnouncement] = useState(\"\");\n\n function openList(focus?: string | null, seed = \"\") {\n if (disabled || open) return;\n setQuery(seed);\n setActive(focus !== undefined ? focus : selected);\n scrollIntent.current = true;\n pendingFocus.current = canSearch ? \"input\" : \"trigger\";\n setAnnouncement(\"\");\n setOpen(true);\n }\n\n const close = useCallback((restoreFocus: boolean) => {\n typeahead.current.buffer = \"\";\n if (restoreFocus) pendingFocus.current = \"trigger\";\n setOpen(false);\n }, []);\n\n function commit(option: MorphSelectOption | undefined, source?: HTMLElement | null) {\n if (!option || option.disabled) return;\n if (option.value !== selected) {\n // The chosen label starts where it stood in the list and settles into the trigger.\n let fly: Change[\"fly\"] = null;\n const slot = slotRef.current;\n const part = source?.querySelector<HTMLElement>(\"[data-part='value']\");\n if (!prefersReducedMotion() && part && slot) {\n const from = part.getBoundingClientRect();\n const to = slot.getBoundingClientRect();\n fly = { x: from.left - to.left, y: from.top + from.height / 2 - (to.top + to.height / 2) };\n }\n const from = selected === null ? -1 : (indexOf.get(selected) ?? -1);\n const to = indexOf.get(option.value) ?? 0;\n setChange({ value: option.value, dir: from < 0 ? 1 : Math.sign(to - from) || 1, fly });\n if (value === undefined) setInner(option.value);\n onValueChange?.(option.value, option);\n }\n close(true);\n }\n\n function typeTo(char: string) {\n const now = performance.now();\n const state = typeahead.current;\n state.buffer = now - state.at > TYPEAHEAD_RESET ? char : state.buffer + char;\n state.at = now;\n const buffer = state.buffer.toLowerCase();\n const repeated = buffer.split(\"\").every((letter) => letter === buffer[0]);\n const search = repeated ? buffer[0]! : buffer;\n const pool = all.filter((option) => !option.disabled);\n const from = open ? current : selected;\n const at = pool.findIndex((option) => option.value === from);\n const offset = repeated || buffer.length === 1 ? 1 : 0;\n const ordered = [...pool.slice(at + offset), ...pool.slice(0, at + offset)];\n return ordered.find((option) => option.label.toLowerCase().startsWith(search));\n }\n\n function move(to: string | null | undefined) {\n if (to === undefined || to === null) return;\n scrollIntent.current = true;\n setActive(to);\n }\n\n function onKey(event: KeyboardEvent<HTMLElement>, fromInput: boolean) {\n const { key } = event;\n if (key === \" \" && !fromInput) suppressClick.current = true;\n const printable = key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey;\n const typing =\n performance.now() - typeahead.current.at < TYPEAHEAD_RESET && typeahead.current.buffer.length > 0;\n\n if (!open) {\n if (key === \"ArrowDown\" || key === \"ArrowUp\" || key === \"Enter\" || (key === \" \" && !typing)) {\n event.preventDefault();\n openList();\n return;\n }\n if (key === \"Home\" || key === \"End\") {\n event.preventDefault();\n const pool = all.filter((option) => !option.disabled);\n openList((key === \"Home\" ? pool[0] : pool[pool.length - 1])?.value ?? null);\n return;\n }\n if (printable) {\n event.preventDefault();\n if (canSearch) openList(undefined, key);\n else {\n const hit = typeTo(key);\n openList(hit?.value ?? selected);\n }\n }\n return;\n }\n\n const at = enabled.findIndex((option) => option.value === current);\n const caretKeys = fromInput && query.length > 0;\n const pick = () => commit(enabled[at], optionRefs.current.get(current ?? \"\"));\n switch (key) {\n case \"ArrowDown\":\n event.preventDefault();\n move(enabled[Math.min(enabled.length - 1, at + 1)]?.value);\n return;\n case \"ArrowUp\":\n event.preventDefault();\n if (event.altKey) {\n pick();\n return;\n }\n move(enabled[Math.max(0, at - 1)]?.value);\n return;\n case \"PageDown\":\n event.preventDefault();\n move(enabled[Math.min(enabled.length - 1, at + PAGE)]?.value);\n return;\n case \"PageUp\":\n event.preventDefault();\n move(enabled[Math.max(0, at - PAGE)]?.value);\n return;\n case \"Home\":\n if (caretKeys) return;\n event.preventDefault();\n move(enabled[0]?.value);\n return;\n case \"End\":\n if (caretKeys) return;\n event.preventDefault();\n move(enabled[enabled.length - 1]?.value);\n return;\n case \"Enter\":\n event.preventDefault();\n pick();\n return;\n case \"Escape\":\n event.preventDefault();\n event.stopPropagation();\n close(true);\n return;\n case \"Tab\":\n close(false);\n return;\n }\n if (fromInput) return;\n if (key === \" \" && !typing) {\n event.preventDefault();\n pick();\n return;\n }\n if (printable) {\n event.preventDefault();\n move(typeTo(key)?.value);\n }\n }\n\n // A press outside or focus leaving the control folds the list back into the trigger.\n useEffect(() => {\n if (!open) return;\n const down = (event: PointerEvent) => {\n if (!rootRef.current?.contains(event.target as Node)) close(false);\n };\n document.addEventListener(\"pointerdown\", down);\n return () => document.removeEventListener(\"pointerdown\", down);\n }, [close, open]);\n function onBlur(event: FocusEvent<HTMLDivElement>) {\n const next = event.relatedTarget as Node | null;\n if (open && next && !event.currentTarget.contains(next)) close(false);\n }\n\n function onQuery(next: string) {\n setQuery(next);\n setActive(null);\n scrollIntent.current = true;\n if (scrollRef.current) scrollRef.current.scrollTop = 0;\n const text = next.trim().toLowerCase();\n const count = text ? all.filter((option) => matches(option, text)).length : all.length;\n setAnnouncement(text ? (count ? `${count} ${count === 1 ? \"option\" : \"options\"}` : \"No matches\") : \"\");\n }\n\n /* ---------------------------------------------- The chosen label ---------------------------------------------- */\n\n // The trigger's value is a layer: a new one flies in from the list (or rolls, when it cannot) while the old one lifts away.\n const layerKey = selected ?? \"__empty\";\n const flying = change.value === selected ? change.fly : null;\n const { leaving, changed, remove } = useLeavingItems(\n layerKey,\n { key: layerKey, node: <Value option={selectedOption} placeholder={placeholder} />, dir: change.dir, fly: Boolean(flying) },\n () => layerRef.current,\n );\n\n const rootStyle = {\n \"--ms-panel-w\": `${panelWidth}px`,\n \"--ms-list-max\": `${maxListHeight}px`,\n } as CSSProperties;\n const layerStyle = {\n \"--dir\": change.dir,\n ...(flying ? { \"--fly-x\": `${flying.x}px`, \"--fly-y\": `${flying.y}px` } : {}),\n } as CSSProperties;\n\n return (\n <div\n ref={rootRef}\n data-slot=\"morph-select\"\n data-open={open || undefined}\n data-align={align}\n data-size={size}\n data-disabled={disabled || undefined}\n style={rootStyle}\n onBlur={onBlur}\n className={cn(\n \"morph-select-root group/ms relative inline-grid min-w-0 justify-items-start gap-2 font-sans leading-(--leading-normal) text-foreground data-[align=end]:justify-items-end data-[open]:z-30 data-[size=sm]:text-sm data-[size=md]:text-base data-[size=lg]:text-base\",\n className,\n )}\n >\n <span id={labelId} data-slot=\"morph-select-label\" className={cnAll(labelVariants(), labelHidden && \"sr-only\")}>\n {label}\n </span>\n\n <div ref={anchorRef} className=\"morph-select-anchor\">\n {/* Sizes the closed trigger. It holds the chosen label with the trigger's own padding, so the width has a target before anything moves. */}\n <span ref={measureRef} aria-hidden=\"true\" className={cnAll(triggerLayout, \"morph-select-measure\")}>\n <Value option={selectedOption} placeholder={placeholder} />\n </span>\n\n <div\n ref={shapeRef}\n data-slot=\"morph-select-shape\"\n data-open={open || undefined}\n data-disabled={disabled || undefined}\n className=\"morph-select-shape\"\n >\n {/* The lid is the trigger row. It stays at the top of the surface in both states and sits above the list, so a lifted label passes over it. */}\n <div className=\"absolute inset-x-0 top-0 z-10 h-(--ms-h)\">\n <button\n ref={triggerRef}\n type=\"button\"\n role=\"combobox\"\n data-slot=\"morph-select-trigger\"\n aria-labelledby={labelId}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={listId}\n aria-activedescendant={\n open && !canSearch && current !== null ? optionId(indexOf.get(current) ?? 0) : undefined\n }\n disabled={disabled}\n tabIndex={showSearch ? -1 : 0}\n onClick={() => {\n if (suppressClick.current) {\n suppressClick.current = false;\n return;\n }\n if (open) close(true);\n else openList();\n }}\n onKeyDown={(event) => onKey(event, false)}\n onKeyUp={(event) => {\n if (event.key === \" \")\n window.setTimeout(() => {\n suppressClick.current = false;\n });\n }}\n className={cnAll(\n triggerLayout,\n \"absolute inset-0 w-full cursor-pointer border-0 bg-transparent text-start text-foreground outline-none [font-family:inherit] [font-size:inherit] [line-height:inherit] [letter-spacing:inherit] [-webkit-tap-highlight-color:transparent] [touch-action:manipulation] disabled:cursor-not-allowed\",\n )}\n >\n <span\n ref={slotRef}\n data-hidden={showSearch || undefined}\n className=\"relative block h-full min-w-0 flex-1 data-[hidden]:opacity-0 motion-safe:transition-opacity motion-safe:duration-(--duration-instant) motion-safe:ease-settle\"\n >\n {leaving.map((item) => (\n <LeavingLayer\n key={item.id}\n id={item.id}\n rect={item.rect}\n dir={item.value.dir}\n fly={item.value.fly}\n onDone={remove}\n >\n {item.value.node}\n </LeavingLayer>\n ))}\n <span\n key={layerKey}\n ref={layerRef}\n style={layerStyle}\n className={cnAll(\n \"absolute inset-0 flex min-w-0 items-center gap-2\",\n changed &&\n (flying\n ? \"motion-safe:animate-morph-select-fly motion-reduce:animate-fade-in\"\n : \"motion-safe:animate-select-value-in motion-reduce:animate-fade-in\"),\n )}\n >\n <Value option={selectedOption} placeholder={placeholder} />\n </span>\n </span>\n </button>\n\n {canSearch ? (\n <div\n ref={searchRowRef}\n className={cnAll(\n \"absolute inset-0 flex items-center gap-2 ps-(--ms-pad) pe-1.5 motion-safe:transition-opacity motion-safe:duration-(--duration-quick) motion-safe:ease-settle\",\n showSearch ? \"opacity-100\" : \"pointer-events-none opacity-0\",\n )}\n >\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"size-4 flex-none text-muted-foreground\"\n >\n <circle cx=\"7\" cy=\"7\" r=\"4.25\" />\n <path d=\"m10.5 10.5 3 3\" />\n </svg>\n <input\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n data-slot=\"morph-select-search\"\n aria-label={`Search ${label.toLowerCase()}`}\n aria-expanded={open}\n aria-controls={listId}\n aria-autocomplete=\"list\"\n aria-activedescendant={\n open && current !== null ? optionId(indexOf.get(current) ?? 0) : undefined\n }\n placeholder={searchPlaceholder ?? selectedOption?.label ?? \"Search\"}\n value={query}\n autoComplete=\"off\"\n spellCheck={false}\n onChange={(event) => onQuery(event.target.value)}\n onKeyDown={(event) => onKey(event, true)}\n className=\"h-full min-w-0 flex-1 border-0 bg-transparent p-0 [font:inherit] text-foreground outline-none placeholder:text-muted-foreground\"\n />\n {query ? (\n <button\n type=\"button\"\n aria-label=\"Clear search\"\n onPointerDown={(event) => event.preventDefault()}\n onClick={() => {\n onQuery(\"\");\n inputRef.current?.focus();\n }}\n className=\"grid size-6 flex-none cursor-pointer place-items-center rounded-pill border-0 bg-muted p-0 text-text-secondary outline-none motion-safe:animate-field-clear-in motion-reduce:animate-fade-in hover:text-foreground\"\n >\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n className=\"size-3.5\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </button>\n ) : null}\n {/* Sits exactly over the shared chevron, so the chevron closes the list in both lid states. */}\n <button\n type=\"button\"\n aria-label={`Close ${label.toLowerCase()}`}\n onClick={() => close(true)}\n className=\"size-9 flex-none cursor-pointer rounded-pill border-0 bg-transparent p-0 outline-none motion-safe:transition-[background-color] motion-safe:duration-(--duration-quick) motion-safe:ease-settle hover:bg-muted\"\n />\n </div>\n ) : null}\n\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"pointer-events-none absolute end-4 top-1/2 -mt-2 size-4 text-muted-foreground motion-safe:transition-[rotate] motion-safe:duration-(--duration-gentle) motion-safe:ease-gentle group-data-[open]/ms:rotate-180\"\n >\n <path d=\"m4 6 4 4 4-4\" />\n </svg>\n </div>\n\n <div\n ref={faceRef}\n data-open={open || undefined}\n className=\"morph-select-face\"\n >\n <div\n ref={scrollRef}\n className=\"max-h-(--ms-list-max) overflow-y-auto overscroll-contain pt-1.5 [scrollbar-width:thin]\"\n >\n <div id={listId} role=\"listbox\" aria-labelledby={labelId} className=\"relative grid grid-cols-1 gap-0.5\">\n <span ref={highlightRef} aria-hidden=\"true\" className=\"morph-select-highlight\" />\n {visibleSections.map((section) => {\n const rows = section.options.map((option) => {\n const index = indexOf.get(option.value) ?? 0;\n const isSelected = option.value === selected;\n return (\n <div\n key={option.value}\n id={optionId(index)}\n role=\"option\"\n aria-selected={isSelected}\n aria-disabled={option.disabled || undefined}\n data-slot=\"morph-select-option\"\n data-active={option.value === current || undefined}\n ref={(node) => {\n if (node) optionRefs.current.set(option.value, node);\n else optionRefs.current.delete(option.value);\n }}\n onPointerMove={(event) => {\n if (event.pointerType === \"mouse\" && !option.disabled && option.value !== current)\n setActive(option.value);\n }}\n onPointerDown={(event) => event.preventDefault()}\n onClick={(event) => commit(option, event.currentTarget)}\n className=\"morph-select-option relative box-border flex min-h-9 cursor-pointer select-none items-center gap-2 rounded-lg text-text-secondary outline-none data-[active]:text-foreground aria-selected:text-foreground aria-disabled:cursor-not-allowed aria-disabled:opacity-(--opacity-disabled) motion-safe:transition-colors motion-safe:duration-(--duration-quick) motion-safe:ease-settle\"\n >\n <span data-part=\"value\" className=\"flex min-w-0 flex-1 items-center gap-2 font-medium\">\n {option.icon ? (\n <span className={iconClass} aria-hidden=\"true\">\n {option.icon}\n </span>\n ) : null}\n <span className=\"min-w-0 truncate\">{option.label}</span>\n </span>\n {option.meta ? (\n <span className=\"flex-none text-xs tabular-nums text-muted-foreground\">\n {option.meta}\n </span>\n ) : null}\n <span\n aria-hidden=\"true\"\n data-on={isSelected || undefined}\n className=\"grid size-4 flex-none scale-60 place-items-center text-foreground opacity-0 data-[on]:scale-100 data-[on]:opacity-100 motion-safe:transition-[opacity,scale] motion-safe:duration-(--duration-quick) motion-safe:ease-settle\"\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"size-4\"\n >\n <path d=\"m3.5 8.5 3 3 6-6\" />\n </svg>\n </span>\n </div>\n );\n });\n return section.label ? (\n <div\n key={section.key}\n role=\"group\"\n aria-labelledby={`${uid}-${section.key}`}\n className=\"grid grid-cols-1 gap-0.5 not-first:mt-1\"\n >\n <div\n id={`${uid}-${section.key}`}\n role=\"presentation\"\n className=\"px-2.5 pt-2 pb-1 text-xs text-muted-foreground\"\n >\n {section.label}\n </div>\n {rows}\n </div>\n ) : (\n <div key={section.key} role=\"presentation\" className=\"grid grid-cols-1 gap-0.5 not-first:mt-1\">\n {rows}\n </div>\n );\n })}\n {resultCount === 0 ? (\n <p className=\"m-0 px-2.5 pt-3 pb-3.5 text-sm text-muted-foreground\">\n No matches for “{query.trim()}”\n </p>\n ) : null}\n </div>\n </div>\n </div>\n </div>\n </div>\n\n {name ? <input type=\"hidden\" name={name} value={selected ?? \"\"} /> : null}\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {announcement}\n </span>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { SelectProps } from "react-aria-components";
|
|
3
|
+
//#region src/multi-select/multi-select.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* MultiSelect: pick several options from a list. A Select in multiple mode
|
|
6
|
+
* (React Aria), so the keyboard, typeahead, focus and screen-reader behaviour
|
|
7
|
+
* are Select's own; the list stays open while picking.
|
|
8
|
+
*
|
|
9
|
+
* The trigger shows the first picks as chips and the rest as "+N". A new chip
|
|
10
|
+
* fades up from a blur and grows in on the snappy spring while the chips beside
|
|
11
|
+
* it glide over; the count rolls up or down as it changes. A clear button scales
|
|
12
|
+
* in beside the chevron when anything is picked. Options are `SelectItem`s, whose
|
|
13
|
+
* tick pops in as an option is picked.
|
|
14
|
+
*/
|
|
15
|
+
export interface MultiSelectProps extends Omit<SelectProps<object, "multiple">, "className" | "style" | "children" | "items" | "selectionMode"> {
|
|
16
|
+
/** `SelectItem` children. */
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
/** What is being chosen, and the control's accessible name. */
|
|
19
|
+
label: ReactNode;
|
|
20
|
+
labelHidden?: boolean;
|
|
21
|
+
/** Shown while nothing is picked. @default "Select options" */
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
/** Why the selection is wrong. Rendered under the control. */
|
|
24
|
+
errorMessage?: ReactNode;
|
|
25
|
+
/** Chips shown in the trigger before the rest become "+N". @default 2 */
|
|
26
|
+
maxVisible?: number;
|
|
27
|
+
/** Accessible name of the clear button. @default "Clear selections" */
|
|
28
|
+
clearLabel?: string;
|
|
29
|
+
/** @default "md" */
|
|
30
|
+
size?: "sm" | "md" | "lg";
|
|
31
|
+
/** Extra classes for the wrapper. Merged last. */
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare const MultiSelect: import("react").ForwardRefExoticComponent<MultiSelectProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
35
|
+
//#endregion
|
|
36
|
+
//# sourceMappingURL=multi-select.d.ts.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn, cnAll } from "../lib/cn.js";
|
|
3
|
+
import { SwapText } from "../lib/swap-text.js";
|
|
4
|
+
import { useFlip } from "../lib/flip.js";
|
|
5
|
+
import { labelVariants } from "../label/label.js";
|
|
6
|
+
import { selectPopoverVariants, selectTriggerVariants, selectVariants } from "../select/select.js";
|
|
7
|
+
import { forwardRef, useContext, useRef, useState } from "react";
|
|
8
|
+
import { Button, ButtonContext, FieldError, Label, ListBox, Popover, Select, SelectStateContext, SelectValue } from "react-aria-components";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
//#region src/multi-select/multi-select.tsx
|
|
11
|
+
/** The picked options as chips, then "+N", reading Select's own state. */
|
|
12
|
+
function Chips({ placeholder, maxVisible }) {
|
|
13
|
+
const state = useContext(SelectStateContext);
|
|
14
|
+
const keys = state?.value ?? [];
|
|
15
|
+
const labelOf = (key) => state?.collection.getItem(key)?.textValue ?? String(key);
|
|
16
|
+
const shown = keys.slice(0, maxVisible);
|
|
17
|
+
const rest = keys.length - shown.length;
|
|
18
|
+
const row = useRef(null);
|
|
19
|
+
useFlip(row, keys.join("|"), "var(--duration-smooth)", "var(--ease-smooth)");
|
|
20
|
+
const [initial] = useState(() => new Set(keys));
|
|
21
|
+
const [count, setCount] = useState({
|
|
22
|
+
rest,
|
|
23
|
+
dir: 1
|
|
24
|
+
});
|
|
25
|
+
if (count.rest !== rest) setCount({
|
|
26
|
+
rest,
|
|
27
|
+
dir: rest > count.rest ? 1 : -1
|
|
28
|
+
});
|
|
29
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
30
|
+
ref: row,
|
|
31
|
+
"aria-hidden": "true",
|
|
32
|
+
className: "relative flex min-h-6.5 min-w-0 flex-1 items-center gap-1.5 overflow-hidden",
|
|
33
|
+
children: [
|
|
34
|
+
keys.length === 0 ? /* @__PURE__ */ jsx("span", {
|
|
35
|
+
"data-slot": "multi-select-placeholder",
|
|
36
|
+
className: "truncate text-muted-foreground motion-safe:animate-fade-in",
|
|
37
|
+
children: placeholder
|
|
38
|
+
}) : null,
|
|
39
|
+
shown.map((key) => /* @__PURE__ */ jsx("span", {
|
|
40
|
+
"data-flip": "",
|
|
41
|
+
"data-slot": "multi-select-chip",
|
|
42
|
+
className: cnAll("multi-select-chip", !initial.has(key) && "motion-safe:animate-chip-reveal"),
|
|
43
|
+
children: labelOf(key)
|
|
44
|
+
}, String(key))),
|
|
45
|
+
rest > 0 ? /* @__PURE__ */ jsxs("span", {
|
|
46
|
+
"data-flip": "",
|
|
47
|
+
"data-slot": "multi-select-more",
|
|
48
|
+
className: "multi-select-chip text-text-secondary tabular-nums",
|
|
49
|
+
children: ["+", /* @__PURE__ */ jsx(SwapText, {
|
|
50
|
+
swapKey: String(rest),
|
|
51
|
+
dir: count.dir,
|
|
52
|
+
motion: "rise",
|
|
53
|
+
children: rest
|
|
54
|
+
})]
|
|
55
|
+
}) : null
|
|
56
|
+
]
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Clears every pick. It sits outside the trigger, so it is its own tab stop. */
|
|
60
|
+
function Clear({ label, disabled }) {
|
|
61
|
+
const state = useContext(SelectStateContext);
|
|
62
|
+
const keys = state?.value ?? [];
|
|
63
|
+
if (!state || keys.length === 0 || disabled) return null;
|
|
64
|
+
return /* @__PURE__ */ jsx(ButtonContext.Provider, {
|
|
65
|
+
value: null,
|
|
66
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
67
|
+
"aria-label": label,
|
|
68
|
+
"data-slot": "multi-select-clear",
|
|
69
|
+
onPress: () => {
|
|
70
|
+
state.setValue([]);
|
|
71
|
+
state.close();
|
|
72
|
+
},
|
|
73
|
+
className: "multi-select-clear motion-safe:animate-field-clear-in motion-reduce:animate-fade-in",
|
|
74
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
75
|
+
"aria-hidden": "true",
|
|
76
|
+
viewBox: "0 0 16 16",
|
|
77
|
+
fill: "none",
|
|
78
|
+
stroke: "currentColor",
|
|
79
|
+
strokeWidth: "1.75",
|
|
80
|
+
strokeLinecap: "round",
|
|
81
|
+
className: "size-3.5",
|
|
82
|
+
children: /* @__PURE__ */ jsx("path", { d: "m4 4 8 8M12 4l-8 8" })
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const MultiSelect = forwardRef(function MultiSelect({ children, label, labelHidden, placeholder = "Select options", errorMessage, maxVisible = 2, clearLabel = "Clear selections", size = "md", className, ...props }, ref) {
|
|
88
|
+
return /* @__PURE__ */ jsxs(Select, {
|
|
89
|
+
...props,
|
|
90
|
+
selectionMode: "multiple",
|
|
91
|
+
"data-slot": "multi-select",
|
|
92
|
+
className: cn(selectVariants(), className),
|
|
93
|
+
children: [
|
|
94
|
+
/* @__PURE__ */ jsx(Label, {
|
|
95
|
+
"data-slot": "multi-select-label",
|
|
96
|
+
className: cnAll(labelVariants(), labelHidden && "sr-only"),
|
|
97
|
+
children: label
|
|
98
|
+
}),
|
|
99
|
+
/* @__PURE__ */ jsxs("div", {
|
|
100
|
+
className: "relative",
|
|
101
|
+
children: [/* @__PURE__ */ jsxs(Button, {
|
|
102
|
+
ref,
|
|
103
|
+
"data-slot": "multi-select-trigger",
|
|
104
|
+
className: cnAll(selectTriggerVariants({ size }), "pe-16 multi-select-trigger"),
|
|
105
|
+
children: [
|
|
106
|
+
/* @__PURE__ */ jsx(SelectValue, {
|
|
107
|
+
"data-slot": "multi-select-value",
|
|
108
|
+
className: "sr-only",
|
|
109
|
+
children: ({ isPlaceholder, selectedText }) => isPlaceholder ? placeholder : selectedText
|
|
110
|
+
}),
|
|
111
|
+
/* @__PURE__ */ jsx(Chips, {
|
|
112
|
+
placeholder,
|
|
113
|
+
maxVisible
|
|
114
|
+
}),
|
|
115
|
+
/* @__PURE__ */ jsx("svg", {
|
|
116
|
+
"aria-hidden": "true",
|
|
117
|
+
viewBox: "0 0 16 16",
|
|
118
|
+
fill: "none",
|
|
119
|
+
stroke: "currentColor",
|
|
120
|
+
strokeWidth: "1.75",
|
|
121
|
+
strokeLinecap: "round",
|
|
122
|
+
strokeLinejoin: "round",
|
|
123
|
+
className: "absolute end-(--density-control-pad-md) text-muted-foreground motion-safe:transition-[rotate] motion-safe:duration-(--duration-gentle) motion-safe:ease-gentle group-aria-expanded/trigger:rotate-180",
|
|
124
|
+
children: /* @__PURE__ */ jsx("path", { d: "m4 6 4 4 4-4" })
|
|
125
|
+
})
|
|
126
|
+
]
|
|
127
|
+
}), /* @__PURE__ */ jsx(Clear, {
|
|
128
|
+
label: clearLabel,
|
|
129
|
+
disabled: props.isDisabled ?? false
|
|
130
|
+
})]
|
|
131
|
+
}),
|
|
132
|
+
errorMessage == null ? null : /* @__PURE__ */ jsx(FieldError, {
|
|
133
|
+
"data-slot": "multi-select-error",
|
|
134
|
+
className: "text-sm leading-(--leading-normal) text-destructive-strong",
|
|
135
|
+
children: errorMessage
|
|
136
|
+
}),
|
|
137
|
+
/* @__PURE__ */ jsx(Popover, {
|
|
138
|
+
"data-slot": "multi-select-popover",
|
|
139
|
+
className: selectPopoverVariants(),
|
|
140
|
+
children: /* @__PURE__ */ jsx(ListBox, {
|
|
141
|
+
"data-slot": "multi-select-list",
|
|
142
|
+
className: "outline-none",
|
|
143
|
+
children
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
]
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
//#endregion
|
|
150
|
+
export { MultiSelect };
|
|
151
|
+
|
|
152
|
+
//# sourceMappingURL=multi-select.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multi-select.js","names":["AriaButton","AriaSelect","AriaLabel","AriaSelectValue","AriaFieldError","AriaPopover","AriaListBox"],"sources":["../../src/multi-select/multi-select.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useContext, useRef, useState, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n ButtonContext,\n FieldError as AriaFieldError,\n Label as AriaLabel,\n ListBox as AriaListBox,\n Popover as AriaPopover,\n Select as AriaSelect,\n SelectStateContext,\n SelectValue as AriaSelectValue,\n type Key,\n type SelectProps as AriaSelectProps,\n} from \"react-aria-components\";\n\nimport { labelVariants } from \"../label/label.js\";\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { useFlip } from \"../lib/flip.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\nimport { selectPopoverVariants, selectTriggerVariants, selectVariants } from \"../select/select.js\";\n\n/**\n * MultiSelect: pick several options from a list. A Select in multiple mode\n * (React Aria), so the keyboard, typeahead, focus and screen-reader behaviour\n * are Select's own; the list stays open while picking.\n *\n * The trigger shows the first picks as chips and the rest as \"+N\". A new chip\n * fades up from a blur and grows in on the snappy spring while the chips beside\n * it glide over; the count rolls up or down as it changes. A clear button scales\n * in beside the chevron when anything is picked. Options are `SelectItem`s, whose\n * tick pops in as an option is picked.\n */\nexport interface MultiSelectProps extends Omit<\n AriaSelectProps<object, \"multiple\">,\n \"className\" | \"style\" | \"children\" | \"items\" | \"selectionMode\"\n> {\n /** `SelectItem` children. */\n children?: ReactNode;\n /** What is being chosen, and the control's accessible name. */\n label: ReactNode;\n labelHidden?: boolean;\n /** Shown while nothing is picked. @default \"Select options\" */\n placeholder?: string;\n /** Why the selection is wrong. Rendered under the control. */\n errorMessage?: ReactNode;\n /** Chips shown in the trigger before the rest become \"+N\". @default 2 */\n maxVisible?: number;\n /** Accessible name of the clear button. @default \"Clear selections\" */\n clearLabel?: string;\n /** @default \"md\" */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\n/** The picked options as chips, then \"+N\", reading Select's own state. */\nfunction Chips({ placeholder, maxVisible }: { placeholder: string; maxVisible: number }) {\n const state = useContext(SelectStateContext);\n const keys = (state?.value as Key[] | undefined) ?? [];\n const labelOf = (key: Key) => state?.collection.getItem(key)?.textValue ?? String(key);\n const shown = keys.slice(0, maxVisible);\n const rest = keys.length - shown.length;\n const row = useRef<HTMLSpanElement>(null);\n // The chips beside a new or removed one glide to their new place.\n useFlip(row, keys.join(\"|\"), \"var(--duration-smooth)\", \"var(--ease-smooth)\");\n // Chips there at first paint never animate; later ones grow in.\n const [initial] = useState(() => new Set(keys));\n const [count, setCount] = useState({ rest, dir: 1 });\n if (count.rest !== rest) setCount({ rest, dir: rest > count.rest ? 1 : -1 });\n return (\n <span\n ref={row}\n aria-hidden=\"true\"\n className=\"relative flex min-h-6.5 min-w-0 flex-1 items-center gap-1.5 overflow-hidden\"\n >\n {keys.length === 0 ? (\n <span\n data-slot=\"multi-select-placeholder\"\n className=\"truncate text-muted-foreground motion-safe:animate-fade-in\"\n >\n {placeholder}\n </span>\n ) : null}\n {shown.map((key) => (\n <span\n key={String(key)}\n data-flip=\"\"\n data-slot=\"multi-select-chip\"\n className={cnAll(\n \"multi-select-chip\",\n !initial.has(key) && \"motion-safe:animate-chip-reveal\",\n )}\n >\n {labelOf(key)}\n </span>\n ))}\n {rest > 0 ? (\n <span\n data-flip=\"\"\n data-slot=\"multi-select-more\"\n className=\"multi-select-chip text-text-secondary tabular-nums\"\n >\n +\n <SwapText swapKey={String(rest)} dir={count.dir} motion=\"rise\">\n {rest}\n </SwapText>\n </span>\n ) : null}\n </span>\n );\n}\n\n/** Clears every pick. It sits outside the trigger, so it is its own tab stop. */\nfunction Clear({ label, disabled }: { label: string; disabled: boolean }) {\n const state = useContext(SelectStateContext);\n const keys = (state?.value as Key[] | undefined) ?? [];\n if (!state || keys.length === 0 || disabled) return null;\n // Select hands its trigger props to every Button inside it; clear that, or this\n // button would become a second trigger named by the label.\n return (\n <ButtonContext.Provider value={null}>\n <AriaButton\n aria-label={label}\n data-slot=\"multi-select-clear\"\n onPress={() => {\n state.setValue([]);\n state.close();\n }}\n className=\"multi-select-clear motion-safe:animate-field-clear-in motion-reduce:animate-fade-in\"\n >\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n className=\"size-3.5\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n </ButtonContext.Provider>\n );\n}\n\nexport const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(function MultiSelect(\n {\n children,\n label,\n labelHidden,\n placeholder = \"Select options\",\n errorMessage,\n maxVisible = 2,\n clearLabel = \"Clear selections\",\n size = \"md\",\n className,\n ...props\n },\n ref,\n) {\n return (\n <AriaSelect<object, \"multiple\">\n {...props}\n selectionMode=\"multiple\"\n data-slot=\"multi-select\"\n className={cn(selectVariants(), className)}\n >\n <AriaLabel\n data-slot=\"multi-select-label\"\n className={cnAll(labelVariants(), labelHidden && \"sr-only\")}\n >\n {label}\n </AriaLabel>\n <div className=\"relative\">\n <AriaButton\n ref={ref}\n data-slot=\"multi-select-trigger\"\n className={cnAll(selectTriggerVariants({ size }), \"pe-16 multi-select-trigger\")}\n >\n <AriaSelectValue data-slot=\"multi-select-value\" className=\"sr-only\">\n {({ isPlaceholder, selectedText }) => (isPlaceholder ? placeholder : selectedText)}\n </AriaSelectValue>\n <Chips placeholder={placeholder} maxVisible={maxVisible} />\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"absolute end-(--density-control-pad-md) text-muted-foreground motion-safe:transition-[rotate] motion-safe:duration-(--duration-gentle) motion-safe:ease-gentle group-aria-expanded/trigger:rotate-180\"\n >\n <path d=\"m4 6 4 4 4-4\" />\n </svg>\n </AriaButton>\n <Clear label={clearLabel} disabled={props.isDisabled ?? false} />\n </div>\n {errorMessage == null ? null : (\n <AriaFieldError\n data-slot=\"multi-select-error\"\n className=\"text-sm leading-(--leading-normal) text-destructive-strong\"\n >\n {errorMessage}\n </AriaFieldError>\n )}\n <AriaPopover data-slot=\"multi-select-popover\" className={selectPopoverVariants()}>\n <AriaListBox data-slot=\"multi-select-list\" className=\"outline-none\">\n {children}\n </AriaListBox>\n </AriaPopover>\n </AriaSelect>\n );\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NumberFieldProps } from "react-aria-components";
|
|
2
|
+
//#region src/number-field/number-field.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* NumberField: a number with stepper buttons and rolling digits, on React Aria's
|
|
5
|
+
* `NumberField`. Parsing and formatting follow the locale, the arrow keys, PageUp,
|
|
6
|
+
* PageDown, Home and End step and jump, a held stepper repeats, and the field
|
|
7
|
+
* clamps to `minValue` and `maxValue` and snaps to `step`. A currency, a percent
|
|
8
|
+
* or a unit comes from `formatOptions`.
|
|
9
|
+
*
|
|
10
|
+
* The digits you see are drawn over the real input: each place value is its own
|
|
11
|
+
* column, so 9 to 10 opens a column and rolls the ones, and a gained or lost digit
|
|
12
|
+
* opens or closes its width. While you type, the columns follow the text at once
|
|
13
|
+
* and the caret never lags; the roll returns with the next step. A press at a limit
|
|
14
|
+
* (an arrow key past the maximum) pushes the digits toward it and they spring home.
|
|
15
|
+
*
|
|
16
|
+
* `description` and `error` open under the field on a spring, as Input's do.
|
|
17
|
+
*/
|
|
18
|
+
interface NumberFieldProps$1 extends Omit<NumberFieldProps, "className" | "style" | "children"> {
|
|
19
|
+
/** What the number is, and the field's accessible name. */
|
|
20
|
+
label: string;
|
|
21
|
+
/** Render the label for screen readers only. */
|
|
22
|
+
labelHidden?: boolean;
|
|
23
|
+
/** Helper text under the field. A string, because the copy is animated word by word. */
|
|
24
|
+
description?: string;
|
|
25
|
+
/** Error text under the field. Sets `aria-invalid` and is announced as an alert. */
|
|
26
|
+
error?: string;
|
|
27
|
+
/** Shown, muted, while the field is empty. */
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
/** PageUp, PageDown and Shift with an arrow move this far. React Aria steps by one on PageUp. Defaults to ten steps. */
|
|
30
|
+
largeStep?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Control scale: the same heights as Input and Button.
|
|
33
|
+
* @default "md"
|
|
34
|
+
*/
|
|
35
|
+
size?: "sm" | "md" | "lg";
|
|
36
|
+
/** Extra classes for the wrapper. Merged last. */
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
declare const NumberField$1: import("react").ForwardRefExoticComponent<NumberFieldProps$1 & import("react").RefAttributes<HTMLInputElement>>;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { NumberField$1 as NumberField, NumberFieldProps$1 as NumberFieldProps };
|
|
42
|
+
//# sourceMappingURL=number-field.d.ts.map
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn, cnAll } from "../lib/cn.js";
|
|
3
|
+
import { fieldSurfaceVariants } from "../lib/field-surface.js";
|
|
4
|
+
import { Label as Label$1 } from "../label/label.js";
|
|
5
|
+
import { FieldMessage, RollingNumber } from "../lib/field-message.js";
|
|
6
|
+
import { cva } from "class-variance-authority";
|
|
7
|
+
import { forwardRef, useContext, useId, useRef, useState } from "react";
|
|
8
|
+
import { Button, Group, Input, NumberField, NumberFieldStateContext } from "react-aria-components";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
//#region src/number-field/number-field.tsx
|
|
11
|
+
const controlVariants = cva([
|
|
12
|
+
fieldSurfaceVariants(),
|
|
13
|
+
"flex items-center gap-0.5 p-0.75",
|
|
14
|
+
"data-[focus-within]:not-data-[invalid]:border-foreground",
|
|
15
|
+
"forced-colors:data-[focus-within]:outline-solid forced-colors:data-[focus-within]:outline-(length:--focus-ring-width) forced-colors:data-[focus-within]:outline-offset-(--focus-ring-offset)"
|
|
16
|
+
], {
|
|
17
|
+
variants: { size: {
|
|
18
|
+
sm: "h-(--density-control-h-sm)",
|
|
19
|
+
md: "h-(--density-control-h-md)",
|
|
20
|
+
lg: "h-(--density-control-h-lg)"
|
|
21
|
+
} },
|
|
22
|
+
defaultVariants: { size: "md" }
|
|
23
|
+
});
|
|
24
|
+
const stepVariants = cva([
|
|
25
|
+
"grid shrink-0 place-items-center rounded-md border-0 bg-transparent p-0",
|
|
26
|
+
"cursor-pointer text-text-secondary outline-none",
|
|
27
|
+
"motion-safe:transition-[background-color,color,scale] motion-safe:duration-(--duration-quick) motion-safe:ease-settle",
|
|
28
|
+
"data-[hovered]:bg-muted data-[hovered]:text-foreground",
|
|
29
|
+
"data-[pressed]:bg-muted data-[pressed]:text-foreground data-[pressed]:scale-95",
|
|
30
|
+
"motion-safe:data-[pressed]:duration-(--duration-instant)",
|
|
31
|
+
"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)",
|
|
32
|
+
"[&_svg]:pointer-events-none [&_svg]:size-4"
|
|
33
|
+
], {
|
|
34
|
+
variants: { size: {
|
|
35
|
+
sm: "size-7",
|
|
36
|
+
md: "size-9",
|
|
37
|
+
lg: "size-12"
|
|
38
|
+
} },
|
|
39
|
+
defaultVariants: { size: "md" }
|
|
40
|
+
});
|
|
41
|
+
/** How far, in px, the digits are pushed toward a limit they just met. */
|
|
42
|
+
const STRAIN_PX = 5;
|
|
43
|
+
const decimalsOf = (value) => (String(value).split(".")[1] ?? "").length;
|
|
44
|
+
function Control({ size, placeholder, largeStep, step, min, max, inputRef }) {
|
|
45
|
+
const state = useContext(NumberFieldStateContext);
|
|
46
|
+
const displayRef = useRef(null);
|
|
47
|
+
const [typing, setTyping] = useState(false);
|
|
48
|
+
const value = state?.numberValue ?? NaN;
|
|
49
|
+
const [trail, setTrail] = useState({
|
|
50
|
+
value,
|
|
51
|
+
direction: 1
|
|
52
|
+
});
|
|
53
|
+
if (!Object.is(trail.value, value)) setTrail({
|
|
54
|
+
value,
|
|
55
|
+
direction: !Number.isNaN(value) && !Number.isNaN(trail.value) && value < trail.value ? -1 : 1
|
|
56
|
+
});
|
|
57
|
+
const text = (state?.inputValue ?? "").replace(/ /g, "\xA0");
|
|
58
|
+
function strain(y) {
|
|
59
|
+
const node = displayRef.current;
|
|
60
|
+
if (!node) return;
|
|
61
|
+
node.style.setProperty("--strain-y", `${y}px`);
|
|
62
|
+
node.removeAttribute("data-strain");
|
|
63
|
+
node.offsetWidth;
|
|
64
|
+
node.setAttribute("data-strain", "");
|
|
65
|
+
}
|
|
66
|
+
/** PageUp, PageDown and Shift with an arrow take a large step. Runs in the capture phase, so React Aria's own single step does not also run. */
|
|
67
|
+
function onKeyDownCapture(event) {
|
|
68
|
+
if (!state) return;
|
|
69
|
+
const page = event.key === "PageUp" || event.key === "PageDown";
|
|
70
|
+
const arrow = event.shiftKey && (event.key === "ArrowUp" || event.key === "ArrowDown");
|
|
71
|
+
if (!page && !arrow) return;
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
event.stopPropagation();
|
|
74
|
+
setTyping(false);
|
|
75
|
+
const toward = event.key.endsWith("Up") ? 1 : -1;
|
|
76
|
+
if (toward > 0 ? !state.canIncrement : !state.canDecrement) {
|
|
77
|
+
strain(-toward * STRAIN_PX);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const next = (Number.isNaN(state.numberValue) ? min ?? 0 : state.numberValue) + toward * (largeStep ?? step * 10);
|
|
81
|
+
const places = Math.max(decimalsOf(step), decimalsOf(largeStep ?? step));
|
|
82
|
+
state.setNumberValue(Number(Math.min(max ?? Infinity, Math.max(min ?? -Infinity, next)).toFixed(places)));
|
|
83
|
+
}
|
|
84
|
+
function onKeyDown(event) {
|
|
85
|
+
const up = event.key === "ArrowUp";
|
|
86
|
+
const down = event.key === "ArrowDown";
|
|
87
|
+
if (up || down || event.key === "Home" || event.key === "End") setTyping(false);
|
|
88
|
+
if (!state) return;
|
|
89
|
+
if ((up || event.key === "End") && !state.canIncrement) strain(-5);
|
|
90
|
+
if ((down || event.key === "Home") && !state.canDecrement) strain(STRAIN_PX);
|
|
91
|
+
}
|
|
92
|
+
return /* @__PURE__ */ jsxs(Group, {
|
|
93
|
+
role: "presentation",
|
|
94
|
+
"data-slot": "number-field-control",
|
|
95
|
+
className: controlVariants({ size }),
|
|
96
|
+
children: [
|
|
97
|
+
/* @__PURE__ */ jsx(Button, {
|
|
98
|
+
slot: "decrement",
|
|
99
|
+
"data-slot": "number-field-decrement",
|
|
100
|
+
onPressStart: () => setTyping(false),
|
|
101
|
+
className: stepVariants({ size }),
|
|
102
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
103
|
+
"aria-hidden": "true",
|
|
104
|
+
viewBox: "0 0 16 16",
|
|
105
|
+
fill: "none",
|
|
106
|
+
stroke: "currentColor",
|
|
107
|
+
strokeWidth: "1.75",
|
|
108
|
+
strokeLinecap: "round",
|
|
109
|
+
children: /* @__PURE__ */ jsx("path", { d: "M3.5 8h9" })
|
|
110
|
+
})
|
|
111
|
+
}),
|
|
112
|
+
/* @__PURE__ */ jsxs("div", {
|
|
113
|
+
className: "relative grid min-w-0 flex-1 cursor-text place-items-center self-stretch",
|
|
114
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
115
|
+
ref: displayRef,
|
|
116
|
+
"aria-hidden": "true",
|
|
117
|
+
"data-slot": "number-field-display",
|
|
118
|
+
className: "pointer-events-none inline-flex items-center whitespace-nowrap font-sans text-base font-medium tabular-nums text-foreground data-[strain]:motion-safe:animate-field-strain",
|
|
119
|
+
children: text ? /* @__PURE__ */ jsx(RollingNumber, {
|
|
120
|
+
word: text,
|
|
121
|
+
direction: trail.direction,
|
|
122
|
+
still: typing
|
|
123
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
124
|
+
className: "text-muted-foreground",
|
|
125
|
+
children: placeholder
|
|
126
|
+
})
|
|
127
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
128
|
+
ref: inputRef,
|
|
129
|
+
"data-slot": "number-field-input",
|
|
130
|
+
onChange: () => setTyping(true),
|
|
131
|
+
onBlur: () => setTyping(false),
|
|
132
|
+
onKeyDownCapture,
|
|
133
|
+
onKeyDown,
|
|
134
|
+
className: "absolute inset-0 z-10 m-0 h-full w-full min-w-0 border-0 bg-transparent p-0 text-center font-sans text-base font-medium tabular-nums text-transparent caret-foreground outline-none selection:bg-primary/20 selection:text-transparent data-[disabled]:cursor-not-allowed"
|
|
135
|
+
})]
|
|
136
|
+
}),
|
|
137
|
+
/* @__PURE__ */ jsx(Button, {
|
|
138
|
+
slot: "increment",
|
|
139
|
+
"data-slot": "number-field-increment",
|
|
140
|
+
onPressStart: () => setTyping(false),
|
|
141
|
+
className: stepVariants({ size }),
|
|
142
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
143
|
+
"aria-hidden": "true",
|
|
144
|
+
viewBox: "0 0 16 16",
|
|
145
|
+
fill: "none",
|
|
146
|
+
stroke: "currentColor",
|
|
147
|
+
strokeWidth: "1.75",
|
|
148
|
+
strokeLinecap: "round",
|
|
149
|
+
children: /* @__PURE__ */ jsx("path", { d: "M3.5 8h9M8 3.5v9" })
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
]
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const NumberField$1 = forwardRef(function NumberField$2({ label, labelHidden, description, error, placeholder, largeStep, size = "md", className, isInvalid, "aria-describedby": describedBy, ...props }, ref) {
|
|
156
|
+
const id = useId();
|
|
157
|
+
const hintId = description ? `${id}-description` : void 0;
|
|
158
|
+
const errorId = error ? `${id}-error` : void 0;
|
|
159
|
+
return /* @__PURE__ */ jsxs(NumberField, {
|
|
160
|
+
...props,
|
|
161
|
+
isInvalid: isInvalid || Boolean(error),
|
|
162
|
+
"aria-describedby": [
|
|
163
|
+
describedBy,
|
|
164
|
+
hintId,
|
|
165
|
+
errorId
|
|
166
|
+
].filter(Boolean).join(" ") || void 0,
|
|
167
|
+
"data-slot": "number-field",
|
|
168
|
+
className: cn("grid w-full min-w-0 max-w-64 font-sans", className),
|
|
169
|
+
children: [
|
|
170
|
+
/* @__PURE__ */ jsx(Label$1, {
|
|
171
|
+
className: cnAll("mb-1.5", labelHidden && "sr-only"),
|
|
172
|
+
children: label
|
|
173
|
+
}),
|
|
174
|
+
/* @__PURE__ */ jsx(Control, {
|
|
175
|
+
size,
|
|
176
|
+
placeholder,
|
|
177
|
+
largeStep,
|
|
178
|
+
step: props.step ?? 1,
|
|
179
|
+
min: props.minValue,
|
|
180
|
+
max: props.maxValue,
|
|
181
|
+
inputRef: ref
|
|
182
|
+
}),
|
|
183
|
+
/* @__PURE__ */ jsx(FieldMessage, {
|
|
184
|
+
id: hintId,
|
|
185
|
+
text: description,
|
|
186
|
+
tone: "description"
|
|
187
|
+
}),
|
|
188
|
+
/* @__PURE__ */ jsx(FieldMessage, {
|
|
189
|
+
id: errorId,
|
|
190
|
+
text: error,
|
|
191
|
+
tone: "error"
|
|
192
|
+
})
|
|
193
|
+
]
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
//#endregion
|
|
197
|
+
export { NumberField$1 as NumberField };
|
|
198
|
+
|
|
199
|
+
//# sourceMappingURL=number-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"number-field.js","names":["AriaGroup","AriaButton","AriaInput","NumberField","AriaNumberField","Label"],"sources":["../../src/number-field/number-field.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useContext, useId, useRef, useState, type KeyboardEvent, type Ref } from \"react\";\nimport {\n Button as AriaButton,\n Group as AriaGroup,\n Input as AriaInput,\n NumberField as AriaNumberField,\n NumberFieldStateContext,\n type NumberFieldProps as AriaNumberFieldProps,\n} from \"react-aria-components\";\n\nimport { Label } from \"../label/label.js\";\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { FieldMessage, RollingNumber } from \"../lib/field-message.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\n\n/**\n * NumberField: a number with stepper buttons and rolling digits, on React Aria's\n * `NumberField`. Parsing and formatting follow the locale, the arrow keys, PageUp,\n * PageDown, Home and End step and jump, a held stepper repeats, and the field\n * clamps to `minValue` and `maxValue` and snaps to `step`. A currency, a percent\n * or a unit comes from `formatOptions`.\n *\n * The digits you see are drawn over the real input: each place value is its own\n * column, so 9 to 10 opens a column and rolls the ones, and a gained or lost digit\n * opens or closes its width. While you type, the columns follow the text at once\n * and the caret never lags; the roll returns with the next step. A press at a limit\n * (an arrow key past the maximum) pushes the digits toward it and they spring home.\n *\n * `description` and `error` open under the field on a spring, as Input's do.\n */\nexport interface NumberFieldProps extends Omit<\n AriaNumberFieldProps,\n \"className\" | \"style\" | \"children\"\n> {\n /** What the number is, and the field's accessible name. */\n label: string;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /** Helper text under the field. A string, because the copy is animated word by word. */\n description?: string;\n /** Error text under the field. Sets `aria-invalid` and is announced as an alert. */\n error?: string;\n /** Shown, muted, while the field is empty. */\n placeholder?: string;\n /** PageUp, PageDown and Shift with an arrow move this far. React Aria steps by one on PageUp. Defaults to ten steps. */\n largeStep?: number;\n /**\n * Control scale: the same heights as Input and Button.\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\nconst controlVariants = cva(\n [\n fieldSurfaceVariants(),\n \"flex items-center gap-0.5 p-0.75\",\n // Focus turns the box's own border dark whenever the input holds focus; no separate ring.\n \"data-[focus-within]:not-data-[invalid]:border-foreground\",\n \"forced-colors:data-[focus-within]:outline-solid forced-colors:data-[focus-within]:outline-(length:--focus-ring-width) forced-colors:data-[focus-within]:outline-offset-(--focus-ring-offset)\",\n ],\n {\n variants: {\n size: {\n sm: \"h-(--density-control-h-sm)\",\n md: \"h-(--density-control-h-md)\",\n lg: \"h-(--density-control-h-lg)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\nconst stepVariants = cva(\n [\n \"grid shrink-0 place-items-center rounded-md border-0 bg-transparent p-0\",\n \"cursor-pointer text-text-secondary outline-none\",\n \"motion-safe:transition-[background-color,color,scale] motion-safe:duration-(--duration-quick) motion-safe:ease-settle\",\n \"data-[hovered]:bg-muted data-[hovered]:text-foreground\",\n \"data-[pressed]:bg-muted data-[pressed]:text-foreground data-[pressed]:scale-95\",\n \"motion-safe:data-[pressed]:duration-(--duration-instant)\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4\",\n ],\n {\n variants: {\n size: { sm: \"size-7\", md: \"size-9\", lg: \"size-12\" },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n/** How far, in px, the digits are pushed toward a limit they just met. */\nconst STRAIN_PX = 5;\n\nconst decimalsOf = (value: number) => (String(value).split(\".\")[1] ?? \"\").length;\n\nfunction Control({\n size,\n placeholder,\n largeStep,\n step,\n min,\n max,\n inputRef,\n}: {\n size: \"sm\" | \"md\" | \"lg\";\n placeholder: string | undefined;\n largeStep: number | undefined;\n step: number;\n min: number | undefined;\n max: number | undefined;\n inputRef: Ref<HTMLInputElement>;\n}) {\n const state = useContext(NumberFieldStateContext);\n const displayRef = useRef<HTMLSpanElement>(null);\n // While the text is being typed the digits follow it at once; a step brings the roll back.\n const [typing, setTyping] = useState(false);\n const value = state?.numberValue ?? Number.NaN;\n const [trail, setTrail] = useState({ value, direction: 1 as 1 | -1 });\n // Object.is: an empty field is NaN, which is never equal to itself.\n if (!Object.is(trail.value, value)) {\n const known = !Number.isNaN(value) && !Number.isNaN(trail.value);\n setTrail({ value, direction: known && value < trail.value ? -1 : 1 });\n }\n const text = (state?.inputValue ?? \"\").replace(/ /g, \" \");\n\n function strain(y: number) {\n const node = displayRef.current;\n if (!node) return;\n node.style.setProperty(\"--strain-y\", `${y}px`);\n // Restart the animation: the attribute has to leave and come back across a style recalculation.\n node.removeAttribute(\"data-strain\");\n void node.offsetWidth;\n node.setAttribute(\"data-strain\", \"\");\n }\n\n /** PageUp, PageDown and Shift with an arrow take a large step. Runs in the capture phase, so React Aria's own single step does not also run. */\n function onKeyDownCapture(event: KeyboardEvent<HTMLInputElement>) {\n if (!state) return;\n const page = event.key === \"PageUp\" || event.key === \"PageDown\";\n const arrow = event.shiftKey && (event.key === \"ArrowUp\" || event.key === \"ArrowDown\");\n if (!page && !arrow) return;\n event.preventDefault();\n event.stopPropagation();\n setTyping(false);\n const toward = event.key.endsWith(\"Up\") ? 1 : -1;\n if (toward > 0 ? !state.canIncrement : !state.canDecrement) {\n strain(-toward * STRAIN_PX);\n return;\n }\n const from = Number.isNaN(state.numberValue) ? (min ?? 0) : state.numberValue;\n const next = from + toward * (largeStep ?? step * 10);\n const places = Math.max(decimalsOf(step), decimalsOf(largeStep ?? step));\n state.setNumberValue(Number(Math.min(max ?? Infinity, Math.max(min ?? -Infinity, next)).toFixed(places)));\n }\n\n function onKeyDown(event: KeyboardEvent<HTMLInputElement>) {\n const up = event.key === \"ArrowUp\";\n const down = event.key === \"ArrowDown\";\n if (up || down || event.key === \"Home\" || event.key === \"End\") setTyping(false);\n if (!state) return;\n if ((up || event.key === \"End\") && !state.canIncrement) strain(-STRAIN_PX);\n if ((down || event.key === \"Home\") && !state.canDecrement) strain(STRAIN_PX);\n }\n\n return (\n <AriaGroup\n role=\"presentation\"\n data-slot=\"number-field-control\"\n className={controlVariants({ size })}\n >\n <AriaButton\n slot=\"decrement\"\n data-slot=\"number-field-decrement\"\n onPressStart={() => setTyping(false)}\n className={stepVariants({ size })}\n >\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"M3.5 8h9\" />\n </svg>\n </AriaButton>\n <div className=\"relative grid min-w-0 flex-1 cursor-text place-items-center self-stretch\">\n {/* The digits the eye sees. The input above them draws only the caret and the selection. */}\n <span\n ref={displayRef}\n aria-hidden=\"true\"\n data-slot=\"number-field-display\"\n className=\"pointer-events-none inline-flex items-center whitespace-nowrap font-sans text-base font-medium tabular-nums text-foreground data-[strain]:motion-safe:animate-field-strain\"\n >\n {text ? (\n <RollingNumber word={text} direction={trail.direction} still={typing} />\n ) : (\n <span className=\"text-muted-foreground\">{placeholder}</span>\n )}\n </span>\n <AriaInput\n ref={inputRef}\n data-slot=\"number-field-input\"\n onChange={() => setTyping(true)}\n onBlur={() => setTyping(false)}\n onKeyDownCapture={onKeyDownCapture}\n onKeyDown={onKeyDown}\n className=\"absolute inset-0 z-10 m-0 h-full w-full min-w-0 border-0 bg-transparent p-0 text-center font-sans text-base font-medium tabular-nums text-transparent caret-foreground outline-none selection:bg-primary/20 selection:text-transparent data-[disabled]:cursor-not-allowed\"\n />\n </div>\n <AriaButton\n slot=\"increment\"\n data-slot=\"number-field-increment\"\n onPressStart={() => setTyping(false)}\n className={stepVariants({ size })}\n >\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"M3.5 8h9M8 3.5v9\" />\n </svg>\n </AriaButton>\n </AriaGroup>\n );\n}\n\nexport const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(function NumberField(\n {\n label,\n labelHidden,\n description,\n error,\n placeholder,\n largeStep,\n size = \"md\",\n className,\n isInvalid,\n \"aria-describedby\": describedBy,\n ...props\n },\n ref,\n) {\n const id = useId();\n const hintId = description ? `${id}-description` : undefined;\n const errorId = error ? `${id}-error` : undefined;\n return (\n <AriaNumberField\n {...props}\n isInvalid={isInvalid || Boolean(error)}\n aria-describedby={[describedBy, hintId, errorId].filter(Boolean).join(\" \") || undefined}\n data-slot=\"number-field\"\n className={cn(\"grid w-full min-w-0 max-w-64 font-sans\", className)}\n >\n <Label className={cnAll(\"mb-1.5\", labelHidden && \"sr-only\")}>{label}</Label>\n <Control\n size={size}\n placeholder={placeholder}\n largeStep={largeStep}\n step={props.step ?? 1}\n min={props.minValue}\n max={props.maxValue}\n inputRef={ref}\n />\n <FieldMessage id={hintId} text={description} tone=\"description\" />\n <FieldMessage id={errorId} text={error} tone=\"error\" />\n </AriaNumberField>\n );\n});\n"],"mappings":"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"}
|