@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-field.js","names":["AriaButton","AriaGroup","AriaInput"],"sources":["../../src/search-field/search-field.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useCallback, useMemo, useRef, useState } from \"react\";\nimport {\n Button as AriaButton,\n Group as AriaGroup,\n Input as AriaInput,\n type InputProps as AriaInputProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { FieldShell, useFieldWiring } from \"../lib/field-shell.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { prefersReducedMotion, useExitBackstop, useIsomorphicLayoutEffect } from \"../lib/motion.js\";\n\n/**\n * The field's box: a React Aria `Group` carrying the shared field surface (see\n * PasswordField for why a Group, and why `data-focus-within` darkens the border\n * as well). `group/shell` lets the glass read the box's focus and filled state.\n */\nconst shellClass = cn(\n fieldSurfaceVariants(),\n \"group/shell flex items-center gap-(--density-control-gap) h-(--density-control-h-md) px-(--density-control-pad-md) [&:where([data-focus-within])]:border-foreground\",\n);\n\n/** Borderless, fill-less input; the browser's own clear button and search decoration are hidden because the field has its own. */\nconst inputClass =\n \"min-w-0 flex-1 appearance-none border-0 bg-transparent p-0 font-sans text-base text-foreground outline-none placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground data-[disabled]:cursor-not-allowed [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden\";\n\nconst clearClass =\n \"motion-safe:button-motion grid size-6 cursor-pointer place-items-center rounded-md border-0 bg-transparent p-0 text-muted-foreground outline-none data-[hovered]:bg-muted data-[hovered]:text-foreground motion-safe:data-[pressed]:scale-96 data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width) data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring) data-[disabled]:cursor-not-allowed\";\n\n/** The glass wakes up with the field: muted at rest, full contrast while the field has focus or a value. */\nfunction SearchGlyph() {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n className=\"size-4.5 flex-none text-muted-foreground motion-safe:transition-colors motion-safe:duration-(--duration-quick) motion-safe:ease-settle group-data-[filled]/shell:text-foreground group-data-[focus-within]/shell:text-foreground\"\n >\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <path d=\"m21 21-4.34-4.34\" />\n </svg>\n );\n}\n\n/**\n * The clear button, in a box that is always there so the field never changes\n * width. It scales in on the snappy spring while it fades and unblurs, and\n * leaves a little smaller. Nothing animates if it is there on first mount, and\n * while it leaves it is inert so it cannot be pressed or focused.\n */\nfunction ClearButton({\n show,\n label,\n disabled,\n onClear,\n}: {\n show: boolean;\n label: string;\n disabled: boolean;\n onClear: () => void;\n}) {\n const [initial] = useState(show);\n const [rendered, setRendered] = useState(show);\n const [animated, setAnimated] = useState(false);\n const leaving = rendered && !show;\n if (show && !rendered) setRendered(true);\n if (!animated && show !== initial) setAnimated(true);\n const unmount = useCallback(() => setRendered(false), []);\n useExitBackstop(leaving, unmount);\n // Reduced motion removes it at once: an animation that is switched off never ends.\n useIsomorphicLayoutEffect(() => {\n if (leaving && prefersReducedMotion()) unmount();\n }, [leaving, unmount]);\n if (!rendered) return null;\n return (\n <span\n ref={(node) => {\n if (!node) return;\n if (leaving) node.setAttribute(\"inert\", \"\");\n else node.removeAttribute(\"inert\");\n }}\n aria-hidden={leaving || undefined}\n onAnimationEnd={(event) => {\n if (\n leaving &&\n event.target === event.currentTarget &&\n event.animationName === \"field-clear-out\"\n )\n unmount();\n }}\n className={cn(\n \"grid size-6 place-items-center\",\n leaving\n ? \"pointer-events-none motion-safe:animate-field-clear-out\"\n : animated && \"motion-safe:animate-field-clear-in motion-reduce:animate-fade-in\",\n )}\n >\n <AriaButton\n type=\"button\"\n isDisabled={disabled}\n aria-label={label}\n onPress={onClear}\n className={clearClass}\n >\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n className=\"size-4\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </AriaButton>\n </span>\n );\n}\n\nexport interface SearchFieldProps extends Omit<\n AriaInputProps,\n \"className\" | \"style\" | \"size\" | \"type\" | \"value\" | \"defaultValue\" | \"onChange\"\n> {\n /** Extra classes for the input. Merged last, so they win conflicts with ours. */\n className?: string;\n /** The visible label, tied to the input with `htmlFor`. */\n label: string;\n /** The current text. Controlled: the field has no state of its own. */\n value: string;\n /** Called with the new text on every edit, and with `\"\"` when the clear button is pressed. */\n onValueChange: (value: string) => void;\n /** The clear button's accessible name. @default \"Clear search\" */\n clearLabel?: string;\n}\n\n/**\n * A search input with a clear button. The button has a reserved box, so the\n * field never changes width as it comes and goes; pressing it clears the text\n * and returns focus to the input. `onChange` is not accepted: use `onValueChange`.\n * Native input props apply to the input, and `disabled` also disables the button.\n *\n * @example\n * <SearchField label=\"Search contacts\" value={query} onValueChange={setQuery} />\n */\nexport const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(function SearchField(\n { label, value, onValueChange, clearLabel = \"Clear search\", className, ...props },\n ref,\n) {\n const inputRef = useRef<HTMLInputElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, inputRef), [ref]);\n const field = useFieldWiring({\n enabled: true,\n id: props.id,\n describedBy: props[\"aria-describedby\"],\n invalid: props[\"aria-invalid\"],\n description: undefined,\n error: undefined,\n });\n const disabled = Boolean(props.disabled);\n\n const control = (\n <AriaGroup\n role=\"presentation\"\n isDisabled={disabled}\n data-filled={value ? \"true\" : undefined}\n data-slot=\"search-field-shell\"\n className={shellClass}\n >\n <SearchGlyph />\n <AriaInput\n ref={refs}\n {...props}\n {...field.controlProps}\n type=\"search\"\n value={value}\n onChange={(event) => onValueChange(event.target.value)}\n data-slot=\"search-field\"\n className={cn(inputClass, className)}\n />\n {/* A clear button's box is reserved, so the field never changes width when it appears or leaves. */}\n <span className=\"grid size-6 flex-none place-items-center\">\n <ClearButton\n show={value !== \"\"}\n label={clearLabel}\n disabled={disabled}\n onClear={() => {\n onValueChange(\"\");\n inputRef.current?.focus();\n }}\n />\n </span>\n </AriaGroup>\n );\n\n if (!field.active) return control;\n return (\n <FieldShell\n label={label}\n controlId={field.controlId}\n description={undefined}\n hintId={undefined}\n error={undefined}\n errorId={undefined}\n >\n {control}\n </FieldShell>\n );\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"search-field.js","names":["AriaButton","SearchField","AriaGroup","AriaInput"],"sources":["../../src/search-field/search-field.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useCallback, useMemo, useRef, useState } from \"react\";\nimport {\n Button as AriaButton,\n Group as AriaGroup,\n Input as AriaInput,\n type InputProps as AriaInputProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { FieldShell, useFieldWiring } from \"../lib/field-shell.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { prefersReducedMotion, useExitBackstop, useIsomorphicLayoutEffect } from \"../lib/motion.js\";\n\n/**\n * The field's box: a React Aria `Group` carrying the shared field surface (see\n * PasswordField for why a Group, and why `data-focus-within` darkens the border\n * as well). `group/shell` lets the glass read the box's focus and filled state.\n */\nconst shellClass = cn(\n fieldSurfaceVariants(),\n \"group/shell flex items-center gap-(--density-control-gap) h-(--density-control-h-md) px-(--density-control-pad-md) [&:where([data-focus-within])]:border-border-strong\",\n);\n\n/** Borderless, fill-less input; the browser's own clear button and search decoration are hidden because the field has its own. */\nconst inputClass =\n \"min-w-0 flex-1 appearance-none border-0 bg-transparent p-0 font-sans text-base text-foreground outline-none placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground data-[disabled]:cursor-not-allowed [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden\";\n\nconst clearClass =\n \"motion-safe:button-motion grid size-6 cursor-pointer place-items-center rounded-control border-0 bg-transparent p-0 text-muted-foreground outline-none data-[hovered]:bg-muted data-[hovered]:text-foreground motion-safe:data-[pressed]:scale-96 data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width) data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring) data-[disabled]:cursor-not-allowed\";\n\n/** The glass wakes up with the field: muted at rest, full contrast while the field has focus or a value. */\nfunction SearchGlyph() {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n className=\"size-4.5 flex-none text-muted-foreground motion-safe:transition-colors motion-safe:duration-(--duration-quick) motion-safe:ease-settle group-data-[filled]/shell:text-foreground group-data-[focus-within]/shell:text-foreground\"\n >\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <path d=\"m21 21-4.34-4.34\" />\n </svg>\n );\n}\n\n/**\n * The clear button, in a box that is always there so the field never changes\n * width. It scales in on the snappy spring while it fades and unblurs, and\n * leaves a little smaller. Nothing animates if it is there on first mount, and\n * while it leaves it is inert so it cannot be pressed or focused.\n */\nfunction ClearButton({\n show,\n label,\n disabled,\n onClear,\n}: {\n show: boolean;\n label: string;\n disabled: boolean;\n onClear: () => void;\n}) {\n const [initial] = useState(show);\n const [rendered, setRendered] = useState(show);\n const [animated, setAnimated] = useState(false);\n const leaving = rendered && !show;\n if (show && !rendered) setRendered(true);\n if (!animated && show !== initial) setAnimated(true);\n const unmount = useCallback(() => setRendered(false), []);\n useExitBackstop(leaving, unmount);\n // Reduced motion removes it at once: an animation that is switched off never ends.\n useIsomorphicLayoutEffect(() => {\n if (leaving && prefersReducedMotion()) unmount();\n }, [leaving, unmount]);\n if (!rendered) return null;\n return (\n <span\n ref={(node) => {\n if (!node) return;\n if (leaving) node.setAttribute(\"inert\", \"\");\n else node.removeAttribute(\"inert\");\n }}\n aria-hidden={leaving || undefined}\n onAnimationEnd={(event) => {\n if (\n leaving &&\n event.target === event.currentTarget &&\n event.animationName === \"field-clear-out\"\n )\n unmount();\n }}\n className={cn(\n \"grid size-6 place-items-center\",\n leaving\n ? \"pointer-events-none motion-safe:animate-field-clear-out\"\n : animated && \"motion-safe:animate-field-clear-in motion-reduce:animate-fade-in\",\n )}\n >\n <AriaButton\n type=\"button\"\n isDisabled={disabled}\n aria-label={label}\n onPress={onClear}\n className={clearClass}\n >\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n className=\"size-4\"\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </AriaButton>\n </span>\n );\n}\n\nexport interface SearchFieldProps extends Omit<\n AriaInputProps,\n \"className\" | \"style\" | \"size\" | \"type\" | \"value\" | \"defaultValue\" | \"onChange\"\n> {\n /** Extra classes for the input. Merged last, so they win conflicts with ours. */\n className?: string;\n /** The visible label, tied to the input with `htmlFor`. */\n label: string;\n /** The current text. Controlled: the field has no state of its own. */\n value: string;\n /** Called with the new text on every edit, and with `\"\"` when the clear button is pressed. */\n onValueChange: (value: string) => void;\n /** The clear button's accessible name. @default \"Clear search\" */\n clearLabel?: string;\n}\n\n/**\n * A search input with a clear button. The button has a reserved box, so the\n * field never changes width as it comes and goes; pressing it clears the text\n * and returns focus to the input. `onChange` is not accepted: use `onValueChange`.\n * Native input props apply to the input, and `disabled` also disables the button.\n *\n * @example\n * <SearchField label=\"Search contacts\" value={query} onValueChange={setQuery} />\n */\nexport const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(function SearchField(\n { label, value, onValueChange, clearLabel = \"Clear search\", className, ...props },\n ref,\n) {\n const inputRef = useRef<HTMLInputElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, inputRef), [ref]);\n const field = useFieldWiring({\n enabled: true,\n id: props.id,\n describedBy: props[\"aria-describedby\"],\n invalid: props[\"aria-invalid\"],\n description: undefined,\n error: undefined,\n });\n const disabled = Boolean(props.disabled);\n\n const control = (\n <AriaGroup\n role=\"presentation\"\n isDisabled={disabled}\n data-filled={value ? \"true\" : undefined}\n data-slot=\"search-field-shell\"\n className={shellClass}\n >\n <SearchGlyph />\n <AriaInput\n ref={refs}\n {...props}\n {...field.controlProps}\n type=\"search\"\n value={value}\n onChange={(event) => onValueChange(event.target.value)}\n data-slot=\"search-field\"\n className={cn(inputClass, className)}\n />\n {/* A clear button's box is reserved, so the field never changes width when it appears or leaves. */}\n <span className=\"grid size-6 flex-none place-items-center\">\n <ClearButton\n show={value !== \"\"}\n label={clearLabel}\n disabled={disabled}\n onClear={() => {\n onValueChange(\"\");\n inputRef.current?.focus();\n }}\n />\n </span>\n </AriaGroup>\n );\n\n if (!field.active) return control;\n return (\n <FieldShell\n label={label}\n controlId={field.controlId}\n description={undefined}\n hintId={undefined}\n error={undefined}\n errorId={undefined}\n >\n {control}\n </FieldShell>\n );\n});\n"],"mappings":"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"}
|
package/dist/select/select.d.ts
CHANGED
|
@@ -29,7 +29,9 @@ export declare const selectVariants: (props?: import("class-variance-authority/t
|
|
|
29
29
|
export declare const selectTriggerVariants: (props?: ({
|
|
30
30
|
size?: "sm" | "md" | "lg" | null | undefined;
|
|
31
31
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
32
|
-
export declare const selectPopoverVariants: (props?:
|
|
32
|
+
export declare const selectPopoverVariants: (props?: ({
|
|
33
|
+
reveal?: "grow" | "fade" | null | undefined;
|
|
34
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
33
35
|
export declare const selectItemVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
34
36
|
/**
|
|
35
37
|
* A single-choice picker.
|
package/dist/select/select.js
CHANGED
|
@@ -37,7 +37,7 @@ const selectVariants = cva(["group flex w-full flex-col gap-1.5 box-border font-
|
|
|
37
37
|
const selectTriggerVariants = cva([
|
|
38
38
|
"group-data-[invalid]:border-destructive-strong",
|
|
39
39
|
fieldSurfaceVariants(),
|
|
40
|
-
"[&:where([data-pressed])]:border-
|
|
40
|
+
"[&:where([data-pressed])]:border-border-strong",
|
|
41
41
|
"data-[hovered]:not-data-[disabled]:bg-muted",
|
|
42
42
|
"data-[pressed]:bg-muted",
|
|
43
43
|
"group/trigger",
|
|
@@ -70,19 +70,31 @@ size: {
|
|
|
70
70
|
const selectPopoverVariants = cva([
|
|
71
71
|
"w-(--trigger-width) min-w-32 overflow-y-auto",
|
|
72
72
|
"[--menu-x:0px] [--menu-y:-6px] origin-top",
|
|
73
|
-
"
|
|
73
|
+
"[--menu-from:var(--density-control-h-md)] [--menu-flip:0]",
|
|
74
|
+
"data-[placement=top]:[--menu-y:6px] data-[placement=top]:origin-bottom data-[placement=top]:[--menu-flip:1]",
|
|
74
75
|
"data-[placement=left]:[--menu-x:6px] data-[placement=left]:[--menu-y:0px] data-[placement=left]:origin-right",
|
|
75
76
|
"data-[placement=right]:[--menu-x:-6px] data-[placement=right]:[--menu-y:0px] data-[placement=right]:origin-left",
|
|
76
|
-
"motion-
|
|
77
|
+
"motion-reduce:data-[entering]:animate-select-fade-in",
|
|
77
78
|
"motion-safe:data-[exiting]:animate-select-popover-out motion-reduce:data-[exiting]:animate-select-fade-out",
|
|
78
79
|
"data-[exiting]:pointer-events-none",
|
|
79
|
-
"rounded-
|
|
80
|
+
"rounded-panel border-(length:--border-width) border-border p-1",
|
|
80
81
|
"bg-card text-card-foreground shadow-floating",
|
|
81
82
|
"box-border font-sans text-sm leading-(--leading-normal)"
|
|
82
|
-
]
|
|
83
|
+
], {
|
|
84
|
+
variants: {
|
|
85
|
+
/**
|
|
86
|
+
* `grow` opens the panel from the trigger's height on the smooth spring (about 700ms). `fade` is the quick 240ms
|
|
87
|
+
* settle for a list whose first row is a search field: a clipped panel would hide it while it grows.
|
|
88
|
+
*/
|
|
89
|
+
reveal: {
|
|
90
|
+
grow: "motion-safe:data-[entering]:animate-select-popover-reveal",
|
|
91
|
+
fade: "motion-safe:data-[entering]:animate-select-popover-in"
|
|
92
|
+
} },
|
|
93
|
+
defaultVariants: { reveal: "grow" }
|
|
94
|
+
});
|
|
83
95
|
const selectItemVariants = cva([
|
|
84
96
|
"relative flex cursor-pointer select-none items-center gap-2",
|
|
85
|
-
"rounded-
|
|
97
|
+
"rounded-control py-1.5 pe-8 ps-2",
|
|
86
98
|
"box-border text-sm leading-(--leading-normal) text-card-foreground",
|
|
87
99
|
"outline-none data-[focused]:bg-muted",
|
|
88
100
|
"motion-safe:transition-colors motion-safe:duration-(--duration-instant) motion-safe:ease-settle",
|
|
@@ -173,7 +185,7 @@ const Select$1 = forwardRef(function Select$2({ children, label, labelHidden, pl
|
|
|
173
185
|
}),
|
|
174
186
|
/* @__PURE__ */ jsx(Popover, {
|
|
175
187
|
"data-slot": "select-popover",
|
|
176
|
-
className: selectPopoverVariants(),
|
|
188
|
+
className: cn(selectPopoverVariants(), "border-0"),
|
|
177
189
|
children: /* @__PURE__ */ jsx(ListBox, {
|
|
178
190
|
"data-slot": "select-list",
|
|
179
191
|
className: "outline-none",
|
|
@@ -199,7 +211,7 @@ const SelectItem = forwardRef(function SelectItem({ className, children, textVal
|
|
|
199
211
|
strokeWidth: "2",
|
|
200
212
|
strokeLinecap: "round",
|
|
201
213
|
strokeLinejoin: "round",
|
|
202
|
-
className: "absolute
|
|
214
|
+
className: "absolute end-2 size-3.5 motion-safe:animate-select-check-in",
|
|
203
215
|
children: /* @__PURE__ */ jsx("path", { d: "m3.5 8.5 3 3 6-6" })
|
|
204
216
|
}) : null, children] })
|
|
205
217
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select.js","names":["AriaSelectValue","Select","AriaSelect","AriaLabel","AriaButton","AriaFieldError","AriaPopover","AriaListBox","AriaListBoxItem"],"sources":["../../src/select/select.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useContext, useState, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n FieldError as AriaFieldError,\n Label as AriaLabel,\n ListBox as AriaListBox,\n ListBoxItem as AriaListBoxItem,\n Popover as AriaPopover,\n Select as AriaSelect,\n SelectStateContext,\n SelectValue as AriaSelectValue,\n type ListBoxItemProps as AriaListBoxItemProps,\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 { fieldSurfaceVariants } from \"../lib/field-surface.js\";\nimport { useIsomorphicLayoutEffect } from \"../lib/motion.js\";\nimport { RollingValue } from \"../lib/rolling-value.js\";\n\n/**\n * Select, ported from the legacy shadcn/Radix component.\n *\n * FOUR PARTS COLLAPSE INTO ONE. The census reads `SelectItem` 35 and\n * `Select`, `SelectTrigger`, `SelectValue`, `SelectContent` 15 each --\n * those four always appear together in the same shape, exactly like\n * Dialog's header trio, so they are one component and a prop.\n * `SelectGroup`, `SelectLabel`, `SelectSeparator`, `SelectScrollUpButton`\n * and `SelectScrollDownButton` have ZERO call sites and are not ported.\n *\n * `label` IS REQUIRED. The legacy exports a `SelectLabel` that nobody\n * renders and passes no `aria-label` anywhere, so all 15 selects are\n * unnamed comboboxes -- the same finding as Progress, Slider and\n * ScrollArea. Pass `labelHidden` when the surrounding UI already says it.\n *\n * The trigger reuses `fieldSurfaceVariants`, shared with Input and\n * Textarea. A select and a text field sit in the same form row and must\n * be the same box; the legacy styled them separately and drifted --\n * `h-10` here against Input's `h-9`, for no stated reason.\n *\n * The popover is `card`, not a `popover` role: `--popover` is\n * byte-identical to `--card` in the legacy theme. Item hover is `muted`,\n * not `accent`, for the same reason.\n */\n/*\n * `group` is load-bearing, not decoration. React Aria puts `data-invalid` on\n * THIS element, never on the trigger -- so the trigger's own\n * `data-[invalid]:` rule, inherited from the shared field surface, can never\n * match and `isInvalid` painted nothing at all. The trigger reads the state\n * from here instead.\n */\nexport const selectVariants = cva([\"group flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\nexport const selectTriggerVariants = cva(\n [\n // See selectVariants: the invalid state lives on the ROOT, so this reads it\n // from the group rather than from itself.\n \"group-data-[invalid]:border-destructive-strong\",\n fieldSurfaceVariants(),\n // While the menu is open the trigger is pressed and focus has moved into the\n // list, so the border keeps the darkness it had instead of falling back.\n \"[&:where([data-pressed])]:border-foreground\",\n // The fill follows the border: hovered, pressed and open all read as one\n // state. Ordinary variants, not :where(): the base background is a plain\n // class, so a zero-specificity rule would lose to it. Nothing else paints\n // the background, and a disabled trigger is never hovered or pressed.\n \"data-[hovered]:not-data-[disabled]:bg-muted\",\n \"data-[pressed]:bg-muted\",\n \"group/trigger\",\n \"flex items-center justify-between gap-2 text-start\",\n \"leading-(--leading-normal) cursor-pointer\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n ],\n {\n variants: {\n /**\n * The same rungs and the same tokens as Input and Button. 14 of the\n * product's 32 `SelectTrigger` call sites override the height by hand,\n * and a Select sits in the same form row as a field -- giving Input the\n * axis without giving it here would be exactly the drift\n * `fieldSurfaceVariants` exists to prevent.\n *\n * Type matches Input's at every rung: 14px on `sm` (16px on a touch\n * screen), 16px on `md` and `lg`. A row whose two controls disagree\n * about type size reads as a mistake. The trigger is a button, so iOS\n * would not zoom on it; the coarse-pointer rule is there to keep it\n * equal to the field beside it.\n */\n size: {\n sm: \"h-(--density-control-h-sm) px-(--density-control-pad-sm) text-sm pointer-coarse:text-base\",\n md: \"h-(--density-control-h-md) px-(--density-control-pad-md) text-base\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg) text-base\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\nexport const selectPopoverVariants = cva([\n // --trigger-width is set inline by React Aria, so the list matches the\n // control it came from rather than guessing a width.\n \"w-(--trigger-width) min-w-32 overflow-y-auto\",\n // Grows from the trigger edge: offset toward it and a little small, 240ms in,\n // 120ms out. React Aria marks the first frame data-entering and the leaving\n // one data-exiting and keeps the list mounted until the animation ends.\n \"[--menu-x:0px] [--menu-y:-6px] origin-top\",\n \"data-[placement=top]:[--menu-y:6px] data-[placement=top]:origin-bottom\",\n \"data-[placement=left]:[--menu-x:6px] data-[placement=left]:[--menu-y:0px] data-[placement=left]:origin-right\",\n \"data-[placement=right]:[--menu-x:-6px] data-[placement=right]:[--menu-y:0px] data-[placement=right]:origin-left\",\n \"motion-safe:data-[entering]:animate-select-popover-in motion-reduce:data-[entering]:animate-select-fade-in\",\n \"motion-safe:data-[exiting]:animate-select-popover-out motion-reduce:data-[exiting]:animate-select-fade-out\",\n \"data-[exiting]:pointer-events-none\",\n \"rounded-md border-(length:--border-width) border-border p-1\",\n \"bg-card text-card-foreground shadow-floating\",\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n]);\n\nexport const selectItemVariants = cva([\n \"relative flex cursor-pointer select-none items-center gap-2\",\n \"rounded-2xs py-1.5 pe-2 ps-8\",\n \"box-border text-sm leading-(--leading-normal) text-card-foreground\",\n // data-focused, not :hover. React Aria drives one highlight from both\n // pointer and keyboard, so the row under the mouse and the row the\n // arrows reached are the same row.\n \"outline-none data-[focused]:bg-muted\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-instant) motion-safe:ease-settle\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n]);\n\n/**\n * A single-choice picker.\n *\n * @example\n * <Select label=\"Country\" placeholder=\"Pick one\"\n * selectedKey={code} onSelectionChange={setCode}>\n * <SelectItem id=\"gb\">United Kingdom</SelectItem>\n * <SelectItem id=\"us\">United States</SelectItem>\n * </Select>\n */\nexport interface SelectProps extends Omit<\n AriaSelectProps<object>,\n \"className\" | \"style\" | \"children\" | \"items\"\n> {\n /** `SelectItem` children. */\n children?: ReactNode;\n /**\n * What is being chosen, and the control's accessible name.\n *\n * **Required.** All 15 legacy selects are unnamed: the legacy exports a\n * `SelectLabel` that nobody renders and passes no `aria-label`.\n */\n label: ReactNode;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /** Shown while nothing is selected. */\n placeholder?: string;\n /**\n * Why the selection is wrong. Rendered under the control, and it is what\n * makes `isInvalid` reach a screen reader.\n *\n * React Aria marks the ROOT `data-invalid` and leaves the button untouched,\n * and an `aria-invalid` handed to that button is dropped by\n * `filterDOMProps`, whose allowlist stops at the labelling attributes -- the\n * same silent loss as `aria-current` on a pagination button. So without a\n * message, an invalid select is invalid only in colour, which is WCAG 1.4.1\n * on its own. React Aria wires this into `aria-describedby` for us.\n */\n errorMessage?: ReactNode;\n /**\n * Control scale of the trigger. Height and inline padding come from the\n * same `--density-control-*` tokens as Input and Button, so a Select, a\n * field and a button on one rung are the same height.\n *\n * Type does not scale with it, matching Input.\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\n/**\n * The shown value. React Aria's own `SelectValue` stays, visually hidden, as the\n * text a screen reader reads; the copy the eye sees rolls when the selection\n * changes: a later option rises from below, an earlier one drops from above. The\n * direction is read from the option's place in the list. A pre-selected value\n * does not animate on first mount.\n */\nfunction ValueRoll({ placeholder }: { placeholder?: string }) {\n const state = useContext(SelectStateContext);\n const key = state?.selectedKey ?? null;\n let index = -1;\n if (state && key != null) {\n let n = 0;\n for (const item of state.collection) {\n if (item.type !== \"item\") continue;\n if (item.key === key) index = n;\n n += 1;\n }\n }\n const [previous, setPrevious] = useState(index);\n const [dir, setDir] = useState(1);\n if (previous !== index) {\n setPrevious(index);\n setDir(index > previous ? 1 : -1);\n }\n const text = state?.selectedItem?.textValue ?? \"\";\n // React Aria resolves a default selection after the first render, so the key\n // starts as null and then settles. Until the collection is built nothing is\n // shown, and the swap remounts once it is, so that settling is not a change.\n const built = (state?.collection.size ?? 0) > 0 || state?.selectedKey != null;\n const [mounted, setMounted] = useState(false);\n useIsomorphicLayoutEffect(() => setMounted(true), []);\n const settled = built || mounted;\n const swapKey = key == null ? \"placeholder\" : `value-${String(key)}`;\n return (\n <RollingValue\n srValue={\n <AriaSelectValue data-slot=\"select-value\" className=\"sr-only\">\n {({ isPlaceholder, selectedText }) => (isPlaceholder ? placeholder : selectedText)}\n </AriaSelectValue>\n }\n swapKey={swapKey}\n dir={dir}\n instanceKey={settled ? \"live\" : \"boot\"}\n >\n {key == null ? (\n <span data-slot=\"select-placeholder\" className=\"text-muted-foreground\">\n {placeholder}\n </span>\n ) : (\n text\n )}\n </RollingValue>\n );\n}\n\nexport const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select(\n { children, label, labelHidden, placeholder, errorMessage, size = \"md\", className, ...props },\n ref,\n) {\n return (\n <AriaSelect {...props} data-slot=\"select\" className={cn(selectVariants(), className)}>\n {/*\n The Label's own classes, so a Select's label and the Label over an\n Input in the same form are one style (same size, weight, colour and\n line height). The select-label slot stays for consumers that target it.\n */}\n <AriaLabel\n data-slot=\"select-label\"\n className={cnAll(labelVariants(), labelHidden && \"sr-only\")}\n >\n {label}\n </AriaLabel>\n\n {/*\n The ref goes on the TRIGGER, not the AriaSelect root above. The root\n is a field-level wrapper div; the trigger is the always-visible\n interactive surface a caller actually wants to focus or click,\n the same reasoning that puts Input's ref on the real control.\n */}\n <AriaButton ref={ref} data-slot=\"select-trigger\" className={selectTriggerVariants({ size })}>\n <ValueRoll placeholder={placeholder} />\n {/*\n aria-hidden: the chevron is decoration. The button already\n announces itself as a combobox, and a second name source only\n makes it announce twice.\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=\"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\n {errorMessage == null ? null : (\n // Not conditional on isInvalid: React Aria renders FieldError only\n // while the field IS invalid, so gating it here as well would just be\n // two sources of truth for one state.\n <AriaFieldError\n data-slot=\"select-error\"\n className=\"text-sm leading-(--leading-normal) text-destructive-strong\"\n >\n {errorMessage}\n </AriaFieldError>\n )}\n\n <AriaPopover data-slot=\"select-popover\" className={selectPopoverVariants()}>\n <AriaListBox data-slot=\"select-list\" className=\"outline-none\">\n {children}\n </AriaListBox>\n </AriaPopover>\n </AriaSelect>\n );\n});\n\n/**\n * One option. Its `id` is the value.\n *\n * @example\n * <SelectItem id=\"gb\">United Kingdom</SelectItem>\n */\nexport interface SelectItemProps extends Omit<\n AriaListBoxItemProps,\n \"className\" | \"style\" | \"children\"\n> {\n children?: ReactNode;\n /**\n * The option's text, for the trigger and for typeahead.\n *\n * Derived from `children` when they are a string. Pass it explicitly\n * when the option renders anything richer than text.\n */\n textValue?: string;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(function SelectItem(\n { className, children, textValue, ...props },\n ref,\n) {\n return (\n <AriaListBoxItem\n ref={ref}\n {...props}\n /*\n textValue, derived from string children.\n React Aria infers an item's text from its children -- but ONLY\n when they are a string. This item uses FUNCTION children so the\n tick can be rendered in JS, which defeats that inference: the\n trigger showed an empty value after selecting, and typeahead had\n nothing to match on. Neither is visible in the type system.\n */\n textValue={textValue ?? (typeof children === \"string\" ? children : undefined)}\n data-slot=\"select-item\"\n className={cn(selectItemVariants(), className)}\n >\n {({ isSelected }) => (\n <>\n {/*\n The tick is rendered in JS rather than toggled with CSS, so\n the precedence is explicit and a test can read it from the\n DOM. ps-8 above reserves the gutter, so selecting an option\n does not shift its label.\n */}\n {isSelected ? (\n <svg\n data-slot=\"select-item-check\"\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"absolute start-2 size-3.5 motion-safe:animate-select-check-in\"\n >\n <path d=\"m3.5 8.5 3 3 6-6\" />\n </svg>\n ) : null}\n {children}\n </>\n )}\n </AriaListBoxItem>\n );\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"select.js","names":["AriaSelectValue","Select","AriaSelect","AriaLabel","AriaButton","AriaFieldError","AriaPopover","AriaListBox","AriaListBoxItem"],"sources":["../../src/select/select.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useContext, useState, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n FieldError as AriaFieldError,\n Label as AriaLabel,\n ListBox as AriaListBox,\n ListBoxItem as AriaListBoxItem,\n Popover as AriaPopover,\n Select as AriaSelect,\n SelectStateContext,\n SelectValue as AriaSelectValue,\n type ListBoxItemProps as AriaListBoxItemProps,\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 { fieldSurfaceVariants } from \"../lib/field-surface.js\";\nimport { useIsomorphicLayoutEffect } from \"../lib/motion.js\";\nimport { RollingValue } from \"../lib/rolling-value.js\";\n\n/**\n * Select, ported from the legacy shadcn/Radix component.\n *\n * FOUR PARTS COLLAPSE INTO ONE. The census reads `SelectItem` 35 and\n * `Select`, `SelectTrigger`, `SelectValue`, `SelectContent` 15 each --\n * those four always appear together in the same shape, exactly like\n * Dialog's header trio, so they are one component and a prop.\n * `SelectGroup`, `SelectLabel`, `SelectSeparator`, `SelectScrollUpButton`\n * and `SelectScrollDownButton` have ZERO call sites and are not ported.\n *\n * `label` IS REQUIRED. The legacy exports a `SelectLabel` that nobody\n * renders and passes no `aria-label` anywhere, so all 15 selects are\n * unnamed comboboxes -- the same finding as Progress, Slider and\n * ScrollArea. Pass `labelHidden` when the surrounding UI already says it.\n *\n * The trigger reuses `fieldSurfaceVariants`, shared with Input and\n * Textarea. A select and a text field sit in the same form row and must\n * be the same box; the legacy styled them separately and drifted --\n * `h-10` here against Input's `h-9`, for no stated reason.\n *\n * The popover is `card`, not a `popover` role: `--popover` is\n * byte-identical to `--card` in the legacy theme. Item hover is `muted`,\n * not `accent`, for the same reason.\n */\n/*\n * `group` is load-bearing, not decoration. React Aria puts `data-invalid` on\n * THIS element, never on the trigger -- so the trigger's own\n * `data-[invalid]:` rule, inherited from the shared field surface, can never\n * match and `isInvalid` painted nothing at all. The trigger reads the state\n * from here instead.\n */\nexport const selectVariants = cva([\"group flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\nexport const selectTriggerVariants = cva(\n [\n // See selectVariants: the invalid state lives on the ROOT, so this reads it\n // from the group rather than from itself.\n \"group-data-[invalid]:border-destructive-strong\",\n fieldSurfaceVariants(),\n // While the menu is open the trigger is pressed and focus has moved into the\n // list, so the border keeps the darkness it had instead of falling back.\n \"[&:where([data-pressed])]:border-border-strong\",\n // The fill follows the border: hovered, pressed and open all read as one\n // state. Ordinary variants, not :where(): the base background is a plain\n // class, so a zero-specificity rule would lose to it. Nothing else paints\n // the background, and a disabled trigger is never hovered or pressed.\n \"data-[hovered]:not-data-[disabled]:bg-muted\",\n \"data-[pressed]:bg-muted\",\n \"group/trigger\",\n \"flex items-center justify-between gap-2 text-start\",\n \"leading-(--leading-normal) cursor-pointer\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n ],\n {\n variants: {\n /**\n * The same rungs and the same tokens as Input and Button. 14 of the\n * product's 32 `SelectTrigger` call sites override the height by hand,\n * and a Select sits in the same form row as a field -- giving Input the\n * axis without giving it here would be exactly the drift\n * `fieldSurfaceVariants` exists to prevent.\n *\n * Type matches Input's at every rung: 14px on `sm` (16px on a touch\n * screen), 16px on `md` and `lg`. A row whose two controls disagree\n * about type size reads as a mistake. The trigger is a button, so iOS\n * would not zoom on it; the coarse-pointer rule is there to keep it\n * equal to the field beside it.\n */\n size: {\n sm: \"h-(--density-control-h-sm) px-(--density-control-pad-sm) text-sm pointer-coarse:text-base\",\n md: \"h-(--density-control-h-md) px-(--density-control-pad-md) text-base\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg) text-base\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\nexport const selectPopoverVariants = cva(\n [\n // --trigger-width is set inline by React Aria, so the list matches the\n // control it came from rather than guessing a width.\n \"w-(--trigger-width) min-w-32 overflow-y-auto\",\n // Grows from the trigger edge: offset toward it, a little small and clipped to\n // the trigger's height, then opening to full height; 120ms out. React Aria\n // marks the first frame data-entering and the leaving one data-exiting and\n // keeps the list mounted until the animation ends.\n \"[--menu-x:0px] [--menu-y:-6px] origin-top\",\n // The panel also GROWS from the trigger's height: --menu-from is where the clip\n // starts, --menu-flip 1 grows it from the bottom edge when it opens upward.\n \"[--menu-from:var(--density-control-h-md)] [--menu-flip:0]\",\n \"data-[placement=top]:[--menu-y:6px] data-[placement=top]:origin-bottom data-[placement=top]:[--menu-flip:1]\",\n \"data-[placement=left]:[--menu-x:6px] data-[placement=left]:[--menu-y:0px] data-[placement=left]:origin-right\",\n \"data-[placement=right]:[--menu-x:-6px] data-[placement=right]:[--menu-y:0px] data-[placement=right]:origin-left\",\n \"motion-reduce:data-[entering]:animate-select-fade-in\",\n \"motion-safe:data-[exiting]:animate-select-popover-out motion-reduce:data-[exiting]:animate-select-fade-out\",\n \"data-[exiting]:pointer-events-none\",\n \"rounded-panel border-(length:--border-width) border-border p-1\",\n \"bg-card text-card-foreground shadow-floating\",\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n ],\n {\n variants: {\n /**\n * `grow` opens the panel from the trigger's height on the smooth spring (about 700ms). `fade` is the quick 240ms\n * settle for a list whose first row is a search field: a clipped panel would hide it while it grows.\n */\n reveal: {\n grow: \"motion-safe:data-[entering]:animate-select-popover-reveal\",\n fade: \"motion-safe:data-[entering]:animate-select-popover-in\",\n },\n },\n defaultVariants: { reveal: \"grow\" },\n },\n);\n\nexport const selectItemVariants = cva([\n \"relative flex cursor-pointer select-none items-center gap-2\",\n // The same 18px as a Menu row: the hover fill is a soft pill inside the panel.\n \"rounded-control py-1.5 pe-8 ps-2\",\n \"box-border text-sm leading-(--leading-normal) text-card-foreground\",\n // data-focused, not :hover. React Aria drives one highlight from both\n // pointer and keyboard, so the row under the mouse and the row the\n // arrows reached are the same row.\n \"outline-none data-[focused]:bg-muted\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-instant) motion-safe:ease-settle\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n]);\n\n/**\n * A single-choice picker.\n *\n * @example\n * <Select label=\"Country\" placeholder=\"Pick one\"\n * selectedKey={code} onSelectionChange={setCode}>\n * <SelectItem id=\"gb\">United Kingdom</SelectItem>\n * <SelectItem id=\"us\">United States</SelectItem>\n * </Select>\n */\nexport interface SelectProps extends Omit<\n AriaSelectProps<object>,\n \"className\" | \"style\" | \"children\" | \"items\"\n> {\n /** `SelectItem` children. */\n children?: ReactNode;\n /**\n * What is being chosen, and the control's accessible name.\n *\n * **Required.** All 15 legacy selects are unnamed: the legacy exports a\n * `SelectLabel` that nobody renders and passes no `aria-label`.\n */\n label: ReactNode;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /** Shown while nothing is selected. */\n placeholder?: string;\n /**\n * Why the selection is wrong. Rendered under the control, and it is what\n * makes `isInvalid` reach a screen reader.\n *\n * React Aria marks the ROOT `data-invalid` and leaves the button untouched,\n * and an `aria-invalid` handed to that button is dropped by\n * `filterDOMProps`, whose allowlist stops at the labelling attributes -- the\n * same silent loss as `aria-current` on a pagination button. So without a\n * message, an invalid select is invalid only in colour, which is WCAG 1.4.1\n * on its own. React Aria wires this into `aria-describedby` for us.\n */\n errorMessage?: ReactNode;\n /**\n * Control scale of the trigger. Height and inline padding come from the\n * same `--density-control-*` tokens as Input and Button, so a Select, a\n * field and a button on one rung are the same height.\n *\n * Type does not scale with it, matching Input.\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\n/**\n * The shown value. React Aria's own `SelectValue` stays, visually hidden, as the\n * text a screen reader reads; the copy the eye sees rolls when the selection\n * changes: a later option rises from below, an earlier one drops from above. The\n * direction is read from the option's place in the list. A pre-selected value\n * does not animate on first mount.\n */\nfunction ValueRoll({ placeholder }: { placeholder?: string }) {\n const state = useContext(SelectStateContext);\n const key = state?.selectedKey ?? null;\n let index = -1;\n if (state && key != null) {\n let n = 0;\n for (const item of state.collection) {\n if (item.type !== \"item\") continue;\n if (item.key === key) index = n;\n n += 1;\n }\n }\n const [previous, setPrevious] = useState(index);\n const [dir, setDir] = useState(1);\n if (previous !== index) {\n setPrevious(index);\n setDir(index > previous ? 1 : -1);\n }\n const text = state?.selectedItem?.textValue ?? \"\";\n // React Aria resolves a default selection after the first render, so the key\n // starts as null and then settles. Until the collection is built nothing is\n // shown, and the swap remounts once it is, so that settling is not a change.\n const built = (state?.collection.size ?? 0) > 0 || state?.selectedKey != null;\n const [mounted, setMounted] = useState(false);\n useIsomorphicLayoutEffect(() => setMounted(true), []);\n const settled = built || mounted;\n const swapKey = key == null ? \"placeholder\" : `value-${String(key)}`;\n return (\n <RollingValue\n srValue={\n <AriaSelectValue data-slot=\"select-value\" className=\"sr-only\">\n {({ isPlaceholder, selectedText }) => (isPlaceholder ? placeholder : selectedText)}\n </AriaSelectValue>\n }\n swapKey={swapKey}\n dir={dir}\n instanceKey={settled ? \"live\" : \"boot\"}\n >\n {key == null ? (\n <span data-slot=\"select-placeholder\" className=\"text-muted-foreground\">\n {placeholder}\n </span>\n ) : (\n text\n )}\n </RollingValue>\n );\n}\n\nexport const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select(\n { children, label, labelHidden, placeholder, errorMessage, size = \"md\", className, ...props },\n ref,\n) {\n return (\n <AriaSelect {...props} data-slot=\"select\" className={cn(selectVariants(), className)}>\n {/*\n The Label's own classes, so a Select's label and the Label over an\n Input in the same form are one style (same size, weight, colour and\n line height). The select-label slot stays for consumers that target it.\n */}\n <AriaLabel\n data-slot=\"select-label\"\n className={cnAll(labelVariants(), labelHidden && \"sr-only\")}\n >\n {label}\n </AriaLabel>\n\n {/*\n The ref goes on the TRIGGER, not the AriaSelect root above. The root\n is a field-level wrapper div; the trigger is the always-visible\n interactive surface a caller actually wants to focus or click,\n the same reasoning that puts Input's ref on the real control.\n */}\n <AriaButton ref={ref} data-slot=\"select-trigger\" className={selectTriggerVariants({ size })}>\n <ValueRoll placeholder={placeholder} />\n {/*\n aria-hidden: the chevron is decoration. The button already\n announces itself as a combobox, and a second name source only\n makes it announce twice.\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=\"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\n {errorMessage == null ? null : (\n // Not conditional on isInvalid: React Aria renders FieldError only\n // while the field IS invalid, so gating it here as well would just be\n // two sources of truth for one state.\n <AriaFieldError\n data-slot=\"select-error\"\n className=\"text-sm leading-(--leading-normal) text-destructive-strong\"\n >\n {errorMessage}\n </AriaFieldError>\n )}\n\n {/* No border colour on the list: it is lifted by its shadow alone. */}\n <AriaPopover\n data-slot=\"select-popover\"\n className={cn(selectPopoverVariants(), \"border-0\")}\n >\n <AriaListBox data-slot=\"select-list\" className=\"outline-none\">\n {children}\n </AriaListBox>\n </AriaPopover>\n </AriaSelect>\n );\n});\n\n/**\n * One option. Its `id` is the value.\n *\n * @example\n * <SelectItem id=\"gb\">United Kingdom</SelectItem>\n */\nexport interface SelectItemProps extends Omit<\n AriaListBoxItemProps,\n \"className\" | \"style\" | \"children\"\n> {\n children?: ReactNode;\n /**\n * The option's text, for the trigger and for typeahead.\n *\n * Derived from `children` when they are a string. Pass it explicitly\n * when the option renders anything richer than text.\n */\n textValue?: string;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(function SelectItem(\n { className, children, textValue, ...props },\n ref,\n) {\n return (\n <AriaListBoxItem\n ref={ref}\n {...props}\n /*\n textValue, derived from string children.\n React Aria infers an item's text from its children -- but ONLY\n when they are a string. This item uses FUNCTION children so the\n tick can be rendered in JS, which defeats that inference: the\n trigger showed an empty value after selecting, and typeahead had\n nothing to match on. Neither is visible in the type system.\n */\n textValue={textValue ?? (typeof children === \"string\" ? children : undefined)}\n data-slot=\"select-item\"\n className={cn(selectItemVariants(), className)}\n >\n {({ isSelected }) => (\n <>\n {/*\n The tick is rendered in JS rather than toggled with CSS, so\n the precedence is explicit and a test can read it from the\n DOM. ps-8 above reserves the gutter, so selecting an option\n does not shift its label.\n */}\n {isSelected ? (\n <svg\n data-slot=\"select-item-check\"\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"absolute end-2 size-3.5 motion-safe:animate-select-check-in\"\n >\n <path d=\"m3.5 8.5 3 3 6-6\" />\n </svg>\n ) : null}\n {children}\n </>\n )}\n </AriaListBoxItem>\n );\n});\n"],"mappings":"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"}
|
|
@@ -21,7 +21,7 @@ import { SeparatorProps } from "react-aria-components";
|
|
|
21
21
|
* simply never be emitted.
|
|
22
22
|
*/
|
|
23
23
|
export declare const separatorVariants: (props?: ({
|
|
24
|
-
orientation?: "
|
|
24
|
+
orientation?: "vertical" | "horizontal" | null | undefined;
|
|
25
25
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
26
26
|
/**
|
|
27
27
|
* A visual divider between two groups of content.
|
package/dist/sheet/sheet.js
CHANGED
|
@@ -91,7 +91,8 @@ const Sheet = forwardRef(function Sheet({ side = "right", className, onBack, bac
|
|
|
91
91
|
children: /* @__PURE__ */ jsx(ScrollArea, {
|
|
92
92
|
scrollResetKey,
|
|
93
93
|
label: bodyLabel ?? (typeof title === "string" ? title : ""),
|
|
94
|
-
className:
|
|
94
|
+
className: "min-h-0 flex-1",
|
|
95
|
+
viewportClassName: bodyClassName,
|
|
95
96
|
children
|
|
96
97
|
})
|
|
97
98
|
});
|
package/dist/sheet/sheet.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sheet.js","names":[],"sources":["../../src/sheet/sheet.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef } from \"react\";\n\nimport { Button } from \"../button/button.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { Dialog, type DialogProps } from \"../dialog/dialog.js\";\nimport { ScrollArea } from \"../scroll-area/scroll-area.js\";\n\n/**\n * Sheet: a dialog that arrives from an edge.\n *\n * A PRESET over `Dialog`, like `AlertDialog`. The legacy `sheet.tsx` is\n * `@radix-ui/react-dialog` again -- the same import, the same parts, the\n * same close button -- with one prop changing where it sits. Keeping a\n * second copy is how the legacy `dialog.tsx` and `alert-dialog.tsx`\n * drifted, and this is the same file for the third time.\n *\n * Census: 2 files, 3 uses. `side=\"right\"` twice, `side=\"left\"` once, and\n * `className` four times. `SheetHeader`, `SheetFooter`, `SheetTitle`,\n * `SheetDescription` and `SheetClose` are exported by the legacy and used\n * by NOBODY -- so all three sheets in the app render a `role=\"dialog\"`\n * with no title at all. Radix even warns about it in the console.\n *\n * `title` is required here, inherited from Dialog, so that is no longer\n * expressible.\n *\n * `side` is `\"left\" | \"right\"` only. The legacy also offers `\"top\"` and\n * `\"bottom\"`; both have zero call sites, and each is two lines to add the\n * day something needs one.\n *\n * A REAL BUG THE PORT FIXES: the legacy panel is `w-3/4 sm:max-w-sm`, and\n * `sm:max-w-sm` names `--container-sm`, which this repo does not define --\n * so in Alfred it would compile to NOTHING and the sheet would be 75% of\n * the viewport at every width, on a phone and on a 4K monitor alike. The\n * width here is a spacing-scale step that actually resolves.\n *\n * NOT PORTED: the slide-in animation. It comes from tailwindcss-animate,\n * which this repo does not ship. React Aria exposes `data-entering` and\n * `data-exiting`, and `--animate-*` tokens now exist, so it is a small\n * follow-up rather than a blocker.\n */\nexport const sheetOverlayVariants = cva(\"p-0\", {\n variants: {\n // The overlay is a flex box centring the panel. A sheet replaces that\n // centring with an edge alignment rather than positioning the panel\n // absolutely, so the scrim and the panel stay one layout.\n side: {\n left: \"justify-start items-stretch\",\n right: \"justify-end items-stretch\",\n },\n },\n defaultVariants: { side: \"right\" },\n});\n\nexport const sheetPanelVariants = cva(\n [\n // max-w-96 is 384px and RESOLVES. See the note above on sm:max-w-sm.\n \"h-full w-full max-w-96\",\n // Square against the edge it is attached to: a rounded corner flush\n // with the viewport edge reads as a rendering fault.\n \"rounded-none\",\n \"max-h-full overflow-y-auto\",\n ],\n {\n variants: {\n side: {\n left: \"border-y-0 border-s-0\",\n right: \"border-y-0 border-e-0\",\n },\n },\n defaultVariants: { side: \"right\" },\n },\n);\n\n/**\n * A panel that arrives from the side.\n *\n * @example\n * <Sheet isOpen={open} onOpenChange={setOpen} title=\"Filters\">\n * ...\n * </Sheet>\n */\nexport interface SheetProps extends Omit<DialogProps, \"overlayClassName\"> {\n /**\n * Which edge it is attached to.\n *\n * `\"top\"` and `\"bottom\"` exist in the legacy and have zero call sites.\n *\n * @default \"right\"\n */\n side?: \"left\" | \"right\";\n /**\n * Show a back control, and what it does.\n *\n * A side panel that goes a level deeper REPLACES its own contents rather\n * than stacking a second modal on top, so the browser's back affordance and\n * the escape key both do the wrong thing -- they close the whole panel\n * instead of returning one level. The product's own note calls this the\n * thing that \"makes items 119 and 120 one change rather than two\".\n */\n onBack?: () => void;\n /** Accessible name and label for the back control. @default \"Back\" */\n backLabel?: string;\n /**\n * Change this when the panel's contents change, and the body scrolls back\n * to the top.\n *\n * Going a level deeper and landing halfway down the new view is\n * disorienting, and it is the kind of thing every call site forgets --\n * which is why it belongs here rather than in each of them.\n */\n scrollResetKey?: string | number;\n /** Extra classes for the scrolling body region. */\n bodyClassName?: string;\n /**\n * Names the scrolling body region. Defaults to `title` when that is a\n * string, which it is at every call site in the product.\n *\n * The body is a `ScrollArea`, so it is focusable -- a scrollable region that\n * is not reachable by keyboard is WCAG 2.1.1, and axe caught exactly that\n * when this was a plain div. The product's own `SidePanelBody` still is one.\n * A focusable element needs a role and a role needs a name, so this is the\n * only combination that is actually correct.\n */\n bodyLabel?: string;\n}\n\nfunction BackArrowGlyph() {\n return (\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n // Logical, so it points the right way once the panel mirrors in RTL.\n className=\"size-(--icon-sm) rtl:-scale-x-100\"\n >\n <path d=\"M10 3 5 8l5 5\" />\n </svg>\n );\n}\n\nexport const Sheet = forwardRef<HTMLElement, SheetProps>(function Sheet(\n {\n side = \"right\",\n className,\n onBack,\n backLabel = \"Back\",\n scrollResetKey,\n bodyClassName,\n bodyLabel,\n title,\n children,\n ...props\n },\n ref,\n) {\n return (\n /*\n No data-slot here. Dialog spreads its props onto the ModalOverlay and\n writes data-slot=\"dialog-overlay\" AFTER the spread, so a data-slot\n passed from here is overwritten before it reaches the DOM -- it read as\n working while rendering nothing. A sheet is identified by its side\n variant and its panel classes instead.\n */\n <Dialog\n entrance=\"none\"\n {...props}\n ref={ref}\n title={title}\n beforeTitle={\n onBack ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n data-slot=\"sheet-back\"\n className=\"-ms-2 w-fit\"\n startIcon={<BackArrowGlyph />}\n onPress={onBack}\n >\n {backLabel}\n </Button>\n ) : undefined\n }\n overlayClassName={sheetOverlayVariants({ side })}\n className={cn(sheetPanelVariants({ side }), className)}\n >\n {/*\n The scrolling region is its own element so the title and the back\n control stay put while the content moves. A panel that scrolled as one\n block would take its own heading off screen.\n */}\n {/*\n No data-slot=\"sheet-body\" here, for the same reason the overlay has\n none: ScrollArea spreads its props and writes data-slot=\"scroll-area\"\n AFTER the spread, so one passed from here is overwritten before it\n reaches the DOM. It read as wired while selecting nothing.\n */}\n <ScrollArea\n scrollResetKey={scrollResetKey}\n label={bodyLabel ?? (typeof title === \"string\" ? title : \"\")}\n className
|
|
1
|
+
{"version":3,"file":"sheet.js","names":[],"sources":["../../src/sheet/sheet.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef } from \"react\";\n\nimport { Button } from \"../button/button.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { Dialog, type DialogProps } from \"../dialog/dialog.js\";\nimport { ScrollArea } from \"../scroll-area/scroll-area.js\";\n\n/**\n * Sheet: a dialog that arrives from an edge.\n *\n * A PRESET over `Dialog`, like `AlertDialog`. The legacy `sheet.tsx` is\n * `@radix-ui/react-dialog` again -- the same import, the same parts, the\n * same close button -- with one prop changing where it sits. Keeping a\n * second copy is how the legacy `dialog.tsx` and `alert-dialog.tsx`\n * drifted, and this is the same file for the third time.\n *\n * Census: 2 files, 3 uses. `side=\"right\"` twice, `side=\"left\"` once, and\n * `className` four times. `SheetHeader`, `SheetFooter`, `SheetTitle`,\n * `SheetDescription` and `SheetClose` are exported by the legacy and used\n * by NOBODY -- so all three sheets in the app render a `role=\"dialog\"`\n * with no title at all. Radix even warns about it in the console.\n *\n * `title` is required here, inherited from Dialog, so that is no longer\n * expressible.\n *\n * `side` is `\"left\" | \"right\"` only. The legacy also offers `\"top\"` and\n * `\"bottom\"`; both have zero call sites, and each is two lines to add the\n * day something needs one.\n *\n * A REAL BUG THE PORT FIXES: the legacy panel is `w-3/4 sm:max-w-sm`, and\n * `sm:max-w-sm` names `--container-sm`, which this repo does not define --\n * so in Alfred it would compile to NOTHING and the sheet would be 75% of\n * the viewport at every width, on a phone and on a 4K monitor alike. The\n * width here is a spacing-scale step that actually resolves.\n *\n * NOT PORTED: the slide-in animation. It comes from tailwindcss-animate,\n * which this repo does not ship. React Aria exposes `data-entering` and\n * `data-exiting`, and `--animate-*` tokens now exist, so it is a small\n * follow-up rather than a blocker.\n */\nexport const sheetOverlayVariants = cva(\"p-0\", {\n variants: {\n // The overlay is a flex box centring the panel. A sheet replaces that\n // centring with an edge alignment rather than positioning the panel\n // absolutely, so the scrim and the panel stay one layout.\n side: {\n left: \"justify-start items-stretch\",\n right: \"justify-end items-stretch\",\n },\n },\n defaultVariants: { side: \"right\" },\n});\n\nexport const sheetPanelVariants = cva(\n [\n // max-w-96 is 384px and RESOLVES. See the note above on sm:max-w-sm.\n \"h-full w-full max-w-96\",\n // Square against the edge it is attached to: a rounded corner flush\n // with the viewport edge reads as a rendering fault.\n \"rounded-none\",\n \"max-h-full overflow-y-auto\",\n ],\n {\n variants: {\n side: {\n left: \"border-y-0 border-s-0\",\n right: \"border-y-0 border-e-0\",\n },\n },\n defaultVariants: { side: \"right\" },\n },\n);\n\n/**\n * A panel that arrives from the side.\n *\n * @example\n * <Sheet isOpen={open} onOpenChange={setOpen} title=\"Filters\">\n * ...\n * </Sheet>\n */\nexport interface SheetProps extends Omit<DialogProps, \"overlayClassName\"> {\n /**\n * Which edge it is attached to.\n *\n * `\"top\"` and `\"bottom\"` exist in the legacy and have zero call sites.\n *\n * @default \"right\"\n */\n side?: \"left\" | \"right\";\n /**\n * Show a back control, and what it does.\n *\n * A side panel that goes a level deeper REPLACES its own contents rather\n * than stacking a second modal on top, so the browser's back affordance and\n * the escape key both do the wrong thing -- they close the whole panel\n * instead of returning one level. The product's own note calls this the\n * thing that \"makes items 119 and 120 one change rather than two\".\n */\n onBack?: () => void;\n /** Accessible name and label for the back control. @default \"Back\" */\n backLabel?: string;\n /**\n * Change this when the panel's contents change, and the body scrolls back\n * to the top.\n *\n * Going a level deeper and landing halfway down the new view is\n * disorienting, and it is the kind of thing every call site forgets --\n * which is why it belongs here rather than in each of them.\n */\n scrollResetKey?: string | number;\n /** Extra classes for the scrolling body region. */\n bodyClassName?: string;\n /**\n * Names the scrolling body region. Defaults to `title` when that is a\n * string, which it is at every call site in the product.\n *\n * The body is a `ScrollArea`, so it is focusable -- a scrollable region that\n * is not reachable by keyboard is WCAG 2.1.1, and axe caught exactly that\n * when this was a plain div. The product's own `SidePanelBody` still is one.\n * A focusable element needs a role and a role needs a name, so this is the\n * only combination that is actually correct.\n */\n bodyLabel?: string;\n}\n\nfunction BackArrowGlyph() {\n return (\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n // Logical, so it points the right way once the panel mirrors in RTL.\n className=\"size-(--icon-sm) rtl:-scale-x-100\"\n >\n <path d=\"M10 3 5 8l5 5\" />\n </svg>\n );\n}\n\nexport const Sheet = forwardRef<HTMLElement, SheetProps>(function Sheet(\n {\n side = \"right\",\n className,\n onBack,\n backLabel = \"Back\",\n scrollResetKey,\n bodyClassName,\n bodyLabel,\n title,\n children,\n ...props\n },\n ref,\n) {\n return (\n /*\n No data-slot here. Dialog spreads its props onto the ModalOverlay and\n writes data-slot=\"dialog-overlay\" AFTER the spread, so a data-slot\n passed from here is overwritten before it reaches the DOM -- it read as\n working while rendering nothing. A sheet is identified by its side\n variant and its panel classes instead.\n */\n <Dialog\n entrance=\"none\"\n {...props}\n ref={ref}\n title={title}\n beforeTitle={\n onBack ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n data-slot=\"sheet-back\"\n className=\"-ms-2 w-fit\"\n startIcon={<BackArrowGlyph />}\n onPress={onBack}\n >\n {backLabel}\n </Button>\n ) : undefined\n }\n overlayClassName={sheetOverlayVariants({ side })}\n className={cn(sheetPanelVariants({ side }), className)}\n >\n {/*\n The scrolling region is its own element so the title and the back\n control stay put while the content moves. A panel that scrolled as one\n block would take its own heading off screen.\n */}\n {/*\n No data-slot=\"sheet-body\" here, for the same reason the overlay has\n none: ScrollArea spreads its props and writes data-slot=\"scroll-area\"\n AFTER the spread, so one passed from here is overwritten before it\n reaches the DOM. It read as wired while selecting nothing.\n */}\n <ScrollArea\n scrollResetKey={scrollResetKey}\n label={bodyLabel ?? (typeof title === \"string\" ? title : \"\")}\n className=\"min-h-0 flex-1\"\n viewportClassName={bodyClassName}\n >\n {children}\n </ScrollArea>\n </Dialog>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAa,uBAAuB,IAAI,OAAO;CAC7C,UAAU,EAIR,MAAM;EACJ,MAAM;EACN,OAAO;CACT,EACF;CACA,iBAAiB,EAAE,MAAM,QAAQ;AACnC,CAAC;AAED,MAAa,qBAAqB,IAChC;CAEE;CAGA;CACA;AACF,GACA;CACE,UAAU,EACR,MAAM;EACJ,MAAM;EACN,OAAO;CACT,EACF;CACA,iBAAiB,EAAE,MAAM,QAAQ;AACnC,CACF;AAuDA,SAAS,iBAAiB;CACxB,OACE,oBAAC,OAAD;EACE,eAAY;EACZ,SAAQ;EACR,MAAK;EACL,QAAO;EACP,aAAY;EACZ,eAAc;EACd,gBAAe;EAEf,WAAU;EAEV,UAAA,oBAAC,QAAD,EAAM,GAAE,gBAAiB,CAAA;CACtB,CAAA;AAET;AAEA,MAAa,QAAQ,WAAoC,SAAS,MAChE,EACE,OAAO,SACP,WACA,QACA,YAAY,QACZ,gBACA,eACA,WACA,OACA,UACA,GAAG,SAEL,KACA;CACA,OAQE,oBAAC,QAAD;EACE,UAAS;EACT,GAAI;EACC;EACE;EACP,aACE,SACE,oBAAC,QAAD;GACE,SAAQ;GACR,MAAK;GACL,aAAU;GACV,WAAU;GACV,WAAW,oBAAC,gBAAD,CAAiB,CAAA;GAC5B,SAAS;GAER,UAAA;EACK,CAAA,IACN,KAAA;EAEN,kBAAkB,qBAAqB,EAAE,KAAK,CAAC;EAC/C,WAAW,GAAG,mBAAmB,EAAE,KAAK,CAAC,GAAG,SAAS;EAarD,UAAA,oBAAC,YAAD;GACkB;GAChB,OAAO,cAAc,OAAO,UAAU,WAAW,QAAQ;GACzD,WAAU;GACV,mBAAmB;GAElB;EACS,CAAA;CACN,CAAA;AAEZ,CAAC"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Platform, ShortcutToken, formatShortcut, matchesShortcut, normalizeShortcut, shortcutFromEvent, shortcutTokens } from "./shortcut-model.js";
|
|
2
|
+
import { ShortcutBinding, ShortcutKbd, ShortcutKbdProps, ShortcutKeys, ShortcutKeysProps, ShortcutRecorder, ShortcutRecorderProps, usePlatform, usePressedKeys } from "./shortcut-recorder.js";
|
|
3
|
+
export { type Platform, type ShortcutBinding, ShortcutKbd, type ShortcutKbdProps, ShortcutKeys, type ShortcutKeysProps, ShortcutRecorder, type ShortcutRecorderProps, type ShortcutToken, formatShortcut, matchesShortcut, normalizeShortcut, shortcutFromEvent, shortcutTokens, usePlatform, usePressedKeys };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { formatShortcut, matchesShortcut, normalizeShortcut, shortcutFromEvent, shortcutTokens } from "./shortcut-model.js";
|
|
2
|
+
import { ShortcutKbd, ShortcutKeys, ShortcutRecorder, usePlatform, usePressedKeys } from "./shortcut-recorder.js";
|
|
3
|
+
export { ShortcutKbd, ShortcutKeys, ShortcutRecorder, formatShortcut, matchesShortcut, normalizeShortcut, shortcutFromEvent, shortcutTokens, usePlatform, usePressedKeys };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/shortcut-recorder/shortcut-model.d.ts
|
|
2
|
+
export type Platform = "mac" | "other";
|
|
3
|
+
export interface ShortcutToken {
|
|
4
|
+
/** What the key cap shows. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Physical key id, matched against held keys. */
|
|
7
|
+
id: string;
|
|
8
|
+
/** How a screen reader says it. */
|
|
9
|
+
spoken: string;
|
|
10
|
+
}
|
|
11
|
+
/** The parts of a keyboard event the model reads, so a test can pass a plain object. */
|
|
12
|
+
export type KeyEventLike = Pick<KeyboardEvent, "key" | "code" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey">;
|
|
13
|
+
/** One spelling per combination: on a Mac, Command is `mod`; elsewhere, Ctrl is. Modifiers come in a fixed order. */
|
|
14
|
+
export declare function normalizeShortcut(shortcut: string, platform: Platform): string;
|
|
15
|
+
/** Reads a keydown into a shortcut string, or `null` while only modifiers are down. */
|
|
16
|
+
export declare function shortcutFromEvent(event: KeyEventLike, platform: Platform): string | null;
|
|
17
|
+
/** True when a keydown is this shortcut. Use it to run the action a recorded shortcut is bound to. */
|
|
18
|
+
export declare function matchesShortcut(event: KeyEventLike, shortcut: string, platform: Platform): boolean;
|
|
19
|
+
/** Key caps for a shortcut in platform order: Control, Option, Shift, Command symbols on a Mac; Ctrl, Alt, Shift, Win elsewhere. */
|
|
20
|
+
export declare function shortcutTokens(shortcut: string, platform: Platform): ShortcutToken[];
|
|
21
|
+
/** The shortcut as text ("⌘⇧K" on a Mac, "Ctrl+Shift+K" elsewhere), plus a spoken form for assistive technology. */
|
|
22
|
+
export declare function formatShortcut(shortcut: string, platform: Platform): {
|
|
23
|
+
text: string;
|
|
24
|
+
spoken: string;
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
//# sourceMappingURL=shortcut-model.d.ts.map
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
//#region src/shortcut-recorder/shortcut-model.ts
|
|
2
|
+
const MODIFIER_ORDER = [
|
|
3
|
+
"mod",
|
|
4
|
+
"ctrl",
|
|
5
|
+
"alt",
|
|
6
|
+
"shift",
|
|
7
|
+
"meta"
|
|
8
|
+
];
|
|
9
|
+
const isModifier = (part) => MODIFIER_ORDER.includes(part);
|
|
10
|
+
const CODE_KEYS = {
|
|
11
|
+
Comma: ",",
|
|
12
|
+
Period: ".",
|
|
13
|
+
Slash: "/",
|
|
14
|
+
Semicolon: ";",
|
|
15
|
+
Quote: "'",
|
|
16
|
+
BracketLeft: "[",
|
|
17
|
+
BracketRight: "]",
|
|
18
|
+
Backslash: "\\",
|
|
19
|
+
Minus: "-",
|
|
20
|
+
Equal: "=",
|
|
21
|
+
Backquote: "`",
|
|
22
|
+
Space: "space",
|
|
23
|
+
Enter: "enter",
|
|
24
|
+
NumpadEnter: "enter",
|
|
25
|
+
Escape: "escape",
|
|
26
|
+
Backspace: "backspace",
|
|
27
|
+
Delete: "delete",
|
|
28
|
+
Tab: "tab",
|
|
29
|
+
ArrowUp: "up",
|
|
30
|
+
ArrowDown: "down",
|
|
31
|
+
ArrowLeft: "left",
|
|
32
|
+
ArrowRight: "right",
|
|
33
|
+
Home: "home",
|
|
34
|
+
End: "end",
|
|
35
|
+
PageUp: "pageup",
|
|
36
|
+
PageDown: "pagedown"
|
|
37
|
+
};
|
|
38
|
+
const MODIFIER_KEYS = {
|
|
39
|
+
Meta: "meta",
|
|
40
|
+
OS: "meta",
|
|
41
|
+
Control: "ctrl",
|
|
42
|
+
Alt: "alt",
|
|
43
|
+
AltGraph: "alt",
|
|
44
|
+
Shift: "shift"
|
|
45
|
+
};
|
|
46
|
+
const HELD_MODIFIERS = [
|
|
47
|
+
"meta",
|
|
48
|
+
"ctrl",
|
|
49
|
+
"alt",
|
|
50
|
+
"shift"
|
|
51
|
+
];
|
|
52
|
+
/** The physical key id for an event: "k", "1", "enter", "meta", "shift"... `null` for keys that never make a shortcut, such as Caps Lock. */
|
|
53
|
+
function keyId(event) {
|
|
54
|
+
const { code, key } = event;
|
|
55
|
+
if (MODIFIER_KEYS[key]) return MODIFIER_KEYS[key] ?? null;
|
|
56
|
+
if (/^Key[A-Z]$/.test(code)) return code.slice(3).toLowerCase();
|
|
57
|
+
if (/^Digit\d$/.test(code)) return code.slice(5);
|
|
58
|
+
if (/^Numpad\d$/.test(code)) return code.slice(6);
|
|
59
|
+
if (CODE_KEYS[code]) return CODE_KEYS[code] ?? null;
|
|
60
|
+
if (/^F\d{1,2}$/.test(key)) return key.toLowerCase();
|
|
61
|
+
if (key === "CapsLock" || key === "Fn" || key === "Dead" || key === "Unidentified" || key === "Process") return null;
|
|
62
|
+
return key.length === 1 ? key.toLowerCase() : null;
|
|
63
|
+
}
|
|
64
|
+
function splitShortcut(shortcut) {
|
|
65
|
+
const parts = shortcut.toLowerCase().split("+").map((part) => part.trim()).filter(Boolean);
|
|
66
|
+
const key = parts.filter((part) => !isModifier(part)).pop() ?? "";
|
|
67
|
+
return {
|
|
68
|
+
mods: new Set(parts.filter(isModifier)),
|
|
69
|
+
key
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** One spelling per combination: on a Mac, Command is `mod`; elsewhere, Ctrl is. Modifiers come in a fixed order. */
|
|
73
|
+
function normalizeShortcut(shortcut, platform) {
|
|
74
|
+
const { mods, key } = splitShortcut(shortcut);
|
|
75
|
+
if (platform === "mac" && mods.delete("meta")) mods.add("mod");
|
|
76
|
+
if (platform === "other" && mods.delete("ctrl")) mods.add("mod");
|
|
77
|
+
return [...MODIFIER_ORDER.filter((mod) => mods.has(mod)), key].filter(Boolean).join("+");
|
|
78
|
+
}
|
|
79
|
+
/** The modifiers an event holds, in the model's spelling. */
|
|
80
|
+
function modifiersFromEvent(event, platform) {
|
|
81
|
+
const mods = [];
|
|
82
|
+
if (platform === "mac" ? event.metaKey : event.ctrlKey) mods.push("mod");
|
|
83
|
+
if (platform === "mac" && event.ctrlKey) mods.push("ctrl");
|
|
84
|
+
if (event.altKey) mods.push("alt");
|
|
85
|
+
if (event.shiftKey) mods.push("shift");
|
|
86
|
+
if (platform === "other" && event.metaKey) mods.push("meta");
|
|
87
|
+
return mods;
|
|
88
|
+
}
|
|
89
|
+
/** Reads a keydown into a shortcut string, or `null` while only modifiers are down. */
|
|
90
|
+
function shortcutFromEvent(event, platform) {
|
|
91
|
+
const key = keyId(event);
|
|
92
|
+
if (!key || HELD_MODIFIERS.includes(key)) return null;
|
|
93
|
+
return normalizeShortcut([...modifiersFromEvent(event, platform), key].join("+"), platform);
|
|
94
|
+
}
|
|
95
|
+
/** True when a keydown is this shortcut. Use it to run the action a recorded shortcut is bound to. */
|
|
96
|
+
function matchesShortcut(event, shortcut, platform) {
|
|
97
|
+
const pressed = shortcutFromEvent(event, platform);
|
|
98
|
+
return !!pressed && pressed === normalizeShortcut(shortcut, platform);
|
|
99
|
+
}
|
|
100
|
+
const MAC_KEYS = {
|
|
101
|
+
enter: ["↩", "Return"],
|
|
102
|
+
backspace: ["⌫", "Delete"],
|
|
103
|
+
delete: ["⌦", "Forward delete"],
|
|
104
|
+
tab: ["⇥", "Tab"],
|
|
105
|
+
escape: ["esc", "Escape"]
|
|
106
|
+
};
|
|
107
|
+
const SHARED_KEYS = {
|
|
108
|
+
space: ["Space", "Space"],
|
|
109
|
+
enter: ["Enter", "Enter"],
|
|
110
|
+
escape: ["Esc", "Escape"],
|
|
111
|
+
backspace: ["Backspace", "Backspace"],
|
|
112
|
+
delete: ["Del", "Delete"],
|
|
113
|
+
tab: ["Tab", "Tab"],
|
|
114
|
+
up: ["↑", "Up arrow"],
|
|
115
|
+
down: ["↓", "Down arrow"],
|
|
116
|
+
left: ["←", "Left arrow"],
|
|
117
|
+
right: ["→", "Right arrow"],
|
|
118
|
+
home: ["Home", "Home"],
|
|
119
|
+
end: ["End", "End"],
|
|
120
|
+
pageup: ["PgUp", "Page up"],
|
|
121
|
+
pagedown: ["PgDn", "Page down"],
|
|
122
|
+
",": [",", "Comma"],
|
|
123
|
+
".": [".", "Period"],
|
|
124
|
+
"/": ["/", "Slash"],
|
|
125
|
+
";": [";", "Semicolon"],
|
|
126
|
+
"'": ["'", "Quote"],
|
|
127
|
+
"[": ["[", "Left bracket"],
|
|
128
|
+
"]": ["]", "Right bracket"],
|
|
129
|
+
"\\": ["\\", "Backslash"],
|
|
130
|
+
"-": ["-", "Minus"],
|
|
131
|
+
"=": ["=", "Equals"],
|
|
132
|
+
"`": ["`", "Backtick"]
|
|
133
|
+
};
|
|
134
|
+
/** Key caps for a shortcut in platform order: Control, Option, Shift, Command symbols on a Mac; Ctrl, Alt, Shift, Win elsewhere. */
|
|
135
|
+
function shortcutTokens(shortcut, platform) {
|
|
136
|
+
const { mods, key } = splitShortcut(normalizeShortcut(shortcut, platform));
|
|
137
|
+
const tokens = [];
|
|
138
|
+
if (platform === "mac") {
|
|
139
|
+
if (mods.has("ctrl")) tokens.push({
|
|
140
|
+
label: "⌃",
|
|
141
|
+
id: "ctrl",
|
|
142
|
+
spoken: "Control"
|
|
143
|
+
});
|
|
144
|
+
if (mods.has("alt")) tokens.push({
|
|
145
|
+
label: "⌥",
|
|
146
|
+
id: "alt",
|
|
147
|
+
spoken: "Option"
|
|
148
|
+
});
|
|
149
|
+
if (mods.has("shift")) tokens.push({
|
|
150
|
+
label: "⇧",
|
|
151
|
+
id: "shift",
|
|
152
|
+
spoken: "Shift"
|
|
153
|
+
});
|
|
154
|
+
if (mods.has("mod") || mods.has("meta")) tokens.push({
|
|
155
|
+
label: "⌘",
|
|
156
|
+
id: "meta",
|
|
157
|
+
spoken: "Command"
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
if (mods.has("mod") || mods.has("ctrl")) tokens.push({
|
|
161
|
+
label: "Ctrl",
|
|
162
|
+
id: "ctrl",
|
|
163
|
+
spoken: "Control"
|
|
164
|
+
});
|
|
165
|
+
if (mods.has("alt")) tokens.push({
|
|
166
|
+
label: "Alt",
|
|
167
|
+
id: "alt",
|
|
168
|
+
spoken: "Alt"
|
|
169
|
+
});
|
|
170
|
+
if (mods.has("shift")) tokens.push({
|
|
171
|
+
label: "Shift",
|
|
172
|
+
id: "shift",
|
|
173
|
+
spoken: "Shift"
|
|
174
|
+
});
|
|
175
|
+
if (mods.has("meta")) tokens.push({
|
|
176
|
+
label: "Win",
|
|
177
|
+
id: "meta",
|
|
178
|
+
spoken: "Windows"
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (key) {
|
|
182
|
+
const named = (platform === "mac" ? MAC_KEYS[key] : void 0) ?? SHARED_KEYS[key];
|
|
183
|
+
tokens.push(named ? {
|
|
184
|
+
label: named[0],
|
|
185
|
+
id: key,
|
|
186
|
+
spoken: named[1]
|
|
187
|
+
} : {
|
|
188
|
+
label: key.toUpperCase(),
|
|
189
|
+
id: key,
|
|
190
|
+
spoken: key.toUpperCase()
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return tokens;
|
|
194
|
+
}
|
|
195
|
+
/** The shortcut as text ("⌘⇧K" on a Mac, "Ctrl+Shift+K" elsewhere), plus a spoken form for assistive technology. */
|
|
196
|
+
function formatShortcut(shortcut, platform) {
|
|
197
|
+
const tokens = shortcutTokens(shortcut, platform);
|
|
198
|
+
return {
|
|
199
|
+
text: tokens.map((token) => token.label).join(platform === "mac" ? "" : "+"),
|
|
200
|
+
spoken: tokens.map((token) => token.spoken).join(" ")
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
export { formatShortcut, keyId, matchesShortcut, modifiersFromEvent, normalizeShortcut, shortcutFromEvent, shortcutTokens, splitShortcut };
|
|
205
|
+
|
|
206
|
+
//# sourceMappingURL=shortcut-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shortcut-model.js","names":[],"sources":["../../src/shortcut-recorder/shortcut-model.ts"],"sourcesContent":["/*\n * The shortcut model. A shortcut is a string such as \"mod+shift+k\". `mod` is\n * Command on Apple platforms and Control elsewhere, so one binding reads right\n * everywhere. Keys come from the physical key (`event.code`) where it matters,\n * so Option+K records as \"alt+k\" instead of the character it types.\n */\n\nexport type Platform = \"mac\" | \"other\";\n\nexport interface ShortcutToken {\n /** What the key cap shows. */\n label: string;\n /** Physical key id, matched against held keys. */\n id: string;\n /** How a screen reader says it. */\n spoken: string;\n}\n\n/** The parts of a keyboard event the model reads, so a test can pass a plain object. */\nexport type KeyEventLike = Pick<\n KeyboardEvent,\n \"key\" | \"code\" | \"metaKey\" | \"ctrlKey\" | \"altKey\" | \"shiftKey\"\n>;\n\nconst MODIFIER_ORDER = [\"mod\", \"ctrl\", \"alt\", \"shift\", \"meta\"] as const;\ntype Modifier = (typeof MODIFIER_ORDER)[number];\nconst isModifier = (part: string): part is Modifier =>\n (MODIFIER_ORDER as readonly string[]).includes(part);\n\nconst CODE_KEYS: Record<string, string> = {\n Comma: \",\",\n Period: \".\",\n Slash: \"/\",\n Semicolon: \";\",\n Quote: \"'\",\n BracketLeft: \"[\",\n BracketRight: \"]\",\n Backslash: \"\\\\\",\n Minus: \"-\",\n Equal: \"=\",\n Backquote: \"`\",\n Space: \"space\",\n Enter: \"enter\",\n NumpadEnter: \"enter\",\n Escape: \"escape\",\n Backspace: \"backspace\",\n Delete: \"delete\",\n Tab: \"tab\",\n ArrowUp: \"up\",\n ArrowDown: \"down\",\n ArrowLeft: \"left\",\n ArrowRight: \"right\",\n Home: \"home\",\n End: \"end\",\n PageUp: \"pageup\",\n PageDown: \"pagedown\",\n};\n\nconst MODIFIER_KEYS: Record<string, string> = {\n Meta: \"meta\",\n OS: \"meta\",\n Control: \"ctrl\",\n Alt: \"alt\",\n AltGraph: \"alt\",\n Shift: \"shift\",\n};\n\nconst HELD_MODIFIERS = [\"meta\", \"ctrl\", \"alt\", \"shift\"];\n\n/** The physical key id for an event: \"k\", \"1\", \"enter\", \"meta\", \"shift\"... `null` for keys that never make a shortcut, such as Caps Lock. */\nexport function keyId(event: Pick<KeyboardEvent, \"key\" | \"code\">): string | null {\n const { code, key } = event;\n if (MODIFIER_KEYS[key]) return MODIFIER_KEYS[key] ?? null;\n if (/^Key[A-Z]$/.test(code)) return code.slice(3).toLowerCase();\n if (/^Digit\\d$/.test(code)) return code.slice(5);\n if (/^Numpad\\d$/.test(code)) return code.slice(6);\n if (CODE_KEYS[code]) return CODE_KEYS[code] ?? null;\n if (/^F\\d{1,2}$/.test(key)) return key.toLowerCase();\n if (\n key === \"CapsLock\" ||\n key === \"Fn\" ||\n key === \"Dead\" ||\n key === \"Unidentified\" ||\n key === \"Process\"\n )\n return null;\n return key.length === 1 ? key.toLowerCase() : null;\n}\n\nexport function splitShortcut(shortcut: string) {\n const parts = shortcut\n .toLowerCase()\n .split(\"+\")\n .map((part) => part.trim())\n .filter(Boolean);\n const key = parts.filter((part) => !isModifier(part)).pop() ?? \"\";\n return { mods: new Set(parts.filter(isModifier)), key };\n}\n\n/** One spelling per combination: on a Mac, Command is `mod`; elsewhere, Ctrl is. Modifiers come in a fixed order. */\nexport function normalizeShortcut(shortcut: string, platform: Platform) {\n const { mods, key } = splitShortcut(shortcut);\n if (platform === \"mac\" && mods.delete(\"meta\")) mods.add(\"mod\");\n if (platform === \"other\" && mods.delete(\"ctrl\")) mods.add(\"mod\");\n return [...MODIFIER_ORDER.filter((mod) => mods.has(mod)), key].filter(Boolean).join(\"+\");\n}\n\n/** The modifiers an event holds, in the model's spelling. */\nexport function modifiersFromEvent(\n event: Pick<KeyEventLike, \"metaKey\" | \"ctrlKey\" | \"altKey\" | \"shiftKey\">,\n platform: Platform,\n) {\n const mods: string[] = [];\n if (platform === \"mac\" ? event.metaKey : event.ctrlKey) mods.push(\"mod\");\n if (platform === \"mac\" && event.ctrlKey) mods.push(\"ctrl\");\n if (event.altKey) mods.push(\"alt\");\n if (event.shiftKey) mods.push(\"shift\");\n if (platform === \"other\" && event.metaKey) mods.push(\"meta\");\n return mods;\n}\n\n/** Reads a keydown into a shortcut string, or `null` while only modifiers are down. */\nexport function shortcutFromEvent(event: KeyEventLike, platform: Platform) {\n const key = keyId(event);\n if (!key || HELD_MODIFIERS.includes(key)) return null;\n return normalizeShortcut([...modifiersFromEvent(event, platform), key].join(\"+\"), platform);\n}\n\n/** True when a keydown is this shortcut. Use it to run the action a recorded shortcut is bound to. */\nexport function matchesShortcut(event: KeyEventLike, shortcut: string, platform: Platform) {\n const pressed = shortcutFromEvent(event, platform);\n return !!pressed && pressed === normalizeShortcut(shortcut, platform);\n}\n\nconst MAC_KEYS: Record<string, [string, string]> = {\n enter: [\"↩\", \"Return\"],\n backspace: [\"⌫\", \"Delete\"],\n delete: [\"⌦\", \"Forward delete\"],\n tab: [\"⇥\", \"Tab\"],\n escape: [\"esc\", \"Escape\"],\n};\n\nconst SHARED_KEYS: Record<string, [string, string]> = {\n space: [\"Space\", \"Space\"],\n enter: [\"Enter\", \"Enter\"],\n escape: [\"Esc\", \"Escape\"],\n backspace: [\"Backspace\", \"Backspace\"],\n delete: [\"Del\", \"Delete\"],\n tab: [\"Tab\", \"Tab\"],\n up: [\"↑\", \"Up arrow\"],\n down: [\"↓\", \"Down arrow\"],\n left: [\"←\", \"Left arrow\"],\n right: [\"→\", \"Right arrow\"],\n home: [\"Home\", \"Home\"],\n end: [\"End\", \"End\"],\n pageup: [\"PgUp\", \"Page up\"],\n pagedown: [\"PgDn\", \"Page down\"],\n \",\": [\",\", \"Comma\"],\n \".\": [\".\", \"Period\"],\n \"/\": [\"/\", \"Slash\"],\n \";\": [\";\", \"Semicolon\"],\n \"'\": [\"'\", \"Quote\"],\n \"[\": [\"[\", \"Left bracket\"],\n \"]\": [\"]\", \"Right bracket\"],\n \"\\\\\": [\"\\\\\", \"Backslash\"],\n \"-\": [\"-\", \"Minus\"],\n \"=\": [\"=\", \"Equals\"],\n \"`\": [\"`\", \"Backtick\"],\n};\n\n/** Key caps for a shortcut in platform order: Control, Option, Shift, Command symbols on a Mac; Ctrl, Alt, Shift, Win elsewhere. */\nexport function shortcutTokens(shortcut: string, platform: Platform): ShortcutToken[] {\n const { mods, key } = splitShortcut(normalizeShortcut(shortcut, platform));\n const tokens: ShortcutToken[] = [];\n if (platform === \"mac\") {\n if (mods.has(\"ctrl\")) tokens.push({ label: \"⌃\", id: \"ctrl\", spoken: \"Control\" });\n if (mods.has(\"alt\")) tokens.push({ label: \"⌥\", id: \"alt\", spoken: \"Option\" });\n if (mods.has(\"shift\")) tokens.push({ label: \"⇧\", id: \"shift\", spoken: \"Shift\" });\n if (mods.has(\"mod\") || mods.has(\"meta\"))\n tokens.push({ label: \"⌘\", id: \"meta\", spoken: \"Command\" });\n } else {\n if (mods.has(\"mod\") || mods.has(\"ctrl\"))\n tokens.push({ label: \"Ctrl\", id: \"ctrl\", spoken: \"Control\" });\n if (mods.has(\"alt\")) tokens.push({ label: \"Alt\", id: \"alt\", spoken: \"Alt\" });\n if (mods.has(\"shift\")) tokens.push({ label: \"Shift\", id: \"shift\", spoken: \"Shift\" });\n if (mods.has(\"meta\")) tokens.push({ label: \"Win\", id: \"meta\", spoken: \"Windows\" });\n }\n if (key) {\n const named = (platform === \"mac\" ? MAC_KEYS[key] : undefined) ?? SHARED_KEYS[key];\n tokens.push(\n named\n ? { label: named[0], id: key, spoken: named[1] }\n : { label: key.toUpperCase(), id: key, spoken: key.toUpperCase() },\n );\n }\n return tokens;\n}\n\n/** The shortcut as text (\"⌘⇧K\" on a Mac, \"Ctrl+Shift+K\" elsewhere), plus a spoken form for assistive technology. */\nexport function formatShortcut(shortcut: string, platform: Platform) {\n const tokens = shortcutTokens(shortcut, platform);\n return {\n text: tokens.map((token) => token.label).join(platform === \"mac\" ? \"\" : \"+\"),\n spoken: tokens.map((token) => token.spoken).join(\" \"),\n };\n}\n"],"mappings":"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"}
|