@seekalfred/primitives 0.6.0 → 0.7.3
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 +214 -15
- 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 +53 -4
- package/dist/alert/alert.js +100 -11
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert/index.d.ts +2 -2
- package/dist/alert/index.js +2 -2
- package/dist/alert-dialog/alert-dialog.d.ts +58 -18
- package/dist/alert-dialog/alert-dialog.js +152 -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 +1 -1
- 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/calendar/calendar.d.ts +1 -1
- package/dist/card/card.d.ts +22 -1
- package/dist/card/card.js +60 -7
- 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 +77 -0
- package/dist/carousel/carousel.js +415 -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/copy-button/copy-button.d.ts +18 -2
- package/dist/copy-button/copy-button.js +29 -8
- package/dist/copy-button/copy-button.js.map +1 -1
- 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 +86 -0
- package/dist/donut-chart/donut-chart.js +593 -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 +42 -10
- package/dist/empty-state/empty-state.js +44 -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 +50 -26
- package/dist/filter-bar/filter-bar.js +317 -157
- 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 +53 -10
- package/dist/index.js +52 -12
- package/dist/input/input.js +4 -4
- package/dist/input/input.js.map +1 -1
- 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-shell.js +10 -3
- package/dist/lib/field-shell.js.map +1 -1
- package/dist/lib/field-surface.js +31 -9
- 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/lib/use-copy-feedback.js +17 -3
- package/dist/lib/use-copy-feedback.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 +116 -0
- package/dist/line-chart/line-chart.js +740 -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 +86 -0
- package/dist/money-input/money-input.js +613 -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 +89 -0
- package/dist/morph-select/morph-select.js +712 -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 +77 -0
- package/dist/multi-select/multi-select.js +209 -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.d.ts +16 -0
- package/dist/password-field/password-field.js +39 -10
- 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/index.d.ts +2 -2
- package/dist/popover/index.js +2 -2
- package/dist/popover/popover.d.ts +51 -2
- package/dist/popover/popover.js +59 -8
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/progress.d.ts +16 -2
- package/dist/progress/progress.js +16 -9
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/index.d.ts +2 -2
- package/dist/radio-group/index.js +2 -2
- package/dist/radio-group/radio-group.d.ts +33 -3
- package/dist/radio-group/radio-group.js +110 -10
- 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 +212 -62
- 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 +24 -2
- package/dist/search-field/search-field.js +24 -9
- package/dist/search-field/search-field.js.map +1 -1
- package/dist/select/select.d.ts +3 -1
- package/dist/select/select.js +28 -14
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +22 -10
- package/dist/separator/separator.js +33 -11
- package/dist/separator/separator.js.map +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 +58 -5
- package/dist/stat-card/stat-card.js +144 -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 +55 -3
- 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.d.ts +36 -1
- package/dist/time-picker/time-picker.js +35 -7
- 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 +7 -1
- 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 +80 -0
- package/dist/usage-meter/usage-meter.js +458 -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":"field-message.js","names":[],"sources":["../../src/lib/field-message.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState, type CSSProperties } from \"react\";\n\nimport { cn } from \"./cn.js\";\nimport {\n prefersReducedMotion,\n useExitBackstop,\n useIsomorphicLayoutEffect,\n useLeavingItems,\n type Rect,\n} from \"./motion.js\";\nimport { SwapText } from \"./swap-text.js\";\n\n/*\n * The helper and error copy under a text field. Three layers of presence, each\n * a small piece of React because CSS cannot keep a node that React has already\n * unmounted: the row (opens and closes its height), the words (a changed word\n * rises in while the old one lifts away) and the numbers (one column per digit,\n * a gained or lost digit opens or closes its width, a changed digit rolls).\n *\n * Nothing animates on first mount at any layer: a row present at first render,\n * words in a row that has just opened, and digits in a column that has just\n * opened all appear as they are.\n */\n\nconst isNumber = (word: string) => /^\\d[\\d.,/:]*%?$/.test(word);\n\n/** How a replaced word leaves: Input's 180ms settle, Textarea's 160ms, or the sharper ease-exit curve Password field uses. */\nexport type WordExit = \"settle\" | \"quick\" | \"sharp\";\n\nconst WORD_OUT: Record<WordExit, string> = {\n settle: \"absolute inline-block whitespace-pre motion-safe:animate-field-word-out\",\n quick: \"absolute inline-block whitespace-pre motion-safe:animate-field-word-out-quick\",\n sharp: \"absolute inline-block whitespace-pre motion-safe:animate-field-word-out-sharp\",\n};\n\n/** The longest a spring exit takes (736ms) plus a margin, for the backstop that removes a node whose animation never ends. */\nconst SPRING_BACKSTOP = 900;\n\nconst rectOf = (node: HTMLElement): Rect => ({\n left: node.offsetLeft,\n top: node.offsetTop,\n width: node.offsetWidth,\n height: node.offsetHeight,\n});\n\ninterface Word {\n key: string;\n word: string;\n /** 0 for the words the text mounted with, which do not animate in. */\n seq: number;\n}\n\ninterface LeavingWord {\n id: number;\n word: string;\n rect: Rect;\n}\n\nfunction LeavingWordNode({\n item,\n onDone,\n exit,\n}: {\n item: LeavingWord;\n onDone: (id: number) => void;\n exit: WordExit;\n}) {\n const { id } = item;\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove);\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget) remove();\n }}\n style={item.rect}\n className={WORD_OUT[exit]}\n >\n {item.word}\n </span>\n );\n}\n\ninterface Column {\n /** Distance from the end of the number, so a new leading digit adds a column on the left. */\n key: number;\n char: string;\n seq: number;\n}\n\nfunction LeavingDigit({\n id,\n rect,\n onDone,\n children,\n}: {\n id: number;\n rect: Rect;\n onDone: (id: number) => void;\n children: string;\n}) {\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove, SPRING_BACKSTOP);\n // Reduced motion swaps instantly, and an animation that is switched off never ends.\n useIsomorphicLayoutEffect(() => {\n if (prefersReducedMotion()) remove();\n }, [remove]);\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget && event.animationName === \"field-digit-move-out\")\n remove();\n }}\n style={rect}\n className=\"absolute motion-safe:animate-field-digit-out\"\n >\n {children}\n </span>\n );\n}\n\nfunction DigitColumn({\n column,\n direction,\n leaving,\n onDone,\n}: {\n column: Column;\n direction: number;\n leaving: boolean;\n onDone: (key: number) => void;\n}) {\n const digitRef = useRef<HTMLSpanElement>(null);\n const digits = useLeavingItems(column.char, column.char, () => digitRef.current);\n const done = useCallback(() => onDone(column.key), [onDone, column.key]);\n useExitBackstop(leaving, done, SPRING_BACKSTOP);\n useIsomorphicLayoutEffect(() => {\n if (leaving && prefersReducedMotion()) done();\n }, [leaving, done]);\n return (\n <span\n style={{ \"--roll\": direction } as CSSProperties}\n onAnimationEnd={(event) => {\n if (\n leaving &&\n event.target === event.currentTarget &&\n event.animationName === \"field-column-shrink\"\n )\n done();\n }}\n className={cn(\n \"field-number-column\",\n leaving\n ? \"motion-safe:animate-field-column-out\"\n : column.seq > 0 && \"motion-safe:animate-field-column-in\",\n )}\n >\n {digits.leaving.map((item) => (\n <LeavingDigit key={item.id} id={item.id} rect={item.rect} onDone={digits.remove}>\n {item.value}\n </LeavingDigit>\n ))}\n <span\n key={column.char}\n ref={digitRef}\n className={digits.changed ? \"motion-safe:animate-field-digit-in\" : undefined}\n >\n {column.char}\n </span>\n </span>\n );\n}\n\n/**\n * A count in the copy, such as \"12 characters\": only the digits that changed\n * roll, and a gained or lost digit opens or closes its width. Digits arrive\n * from below when the number grew and from above when it shrank.\n */\nfunction RollingNumber({ word }: { word: string }) {\n const [state, setState] = useState(() => ({\n word,\n direction: 1,\n seq: 0,\n columns: toColumns(word, 0, new Map<number, Column>()),\n gone: [] as Column[],\n }));\n let { columns, gone, direction } = state;\n if (state.word !== word) {\n const seq = state.seq + 1;\n direction =\n parseFloat(word.replace(/,/g, \"\")) < parseFloat(state.word.replace(/,/g, \"\")) ? -1 : 1;\n columns = toColumns(word, seq, new Map(state.columns.map((column) => [column.key, column])));\n const kept = new Set(columns.map((column) => column.key));\n // A column that comes back while it is closing is shown again, not kept twice.\n gone = [\n ...state.gone.filter((column) => !kept.has(column.key)),\n ...state.columns.filter((column) => !kept.has(column.key)),\n ];\n setState({ word, direction, seq, columns, gone });\n }\n const finish = useCallback(\n (key: number) =>\n setState((current) => ({\n ...current,\n gone: current.gone.filter((column) => column.key !== key),\n })),\n [],\n );\n // Left to right is key descending, so a closing column keeps its place while it shrinks.\n const all = [\n ...columns.map((column) => ({ column, leaving: false })),\n ...gone.map((column) => ({ column, leaving: true })),\n ].sort((a, b) => b.column.key - a.column.key);\n return (\n <>\n {all.map(({ column, leaving }) => (\n <DigitColumn\n key={column.key}\n column={column}\n direction={direction}\n leaving={leaving}\n onDone={finish}\n />\n ))}\n </>\n );\n}\n\nfunction toColumns(word: string, seq: number, previous: Map<number, Column>): Column[] {\n const chars = word.split(\"\");\n return chars.map((char, index) => {\n const key = chars.length - index;\n const old = previous.get(key);\n return { key, char, seq: old ? old.seq : seq };\n });\n}\n\n/**\n * Copy that changes word by word. Words are keyed by position (numbers keep\n * their identity, so only their digits move), so inserting a word near the\n * front replaces every word after it. A changed word rises in from a soft blur\n * and the old one lifts away from where it stood. Assistive technology reads\n * the plain copy; the animated copy is hidden from it.\n */\nfunction MotionText({ text, exit, numbers }: { text: string; exit: WordExit; numbers: boolean }) {\n const [state, setState] = useState(() => ({\n text,\n seq: 0,\n words: toWords(text, 0, new Map<string, Word>(), numbers),\n }));\n let { words } = state;\n if (state.text !== text) {\n const seq = state.seq + 1;\n words = toWords(text, seq, new Map(state.words.map((word) => [word.key, word])), numbers);\n setState({ text, seq, words });\n }\n\n const rowRef = useRef<HTMLSpanElement>(null);\n const before = useRef(new Map<string, { rect: Rect; word: string }>());\n const nextId = useRef(0);\n const [leaving, setLeaving] = useState<LeavingWord[]>([]);\n\n // After every commit: remember where each word stands, and keep the ones that are gone, where they stood.\n useIsomorphicLayoutEffect(() => {\n const row = rowRef.current;\n if (!row) return;\n const now = new Map<string, { rect: Rect; word: string }>();\n for (const node of row.querySelectorAll<HTMLElement>(\"[data-word]\")) {\n now.set(node.dataset.word!, { rect: rectOf(node), word: node.textContent ?? \"\" });\n }\n const gone: LeavingWord[] = [];\n // Reduced motion removes a replaced word at once, and a box with no size has nothing to lift away.\n if (!prefersReducedMotion()) {\n for (const [key, was] of before.current) {\n if (!now.has(key) && was.rect.width && was.rect.height)\n gone.push({ id: nextId.current++, word: was.word, rect: was.rect });\n }\n }\n before.current = now;\n if (gone.length) setLeaving((list) => [...list, ...gone]);\n });\n\n const finish = useCallback(\n (id: number) => setLeaving((list) => list.filter((item) => item.id !== id)),\n [],\n );\n\n return (\n <>\n <span className=\"sr-only\">{text}</span>\n <span ref={rowRef} aria-hidden=\"true\" className=\"relative block\">\n {leaving.map((item) => (\n <LeavingWordNode key={item.id} item={item} onDone={finish} exit={exit} />\n ))}\n {words.map((entry, index) => (\n <span\n key={entry.key}\n data-word={entry.key}\n className={cn(\n \"inline-block whitespace-pre\",\n entry.seq > 0 && \"motion-safe:animate-field-word-in motion-reduce:animate-fade-in\",\n )}\n >\n {numbers && isNumber(entry.word) ? <RollingNumber word={entry.word} /> : entry.word}\n {index < words.length - 1 ? \" \" : null}\n </span>\n ))}\n </span>\n </>\n );\n}\n\nfunction toWords(text: string, seq: number, previous: Map<string, Word>, numbers: boolean): Word[] {\n return text.split(\" \").map((word, index) => {\n const key = `${index}:${numbers && isNumber(word) ? \"#\" : word}`;\n const old = previous.get(key);\n return { key, word, seq: old ? old.seq : seq };\n });\n}\n\ninterface Row {\n text: string;\n /** entering: mounted closed, about to open. closing: kept on screen while it shuts. */\n phase: \"entering\" | \"open\" | \"closing\";\n /** False for a row the field mounted with, whose copy does not settle in. */\n animate: boolean;\n}\n\n/** Writes the copy's height where the slot's height transition reads it. */\nfunction syncHeight(slot: HTMLElement, copy: HTMLElement) {\n const height = copy.getBoundingClientRect().height;\n if (height) slot.style.setProperty(\"--field-message-h\", height + \"px\");\n}\n\n/**\n * A field's helper or error row. `text` empty means no row. A row that appears\n * after mount opens its height on a spring and its copy settles in; one that\n * goes away closes the same way, still showing its last text while it does, and\n * text that comes back mid-close reopens the same row. A row present at first\n * render never animates. Error copy is an alert.\n *\n * The slot's height is written from script (a transition cannot start from or\n * end at auto): measured in a layout effect before the row opens, and again\n * whenever the copy resizes, so a message that wraps opens its next line.\n */\nexport function FieldMessage({\n id,\n text,\n tone,\n wordExit = \"settle\",\n numbers = true,\n copy = \"words\",\n}: {\n id?: string | undefined;\n text?: string | undefined;\n tone: \"description\" | \"error\";\n /** How a replaced word leaves (Input settles in 180ms, Textarea in 160ms, Password field uses the sharper exit curve). */\n wordExit?: WordExit;\n /** Roll digits in counts such as \"12 characters\". Off for copy that is words only. */\n numbers?: boolean;\n /** `\"words\"` animates a changed message word by word; `\"swap\"` replaces it as a whole, rising in as the old one lifts away. */\n copy?: \"words\" | \"swap\";\n}) {\n // Decided during render, so the id exists in the same commit as the aria-describedby that names it.\n const [row, setRow] = useState<Row | null>(() =>\n text ? { text, phase: \"open\", animate: false } : null,\n );\n if (text && (!row || row.text !== text || row.phase === \"closing\")) {\n setRow(\n row\n ? { text, phase: row.phase === \"closing\" ? \"open\" : row.phase, animate: row.animate }\n : { text, phase: \"entering\", animate: true },\n );\n } else if (!text && row && row.phase !== \"closing\") {\n setRow({ ...row, phase: \"closing\" });\n }\n\n const slotRef = useRef<HTMLSpanElement>(null);\n const copyRef = useRef<HTMLSpanElement>(null);\n const phase = row?.phase;\n\n // Measure before the row opens, and force the closed style to be computed first so there is something to transition from.\n useIsomorphicLayoutEffect(() => {\n const slot = slotRef.current;\n const copy = copyRef.current;\n if (!slot || !copy || phase === \"closing\") return;\n syncHeight(slot, copy);\n if (phase === \"entering\") {\n void slot.offsetHeight;\n setRow((current) =>\n current?.phase === \"entering\" ? { ...current, phase: \"open\" } : current,\n );\n }\n });\n\n // Remove the row once its height has closed. A row that never opened, or reduced motion, has nothing to wait for.\n useIsomorphicLayoutEffect(() => {\n if (phase !== \"closing\") return;\n const slot = slotRef.current;\n const gone = () => setRow((current) => (current?.phase === \"closing\" ? null : current));\n if (!slot || prefersReducedMotion()) {\n gone();\n return;\n }\n const style = getComputedStyle(slot);\n if (parseFloat(style.height) === 0 || style.opacity === \"0\") {\n gone();\n return;\n }\n const onEnd = (event: TransitionEvent) => {\n if (event.target === slot && event.propertyName === \"height\") gone();\n };\n slot.addEventListener(\"transitionend\", onEnd);\n const backstop = setTimeout(gone, SPRING_BACKSTOP);\n return () => {\n slot.removeEventListener(\"transitionend\", onEnd);\n clearTimeout(backstop);\n };\n }, [phase]);\n\n // Later size changes only (the first measurement is the layout effect above). A frame later, so the write cannot report a ResizeObserver loop.\n const mounted = row !== null;\n useEffect(() => {\n const slot = slotRef.current;\n const copy = copyRef.current;\n if (!slot || !copy || typeof ResizeObserver === \"undefined\") return;\n let frame = 0;\n const observer = new ResizeObserver(([entry]) => {\n const height = entry?.borderBoxSize[0]?.blockSize ?? copy.offsetHeight;\n if (!height) return;\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(() =>\n slot.style.setProperty(\"--field-message-h\", height + \"px\"),\n );\n });\n observer.observe(copy);\n return () => {\n cancelAnimationFrame(frame);\n observer.disconnect();\n };\n }, [mounted]);\n\n if (!row) return null;\n const closing = row.phase === \"closing\";\n return (\n <span\n ref={slotRef}\n data-slot=\"field-message-slot\"\n data-state={closing || row.phase === \"entering\" ? \"closed\" : \"open\"}\n className=\"field-message-slot\"\n >\n <span\n ref={copyRef}\n // A closing row no longer describes the field: it gives up its id and its alert role.\n id={closing ? undefined : id}\n role={tone === \"error\" && !closing ? \"alert\" : undefined}\n className={cn(\n \"block pt-2 text-sm leading-(--leading-normal) \" +\n (tone === \"error\" ? \"text-destructive-strong\" : \"text-muted-foreground\") +\n (row.animate ? \" motion-safe:animate-field-copy-in\" : \"\"),\n )}\n >\n {copy === \"swap\" ? (\n <>\n <span className=\"sr-only\">{row.text}</span>\n <span aria-hidden=\"true\" className=\"block\">\n <SwapText swapKey={row.text} className=\"block\">\n {row.text}\n </SwapText>\n </span>\n </>\n ) : (\n <MotionText text={row.text} exit={wordExit} numbers={numbers} />\n )}\n </span>\n </span>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"field-message.js","names":[],"sources":["../../src/lib/field-message.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState, type CSSProperties, type RefObject } from \"react\";\n\nimport { cn } from \"./cn.js\";\nimport {\n prefersReducedMotion,\n useExitBackstop,\n useIsomorphicLayoutEffect,\n useLeavingItems,\n type Rect,\n} from \"./motion.js\";\nimport { SwapText } from \"./swap-text.js\";\n\n/*\n * The helper and error copy under a text field. Three layers of presence, each\n * a small piece of React because CSS cannot keep a node that React has already\n * unmounted: the row (opens and closes its height), the words (a changed word\n * rises in while the old one lifts away) and the numbers (one column per digit,\n * a gained or lost digit opens or closes its width, a changed digit rolls).\n *\n * Nothing animates on first mount at any layer: a row present at first render,\n * words in a row that has just opened, and digits in a column that has just\n * opened all appear as they are.\n */\n\nconst isNumber = (word: string) => /^\\d[\\d.,/:]*%?$/.test(word);\n\n/** How a replaced word leaves: Input's 180ms settle, Textarea's 160ms, or the sharper ease-exit curve Password field uses. */\nexport type WordExit = \"settle\" | \"quick\" | \"sharp\";\n\nconst WORD_OUT: Record<WordExit, string> = {\n settle: \"absolute inline-block whitespace-pre motion-safe:animate-field-word-out\",\n quick: \"absolute inline-block whitespace-pre motion-safe:animate-field-word-out-quick\",\n sharp: \"absolute inline-block whitespace-pre motion-safe:animate-field-word-out-sharp\",\n};\n\n/** The longest a spring exit takes (736ms) plus a margin, for the backstop that removes a node whose animation never ends. */\nconst SPRING_BACKSTOP = 900;\n\nconst rectOf = (node: HTMLElement): Rect => ({\n left: node.offsetLeft,\n top: node.offsetTop,\n width: node.offsetWidth,\n height: node.offsetHeight,\n});\n\ninterface Word {\n key: string;\n word: string;\n /** 0 for the words the text mounted with, which do not animate in. */\n seq: number;\n}\n\ninterface LeavingWord {\n id: number;\n word: string;\n rect: Rect;\n}\n\nfunction LeavingWordNode({\n item,\n onDone,\n exit,\n}: {\n item: LeavingWord;\n onDone: (id: number) => void;\n exit: WordExit;\n}) {\n const { id } = item;\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove);\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget) remove();\n }}\n style={item.rect}\n className={WORD_OUT[exit]}\n >\n {item.word}\n </span>\n );\n}\n\ninterface Column {\n /** Distance from the end of the number, so a new leading digit adds a column on the left. */\n key: number;\n char: string;\n seq: number;\n}\n\nfunction LeavingDigit({\n id,\n rect,\n onDone,\n children,\n}: {\n id: number;\n rect: Rect;\n onDone: (id: number) => void;\n children: string;\n}) {\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove, SPRING_BACKSTOP);\n // Reduced motion swaps instantly, and an animation that is switched off never ends.\n useIsomorphicLayoutEffect(() => {\n if (prefersReducedMotion()) remove();\n }, [remove]);\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget && event.animationName === \"field-digit-move-out\")\n remove();\n }}\n style={rect}\n className=\"absolute motion-safe:animate-field-digit-out\"\n >\n {children}\n </span>\n );\n}\n\n/** One digit, rolling in or not as it was when it mounted: a later change of `animate` must not replay it. */\nfunction Digit({\n nodeRef,\n animate,\n children,\n}: {\n nodeRef: RefObject<HTMLSpanElement | null>;\n animate: boolean;\n children: string;\n}) {\n const [rolls] = useState(animate);\n return (\n <span ref={nodeRef} className={rolls ? \"motion-safe:animate-field-digit-in\" : undefined}>\n {children}\n </span>\n );\n}\n\nfunction DigitColumn({\n column,\n direction,\n leaving,\n still,\n onDone,\n}: {\n column: Column;\n direction: number;\n leaving: boolean;\n /** Show the digits as they are, without rolling or opening columns. */\n still: boolean;\n onDone: (key: number) => void;\n}) {\n const digitRef = useRef<HTMLSpanElement>(null);\n const digits = useLeavingItems(column.char, column.char, () => digitRef.current);\n // A digit that arrives after the column is already there rolls in; the one it mounted with does not.\n const [shownDigit, setShownDigit] = useState({ char: column.char, changes: 0 });\n if (shownDigit.char !== column.char)\n setShownDigit({ char: column.char, changes: shownDigit.changes + 1 });\n const done = useCallback(() => onDone(column.key), [onDone, column.key]);\n useExitBackstop(leaving, done, SPRING_BACKSTOP);\n useIsomorphicLayoutEffect(() => {\n if (leaving && prefersReducedMotion()) done();\n }, [leaving, done]);\n return (\n <span\n style={{ \"--roll\": direction } as CSSProperties}\n onAnimationEnd={(event) => {\n if (\n leaving &&\n event.target === event.currentTarget &&\n event.animationName === \"field-column-shrink\"\n )\n done();\n }}\n className={cn(\n \"field-number-column\",\n leaving\n ? \"motion-safe:animate-field-column-out\"\n : column.seq > 0 && \"motion-safe:animate-field-column-in\",\n )}\n >\n {(still ? [] : digits.leaving).map((item) => (\n <LeavingDigit key={item.id} id={item.id} rect={item.rect} onDone={digits.remove}>\n {item.value}\n </LeavingDigit>\n ))}\n <Digit key={column.char} nodeRef={digitRef} animate={shownDigit.changes > 0 && !still}>\n {column.char}\n </Digit>\n </span>\n );\n}\n\n/**\n * A count in the copy, such as \"12 characters\": only the digits that changed\n * roll, and a gained or lost digit opens or closes its width. Digits arrive\n * from below when the number grew and from above when it shrank.\n */\nexport function RollingNumber({\n word,\n direction: forced,\n still = false,\n}: {\n word: string;\n /** Which way the digits roll (1 up, -1 down). Read from the number's own change by default, which fails for text such as \"$12\". */\n direction?: 1 | -1;\n /** Follow the text at once, with no rolling: for a number that is being typed. */\n still?: boolean;\n}) {\n const [state, setState] = useState(() => ({\n word,\n direction: 1,\n seq: 0,\n columns: toColumns(word, 0, new Map<number, Column>()),\n gone: [] as Column[],\n }));\n let { columns, gone, direction } = state;\n if (state.word !== word) {\n const seq = state.seq + 1;\n direction =\n forced ??\n (parseFloat(word.replace(/,/g, \"\")) < parseFloat(state.word.replace(/,/g, \"\")) ? -1 : 1);\n // A still number opens its new columns at once: seq 0 is the seq of a column the text mounted with.\n columns = toColumns(\n word,\n still ? 0 : seq,\n new Map(state.columns.map((column) => [column.key, column])),\n );\n const kept = new Set(columns.map((column) => column.key));\n // A column that comes back while it is closing is shown again, not kept twice.\n gone = still\n ? []\n : [\n ...state.gone.filter((column) => !kept.has(column.key)),\n ...state.columns.filter((column) => !kept.has(column.key)),\n ];\n setState({ word, direction, seq, columns, gone });\n }\n const finish = useCallback(\n (key: number) =>\n setState((current) => ({\n ...current,\n gone: current.gone.filter((column) => column.key !== key),\n })),\n [],\n );\n // Left to right is key descending, so a closing column keeps its place while it shrinks.\n const all = [\n ...columns.map((column) => ({ column, leaving: false })),\n ...gone.map((column) => ({ column, leaving: true })),\n ].sort((a, b) => b.column.key - a.column.key);\n return (\n <>\n {all.map(({ column, leaving }) => (\n <DigitColumn\n key={column.key}\n column={column}\n direction={direction}\n leaving={leaving}\n still={still}\n onDone={finish}\n />\n ))}\n </>\n );\n}\n\nfunction toColumns(word: string, seq: number, previous: Map<number, Column>): Column[] {\n const chars = word.split(\"\");\n return chars.map((char, index) => {\n const key = chars.length - index;\n const old = previous.get(key);\n return { key, char, seq: old ? old.seq : seq };\n });\n}\n\n/**\n * Copy that changes word by word. Words are keyed by position (numbers keep\n * their identity, so only their digits move), so inserting a word near the\n * front replaces every word after it. A changed word rises in from a soft blur\n * and the old one lifts away from where it stood. Assistive technology reads\n * the plain copy; the animated copy is hidden from it.\n */\nfunction MotionText({ text, exit, numbers }: { text: string; exit: WordExit; numbers: boolean }) {\n const [state, setState] = useState(() => ({\n text,\n seq: 0,\n words: toWords(text, 0, new Map<string, Word>(), numbers),\n }));\n let { words } = state;\n if (state.text !== text) {\n const seq = state.seq + 1;\n words = toWords(text, seq, new Map(state.words.map((word) => [word.key, word])), numbers);\n setState({ text, seq, words });\n }\n\n const rowRef = useRef<HTMLSpanElement>(null);\n const before = useRef(new Map<string, { rect: Rect; word: string }>());\n const nextId = useRef(0);\n const [leaving, setLeaving] = useState<LeavingWord[]>([]);\n\n // After every commit: remember where each word stands, and keep the ones that are gone, where they stood.\n useIsomorphicLayoutEffect(() => {\n const row = rowRef.current;\n if (!row) return;\n const now = new Map<string, { rect: Rect; word: string }>();\n for (const node of row.querySelectorAll<HTMLElement>(\"[data-word]\")) {\n now.set(node.dataset.word!, { rect: rectOf(node), word: node.textContent ?? \"\" });\n }\n const gone: LeavingWord[] = [];\n // Reduced motion removes a replaced word at once, and a box with no size has nothing to lift away.\n if (!prefersReducedMotion()) {\n for (const [key, was] of before.current) {\n if (!now.has(key) && was.rect.width && was.rect.height)\n gone.push({ id: nextId.current++, word: was.word, rect: was.rect });\n }\n }\n before.current = now;\n if (gone.length) setLeaving((list) => [...list, ...gone]);\n });\n\n const finish = useCallback(\n (id: number) => setLeaving((list) => list.filter((item) => item.id !== id)),\n [],\n );\n\n return (\n <>\n <span className=\"sr-only\">{text}</span>\n <span ref={rowRef} aria-hidden=\"true\" className=\"relative block\">\n {leaving.map((item) => (\n <LeavingWordNode key={item.id} item={item} onDone={finish} exit={exit} />\n ))}\n {words.map((entry, index) => (\n <span\n key={entry.key}\n data-word={entry.key}\n className={cn(\n \"inline-block whitespace-pre\",\n entry.seq > 0 && \"motion-safe:animate-field-word-in motion-reduce:animate-fade-in\",\n )}\n >\n {numbers && isNumber(entry.word) ? <RollingNumber word={entry.word} /> : entry.word}\n {index < words.length - 1 ? \" \" : null}\n </span>\n ))}\n </span>\n </>\n );\n}\n\nfunction toWords(text: string, seq: number, previous: Map<string, Word>, numbers: boolean): Word[] {\n return text.split(\" \").map((word, index) => {\n const key = `${index}:${numbers && isNumber(word) ? \"#\" : word}`;\n const old = previous.get(key);\n return { key, word, seq: old ? old.seq : seq };\n });\n}\n\ninterface Row {\n text: string;\n /** entering: mounted closed, about to open. closing: kept on screen while it shuts. */\n phase: \"entering\" | \"open\" | \"closing\";\n /** False for a row the field mounted with, whose copy does not settle in. */\n animate: boolean;\n}\n\n/** Writes the copy's height where the slot's height transition reads it. */\nfunction syncHeight(slot: HTMLElement, copy: HTMLElement) {\n const height = copy.getBoundingClientRect().height;\n if (height) slot.style.setProperty(\"--field-message-h\", height + \"px\");\n}\n\n/**\n * A field's helper or error row. `text` empty means no row. A row that appears\n * after mount opens its height on a spring and its copy settles in; one that\n * goes away closes the same way, still showing its last text while it does, and\n * text that comes back mid-close reopens the same row. A row present at first\n * render never animates. Error copy is an alert.\n *\n * The slot's height is written from script (a transition cannot start from or\n * end at auto): measured in a layout effect before the row opens, and again\n * whenever the copy resizes, so a message that wraps opens its next line.\n */\nexport function FieldMessage({\n id,\n text,\n tone,\n wordExit = \"settle\",\n numbers = true,\n copy = \"words\",\n}: {\n id?: string | undefined;\n text?: string | undefined;\n tone: \"description\" | \"error\";\n /** How a replaced word leaves (Input settles in 180ms, Textarea in 160ms, Password field uses the sharper exit curve). */\n wordExit?: WordExit;\n /** Roll digits in counts such as \"12 characters\". Off for copy that is words only. */\n numbers?: boolean;\n /** `\"words\"` animates a changed message word by word; `\"swap\"` replaces it as a whole, rising in as the old one lifts away. */\n copy?: \"words\" | \"swap\";\n}) {\n // Decided during render, so the id exists in the same commit as the aria-describedby that names it.\n const [row, setRow] = useState<Row | null>(() =>\n text ? { text, phase: \"open\", animate: false } : null,\n );\n if (text && (!row || row.text !== text || row.phase === \"closing\")) {\n setRow(\n row\n ? { text, phase: row.phase === \"closing\" ? \"open\" : row.phase, animate: row.animate }\n : { text, phase: \"entering\", animate: true },\n );\n } else if (!text && row && row.phase !== \"closing\") {\n setRow({ ...row, phase: \"closing\" });\n }\n\n const slotRef = useRef<HTMLSpanElement>(null);\n const copyRef = useRef<HTMLSpanElement>(null);\n const phase = row?.phase;\n\n // Measure before the row opens, and force the closed style to be computed first so there is something to transition from.\n useIsomorphicLayoutEffect(() => {\n const slot = slotRef.current;\n const copy = copyRef.current;\n if (!slot || !copy || phase === \"closing\") return;\n syncHeight(slot, copy);\n if (phase === \"entering\") {\n void slot.offsetHeight;\n setRow((current) =>\n current?.phase === \"entering\" ? { ...current, phase: \"open\" } : current,\n );\n }\n });\n\n // Remove the row once its height has closed. A row that never opened, or reduced motion, has nothing to wait for.\n useIsomorphicLayoutEffect(() => {\n if (phase !== \"closing\") return;\n const slot = slotRef.current;\n const gone = () => setRow((current) => (current?.phase === \"closing\" ? null : current));\n if (!slot || prefersReducedMotion()) {\n gone();\n return;\n }\n const style = getComputedStyle(slot);\n if (parseFloat(style.height) === 0 || style.opacity === \"0\") {\n gone();\n return;\n }\n const onEnd = (event: TransitionEvent) => {\n if (event.target === slot && event.propertyName === \"height\") gone();\n };\n slot.addEventListener(\"transitionend\", onEnd);\n const backstop = setTimeout(gone, SPRING_BACKSTOP);\n return () => {\n slot.removeEventListener(\"transitionend\", onEnd);\n clearTimeout(backstop);\n };\n }, [phase]);\n\n // Later size changes only (the first measurement is the layout effect above). A frame later, so the write cannot report a ResizeObserver loop.\n const mounted = row !== null;\n useEffect(() => {\n const slot = slotRef.current;\n const copy = copyRef.current;\n if (!slot || !copy || typeof ResizeObserver === \"undefined\") return;\n let frame = 0;\n const observer = new ResizeObserver(([entry]) => {\n const height = entry?.borderBoxSize[0]?.blockSize ?? copy.offsetHeight;\n if (!height) return;\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(() =>\n slot.style.setProperty(\"--field-message-h\", height + \"px\"),\n );\n });\n observer.observe(copy);\n return () => {\n cancelAnimationFrame(frame);\n observer.disconnect();\n };\n }, [mounted]);\n\n if (!row) return null;\n const closing = row.phase === \"closing\";\n return (\n <span\n ref={slotRef}\n data-slot=\"field-message-slot\"\n data-state={closing || row.phase === \"entering\" ? \"closed\" : \"open\"}\n className=\"field-message-slot\"\n >\n <span\n ref={copyRef}\n // A closing row no longer describes the field: it gives up its id and its alert role.\n id={closing ? undefined : id}\n role={tone === \"error\" && !closing ? \"alert\" : undefined}\n className={cn(\n \"block pt-2 text-sm leading-(--leading-normal) \" +\n (tone === \"error\" ? \"text-destructive-strong\" : \"text-muted-foreground\") +\n (row.animate ? \" motion-safe:animate-field-copy-in\" : \"\"),\n )}\n >\n {copy === \"swap\" ? (\n <>\n <span className=\"sr-only\">{row.text}</span>\n <span aria-hidden=\"true\" className=\"block\">\n <SwapText swapKey={row.text} className=\"block\">\n {row.text}\n </SwapText>\n </span>\n </>\n ) : (\n <MotionText text={row.text} exit={wordExit} numbers={numbers} />\n )}\n </span>\n </span>\n );\n}\n"],"mappings":"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"}
|
package/dist/lib/field-shell.js
CHANGED
|
@@ -32,6 +32,8 @@ function useFieldWiring({ enabled, id, describedBy, invalid, description, error
|
|
|
32
32
|
const errorId = active && error ? `${controlId}-error` : void 0;
|
|
33
33
|
return {
|
|
34
34
|
active,
|
|
35
|
+
/** True inside a React Aria field, which supplies the label and its own messages. */
|
|
36
|
+
inside,
|
|
35
37
|
controlId,
|
|
36
38
|
hintId,
|
|
37
39
|
errorId,
|
|
@@ -47,15 +49,20 @@ function useFieldWiring({ enabled, id, describedBy, invalid, description, error
|
|
|
47
49
|
};
|
|
48
50
|
}
|
|
49
51
|
/** The wrapper: label above the control, then the helper row and the error row. Each row owns its top space, so a closed row costs nothing. */
|
|
50
|
-
function FieldShell({ label, controlId, description, hintId, error, errorId, wordExit, numbers, children }) {
|
|
52
|
+
function FieldShell({ label, controlId, description, hintId, error, errorId, wordExit, numbers, required = false, children }) {
|
|
51
53
|
return /* @__PURE__ */ jsxs("div", {
|
|
52
54
|
"data-slot": "field",
|
|
53
55
|
className: "grid min-w-0 font-sans",
|
|
54
56
|
children: [
|
|
55
|
-
label == null ? null : /* @__PURE__ */
|
|
57
|
+
label == null ? null : /* @__PURE__ */ jsxs(Label$1, {
|
|
56
58
|
htmlFor: controlId,
|
|
57
59
|
className: "mb-1.5",
|
|
58
|
-
children: label
|
|
60
|
+
children: [label, required ? /* @__PURE__ */ jsx("span", {
|
|
61
|
+
"aria-hidden": "true",
|
|
62
|
+
"data-slot": "field-required",
|
|
63
|
+
className: "-ms-1 text-destructive-strong",
|
|
64
|
+
children: "*"
|
|
65
|
+
}) : null]
|
|
59
66
|
}),
|
|
60
67
|
children,
|
|
61
68
|
/* @__PURE__ */ jsx(FieldMessage, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field-shell.js","names":["Label"],"sources":["../../src/lib/field-shell.tsx"],"sourcesContent":["import { useId, type ReactNode } from \"react\";\nimport { InputContext, LabelContext, useSlottedContext } from \"react-aria-components\";\n\nimport { Label } from \"../label/label.js\";\nimport { FieldMessage, type WordExit } from \"./field-message.js\";\n\n/*\n * The label, helper text and error text around a text control, for Input and\n * Textarea. The first lib file that imports a sibling component (Label): the\n * layer rule allows primitives to use primitives, and a field's label is the\n * same Label everywhere else.\n */\n\n/**\n * True when any field prop was passed at all, even as `undefined`. Deciding by\n * presence, not value, is what keeps `error={maybeMessage}` from moving the\n * control between a wrapper and no wrapper when the message appears: that would\n * remount the control and drop focus and an uncontrolled value.\n */\nexport const hasFieldProps = (props: object) =>\n \"label\" in props || \"description\" in props || \"error\" in props;\n\n/** A control inside a React Aria `TextField` already has its label and messages supplied by the field. */\nfunction useInsideReactAriaField() {\n const label = useSlottedContext(LabelContext);\n const input = useSlottedContext(InputContext);\n return Object.keys(label ?? {}).length > 0 || Object.keys(input ?? {}).length > 0;\n}\n\n/**\n * The ids and aria wiring for a control with field props. `active` is false\n * when no field prop was passed, or inside a React Aria field, and then\n * `controlProps` is empty: the control's own props are untouched, and a bare\n * control has no generated `id`.\n */\nexport function useFieldWiring({\n enabled,\n id,\n describedBy,\n invalid,\n description,\n error,\n}: {\n enabled: boolean;\n id: string | undefined;\n describedBy: string | undefined;\n invalid: boolean | \"true\" | \"false\" | \"grammar\" | \"spelling\" | undefined;\n description: string | undefined;\n error: string | undefined;\n}) {\n const generated = useId();\n const inside = useInsideReactAriaField();\n const active = enabled && !inside;\n const controlId = id ?? generated;\n const hintId = active && description ? `${controlId}-description` : undefined;\n const errorId = active && error ? `${controlId}-error` : undefined;\n return {\n active,\n controlId,\n hintId,\n errorId,\n controlProps: active\n ? {\n id: controlId,\n \"aria-invalid\": error ? true : invalid,\n \"aria-describedby\": [describedBy, hintId, errorId].filter(Boolean).join(\" \") || undefined,\n }\n : {},\n };\n}\n\n/** The wrapper: label above the control, then the helper row and the error row. Each row owns its top space, so a closed row costs nothing. */\nexport function FieldShell({\n label,\n controlId,\n description,\n hintId,\n error,\n errorId,\n wordExit,\n numbers,\n children,\n}: {\n label: ReactNode;\n controlId: string;\n description: string | undefined;\n hintId: string | undefined;\n error: string | undefined;\n errorId: string | undefined;\n wordExit?: WordExit;\n numbers?: boolean;\n children: ReactNode;\n}) {\n return (\n <div data-slot=\"field\" className=\"grid min-w-0 font-sans\">\n {label == null ? null : (\n // 6px, the gap Select and DatePicker keep between their label and control, so a labelled Input sits level with them.\n <Label htmlFor={controlId} className=\"mb-1.5\">\n {label}\n </Label>\n )}\n {children}\n <FieldMessage\n id={hintId}\n text={description}\n tone=\"description\"\n wordExit={wordExit}\n numbers={numbers}\n />\n <FieldMessage id={errorId} text={error} tone=\"error\" wordExit={wordExit} numbers={numbers} />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAmBA,MAAa,iBAAiB,UAC5B,WAAW,SAAS,iBAAiB,SAAS,WAAW;;AAG3D,SAAS,0BAA0B;CACjC,MAAM,QAAQ,kBAAkB,YAAY;CAC5C,MAAM,QAAQ,kBAAkB,YAAY;CAC5C,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS;AAClF;;;;;;;AAQA,SAAgB,eAAe,EAC7B,SACA,IACA,aACA,SACA,aACA,SAQC;CACD,MAAM,YAAY,MAAM;CACxB,MAAM,SAAS,wBAAwB;CACvC,MAAM,SAAS,WAAW,CAAC;CAC3B,MAAM,YAAY,MAAM;CACxB,MAAM,SAAS,UAAU,cAAc,GAAG,UAAU,gBAAgB,KAAA;CACpE,MAAM,UAAU,UAAU,QAAQ,GAAG,UAAU,UAAU,KAAA;CACzD,OAAO;EACL;EACA;EACA;EACA;EACA,cAAc,SACV;GACE,IAAI;GACJ,gBAAgB,QAAQ,OAAO;GAC/B,oBAAoB;IAAC;IAAa;IAAQ;GAAO,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,KAAK,KAAA;EAClF,IACA,CAAC;CACP;AACF;;AAGA,SAAgB,WAAW,EACzB,OACA,WACA,aACA,QACA,OACA,SACA,UACA,SACA,
|
|
1
|
+
{"version":3,"file":"field-shell.js","names":["Label"],"sources":["../../src/lib/field-shell.tsx"],"sourcesContent":["import { useId, type ReactNode } from \"react\";\nimport { InputContext, LabelContext, useSlottedContext } from \"react-aria-components\";\n\nimport { Label } from \"../label/label.js\";\nimport { FieldMessage, type WordExit } from \"./field-message.js\";\n\n/*\n * The label, helper text and error text around a text control, for Input and\n * Textarea. The first lib file that imports a sibling component (Label): the\n * layer rule allows primitives to use primitives, and a field's label is the\n * same Label everywhere else.\n */\n\n/**\n * True when any field prop was passed at all, even as `undefined`. Deciding by\n * presence, not value, is what keeps `error={maybeMessage}` from moving the\n * control between a wrapper and no wrapper when the message appears: that would\n * remount the control and drop focus and an uncontrolled value.\n */\nexport const hasFieldProps = (props: object) =>\n \"label\" in props || \"description\" in props || \"error\" in props;\n\n/** A control inside a React Aria `TextField` already has its label and messages supplied by the field. */\nfunction useInsideReactAriaField() {\n const label = useSlottedContext(LabelContext);\n const input = useSlottedContext(InputContext);\n return Object.keys(label ?? {}).length > 0 || Object.keys(input ?? {}).length > 0;\n}\n\n/**\n * The ids and aria wiring for a control with field props. `active` is false\n * when no field prop was passed, or inside a React Aria field, and then\n * `controlProps` is empty: the control's own props are untouched, and a bare\n * control has no generated `id`.\n */\nexport function useFieldWiring({\n enabled,\n id,\n describedBy,\n invalid,\n description,\n error,\n}: {\n enabled: boolean;\n id: string | undefined;\n describedBy: string | undefined;\n invalid: boolean | \"true\" | \"false\" | \"grammar\" | \"spelling\" | undefined;\n description: string | undefined;\n error: string | undefined;\n}) {\n const generated = useId();\n const inside = useInsideReactAriaField();\n const active = enabled && !inside;\n const controlId = id ?? generated;\n const hintId = active && description ? `${controlId}-description` : undefined;\n const errorId = active && error ? `${controlId}-error` : undefined;\n return {\n active,\n /** True inside a React Aria field, which supplies the label and its own messages. */\n inside,\n controlId,\n hintId,\n errorId,\n controlProps: active\n ? {\n id: controlId,\n \"aria-invalid\": error ? true : invalid,\n \"aria-describedby\": [describedBy, hintId, errorId].filter(Boolean).join(\" \") || undefined,\n }\n : {},\n };\n}\n\n/** The wrapper: label above the control, then the helper row and the error row. Each row owns its top space, so a closed row costs nothing. */\nexport function FieldShell({\n label,\n controlId,\n description,\n hintId,\n error,\n errorId,\n wordExit,\n numbers,\n required = false,\n children,\n}: {\n label: ReactNode;\n controlId: string;\n description: string | undefined;\n hintId: string | undefined;\n error: string | undefined;\n errorId: string | undefined;\n wordExit?: WordExit;\n numbers?: boolean;\n /**\n * Draws the required marker after the label. Decoration only (aria-hidden):\n * the control's own `required` is what a screen reader announces, so the\n * name stays \"Password\", not \"Password star\".\n */\n required?: boolean;\n children: ReactNode;\n}) {\n return (\n <div data-slot=\"field\" className=\"grid min-w-0 font-sans\">\n {label == null ? null : (\n // 6px, the gap Select and DatePicker keep between their label and control, so a labelled Input sits level with them.\n <Label htmlFor={controlId} className=\"mb-1.5\">\n {label}\n {required ? (\n <span\n aria-hidden=\"true\"\n data-slot=\"field-required\"\n className=\"-ms-1 text-destructive-strong\"\n >\n *\n </span>\n ) : null}\n </Label>\n )}\n {children}\n <FieldMessage\n id={hintId}\n text={description}\n tone=\"description\"\n wordExit={wordExit}\n numbers={numbers}\n />\n <FieldMessage id={errorId} text={error} tone=\"error\" wordExit={wordExit} numbers={numbers} />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAmBA,MAAa,iBAAiB,UAC5B,WAAW,SAAS,iBAAiB,SAAS,WAAW;;AAG3D,SAAS,0BAA0B;CACjC,MAAM,QAAQ,kBAAkB,YAAY;CAC5C,MAAM,QAAQ,kBAAkB,YAAY;CAC5C,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS;AAClF;;;;;;;AAQA,SAAgB,eAAe,EAC7B,SACA,IACA,aACA,SACA,aACA,SAQC;CACD,MAAM,YAAY,MAAM;CACxB,MAAM,SAAS,wBAAwB;CACvC,MAAM,SAAS,WAAW,CAAC;CAC3B,MAAM,YAAY,MAAM;CACxB,MAAM,SAAS,UAAU,cAAc,GAAG,UAAU,gBAAgB,KAAA;CACpE,MAAM,UAAU,UAAU,QAAQ,GAAG,UAAU,UAAU,KAAA;CACzD,OAAO;EACL;;EAEA;EACA;EACA;EACA;EACA,cAAc,SACV;GACE,IAAI;GACJ,gBAAgB,QAAQ,OAAO;GAC/B,oBAAoB;IAAC;IAAa;IAAQ;GAAO,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,KAAK,KAAA;EAClF,IACA,CAAC;CACP;AACF;;AAGA,SAAgB,WAAW,EACzB,OACA,WACA,aACA,QACA,OACA,SACA,UACA,SACA,WAAW,OACX,YAiBC;CACD,OACE,qBAAC,OAAD;EAAK,aAAU;EAAQ,WAAU;EAAjC,UAAA;GACG,SAAS,OAAO,OAEf,qBAACA,SAAD;IAAO,SAAS;IAAW,WAAU;IAArC,UAAA,CACG,OACA,WACC,oBAAC,QAAD;KACE,eAAY;KACZ,aAAU;KACV,WAAU;KACX,UAAA;IAEK,CAAA,IACJ,IACC;;GAER;GACD,oBAAC,cAAD;IACE,IAAI;IACJ,MAAM;IACN,MAAK;IACK;IACD;GACV,CAAA;GACD,oBAAC,cAAD;IAAc,IAAI;IAAS,MAAM;IAAO,MAAK;IAAkB;IAAmB;GAAU,CAAA;EACzF;;AAET"}
|
|
@@ -20,23 +20,45 @@ import { cva } from "class-variance-authority";
|
|
|
20
20
|
const fieldSurfaceVariants = cva([
|
|
21
21
|
"w-full min-w-0",
|
|
22
22
|
"box-border font-sans",
|
|
23
|
-
"rounded-
|
|
24
|
-
"bg-
|
|
23
|
+
"rounded-control border-(length:--border-width) border-border",
|
|
24
|
+
"bg-surface text-foreground",
|
|
25
25
|
"shadow-surface",
|
|
26
26
|
"placeholder:text-muted-foreground",
|
|
27
27
|
"selection:bg-primary selection:text-primary-foreground",
|
|
28
28
|
"motion-safe:transition-[border-color,background-color] motion-safe:duration-(--duration-quick) motion-safe:ease-settle",
|
|
29
29
|
"outline-none",
|
|
30
|
-
"data-[focus-visible]:outline-solid",
|
|
31
|
-
"data-[focus-visible]:outline-(length:--focus-ring-width)",
|
|
32
|
-
"data-[focus-visible]:outline-offset-(--focus-ring-offset)",
|
|
33
|
-
"data-[focus-visible]:outline-(--color-ring)",
|
|
30
|
+
"forced-colors:data-[focus-visible]:outline-solid",
|
|
31
|
+
"forced-colors:data-[focus-visible]:outline-(length:--focus-ring-width)",
|
|
32
|
+
"forced-colors:data-[focus-visible]:outline-offset-(--focus-ring-offset)",
|
|
34
33
|
"data-[invalid]:border-destructive-strong",
|
|
35
|
-
"[&:where([data-hovered]:not([data-disabled]))]:border-
|
|
36
|
-
"
|
|
34
|
+
"[&:where([data-hovered]:not([data-disabled]))]:border-border-strong",
|
|
35
|
+
"data-[focus-visible]:not-data-[invalid]:not-group-data-[invalid]:border-foreground",
|
|
36
|
+
"[&:is(input,textarea):focus:not([data-invalid])]:border-foreground",
|
|
37
|
+
"[&:has(:is(input,textarea,[role=spinbutton]):focus):not([data-invalid]):not(:where(.group[data-invalid]_*))]:border-foreground",
|
|
37
38
|
"data-[disabled]:cursor-not-allowed data-[disabled]:bg-muted data-[disabled]:opacity-(--opacity-disabled)"
|
|
38
39
|
]);
|
|
40
|
+
/**
|
|
41
|
+
* The control scale of a text entry box: height and inline padding from the
|
|
42
|
+
* `--density-control-*` tokens, the same three rungs as Button, so a field
|
|
43
|
+
* sits level with the buttons in its row. Input, Select and SearchField all
|
|
44
|
+
* read it, so the rungs cannot drift apart.
|
|
45
|
+
*/
|
|
46
|
+
const fieldBoxSize = {
|
|
47
|
+
sm: "h-(--density-control-h-sm) px-(--density-control-pad-sm)",
|
|
48
|
+
md: "h-(--density-control-h-md) px-(--density-control-pad-md)",
|
|
49
|
+
lg: "h-(--density-control-h-lg) px-(--density-control-pad-lg)"
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The type rule that goes with `fieldBoxSize`: `sm` is 14px, except on a
|
|
53
|
+
* coarse pointer, where iOS would zoom a focused field under 16px; `md` and
|
|
54
|
+
* `lg` are 16px. See Input's `size` variant for the reasoning.
|
|
55
|
+
*/
|
|
56
|
+
const fieldTextSize = {
|
|
57
|
+
sm: "text-sm pointer-coarse:text-base",
|
|
58
|
+
md: "text-base",
|
|
59
|
+
lg: "text-base"
|
|
60
|
+
};
|
|
39
61
|
//#endregion
|
|
40
|
-
export { fieldSurfaceVariants };
|
|
62
|
+
export { fieldBoxSize, fieldSurfaceVariants, fieldTextSize };
|
|
41
63
|
|
|
42
64
|
//# sourceMappingURL=field-surface.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field-surface.js","names":[],"sources":["../../src/lib/field-surface.ts"],"sourcesContent":["import { cva } from \"class-variance-authority\";\n\n/**\n * The shared surface of a text entry control.\n *\n * Extracted at the Rule of Two, and for a reason the legacy demonstrates:\n * its `input.tsx` and `textarea.tsx` styled the same surface twice and had\n * already drifted. The input is `bg-transparent`, the textarea is\n * `bg-background`. The input focuses with `focus-visible:border-ring` plus a\n * 3px ring; the textarea uses a 2px ring with an offset. The input has\n * `aria-invalid` treatment, the textarea has none. None of those differences\n * is a decision anyone made -- they are two files edited at different times.\n *\n * Everything that must look identical between an Input and a Textarea lives\n * here. Only the box geometry differs, and that is the caller's variant.\n *\n * Every string is a STATIC literal. Tailwind scans source text, so a class\n * assembled at runtime is invisible to it and would never be emitted.\n */\nexport const fieldSurfaceVariants = cva([\n \"w-full min-w-0\",\n\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none.\n // A bare <input> keeps the user-agent border, background and Arial face,\n // and content-box would add the padding on top of the width.\n \"box-border font-sans\",\n\n \"rounded-
|
|
1
|
+
{"version":3,"file":"field-surface.js","names":[],"sources":["../../src/lib/field-surface.ts"],"sourcesContent":["import { cva } from \"class-variance-authority\";\n\n/**\n * The shared surface of a text entry control.\n *\n * Extracted at the Rule of Two, and for a reason the legacy demonstrates:\n * its `input.tsx` and `textarea.tsx` styled the same surface twice and had\n * already drifted. The input is `bg-transparent`, the textarea is\n * `bg-background`. The input focuses with `focus-visible:border-ring` plus a\n * 3px ring; the textarea uses a 2px ring with an offset. The input has\n * `aria-invalid` treatment, the textarea has none. None of those differences\n * is a decision anyone made -- they are two files edited at different times.\n *\n * Everything that must look identical between an Input and a Textarea lives\n * here. Only the box geometry differs, and that is the caller's variant.\n *\n * Every string is a STATIC literal. Tailwind scans source text, so a class\n * assembled at runtime is invisible to it and would never be emitted.\n */\nexport const fieldSurfaceVariants = cva([\n \"w-full min-w-0\",\n\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none.\n // A bare <input> keeps the user-agent border, background and Arial face,\n // and content-box would add the padding on top of the width.\n \"box-border font-sans\",\n\n \"rounded-control border-(length:--border-width) border-border\",\n \"bg-surface text-foreground\",\n \"shadow-surface\",\n\n // --color-muted-foreground is 5.25:1 on the page, so the placeholder is\n // legible rather than the usual near-invisible grey. It is still text and\n // the contrast contract holds it to the 4.5 floor.\n \"placeholder:text-muted-foreground\",\n \"selection:bg-primary selection:text-primary-foreground\",\n\n // Only the two properties that change, on the settle curve: the border darkens\n // on hover and focus, and the background turns muted when disabled. The focus\n // outline is not animated (it switches between none and solid).\n \"motion-safe:transition-[border-color,background-color] motion-safe:duration-(--duration-quick) motion-safe:ease-settle\",\n\n // outline-solid is load-bearing. `outline-none` sets BOTH\n // outline-style:none and --tw-outline-style:none, and the width utility\n // resolves its style from that same variable -- so a focus rule can apply\n // a width and a colour while faithfully re-applying none, and paint\n // nothing. Button shipped exactly that.\n //\n // A text field shows focus on its own border (below), not a separate ring:\n // UX decision (Oct 2026). The outline comes back only under forced colours,\n // where a border colour is overridden and would leave no focus indicator.\n \"outline-none\",\n \"forced-colors:data-[focus-visible]:outline-solid\",\n \"forced-colors:data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"forced-colors:data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n\n // React Aria sets data-invalid from the native `aria-invalid` attribute,\n // so this costs no API surface and works both bare and inside a\n // TextField. destructive-strong rather than destructive: the fill token\n // is 2.44:1-class and cannot describe a boundary (WCAG 1.4.11).\n //\n // The border CHANGES COLOUR only. Colour alone is not a state cue\n // (WCAG 1.4.1), which is why an invalid field must also carry a message --\n // that is the consumer's job, and the migration table says so.\n \"data-[invalid]:border-destructive-strong\",\n\n // Hover and focus darken the border to border-strong, in addition to the\n // outline ring above (which stays: forced-colours drops a border colour, and\n // WCAG 2.4.7 wants a visible indicator). The conditions sit inside :where(),\n // which adds no specificity, so the invalid border always wins by specificity\n // rather than by the order Tailwind happens to emit utilities in -- on an\n // Input through data-[invalid], on a Select or DatePicker trigger through\n // group-data-[invalid], because their ROOT carries the state. A disabled field\n // does not darken: React Aria's TextArea still reports hover while disabled.\n \"[&:where([data-hovered]:not([data-disabled]))]:border-border-strong\",\n // Focus turns the border itself dark, same width, no gap. An invalid field\n // keeps its red border while focused.\n \"data-[focus-visible]:not-data-[invalid]:not-group-data-[invalid]:border-foreground\",\n // A mouse click into a text box is not \"focus-visible\" to React Aria, but the\n // border must still show focus: key off real focus on the input itself, or on\n // the input, textarea or date segment inside a wrapping box.\n \"[&:is(input,textarea):focus:not([data-invalid])]:border-foreground\",\n \"[&:has(:is(input,textarea,[role=spinbutton]):focus):not([data-invalid]):not(:where(.group[data-invalid]_*))]:border-foreground\",\n\n \"data-[disabled]:cursor-not-allowed data-[disabled]:bg-muted data-[disabled]:opacity-(--opacity-disabled)\",\n]);\n\n/**\n * The control scale of a text entry box: height and inline padding from the\n * `--density-control-*` tokens, the same three rungs as Button, so a field\n * sits level with the buttons in its row. Input, Select and SearchField all\n * read it, so the rungs cannot drift apart.\n */\nexport const fieldBoxSize = {\n sm: \"h-(--density-control-h-sm) px-(--density-control-pad-sm)\",\n md: \"h-(--density-control-h-md) px-(--density-control-pad-md)\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg)\",\n} as const;\n\n/**\n * The type rule that goes with `fieldBoxSize`: `sm` is 14px, except on a\n * coarse pointer, where iOS would zoom a focused field under 16px; `md` and\n * `lg` are 16px. See Input's `size` variant for the reasoning.\n */\nexport const fieldTextSize = {\n sm: \"text-sm pointer-coarse:text-base\",\n md: \"text-base\",\n lg: \"text-base\",\n} as const;\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,MAAa,uBAAuB,IAAI;CACtC;CAKA;CAEA;CACA;CACA;CAKA;CACA;CAKA;CAWA;CACA;CACA;CACA;CAUA;CAUA;CAGA;CAIA;CACA;CAEA;AACF,CAAC;;;;;;;AAQD,MAAa,eAAe;CAC1B,IAAI;CACJ,IAAI;CACJ,IAAI;AACN;;;;;;AAOA,MAAa,gBAAgB;CAC3B,IAAI;CACJ,IAAI;CACJ,IAAI;AACN"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
//#region src/lib/flag-emoji.ts
|
|
3
|
+
/**
|
|
4
|
+
* Whether this platform draws flag emoji. Windows ships none: a flag there comes
|
|
5
|
+
* out as two boxed letters, so the picker shows the plain ISO code instead.
|
|
6
|
+
*
|
|
7
|
+
* The test draws a flag in black on an offscreen canvas. A real flag glyph is
|
|
8
|
+
* coloured whatever the fill colour is; the fallback letters take the fill and
|
|
9
|
+
* stay black. Any saturated pixel means the platform can draw it.
|
|
10
|
+
*/
|
|
11
|
+
function detect() {
|
|
12
|
+
try {
|
|
13
|
+
const canvas = document.createElement("canvas");
|
|
14
|
+
canvas.width = 48;
|
|
15
|
+
canvas.height = 32;
|
|
16
|
+
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
17
|
+
if (!context) return false;
|
|
18
|
+
context.textBaseline = "top";
|
|
19
|
+
context.font = "24px sans-serif";
|
|
20
|
+
context.fillStyle = "black";
|
|
21
|
+
context.fillText("🇺🇸", 0, 2);
|
|
22
|
+
const { data } = context.getImageData(0, 0, canvas.width, canvas.height);
|
|
23
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
24
|
+
const spread = Math.max(data[i], data[i + 1], data[i + 2]) - Math.min(data[i], data[i + 1], data[i + 2]);
|
|
25
|
+
if (data[i + 3] > 0 && spread > 40) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
let cached;
|
|
33
|
+
/** The check runs once per page and its answer is kept. Browser only: it needs a canvas. */
|
|
34
|
+
function supportsFlagEmoji() {
|
|
35
|
+
if (typeof document === "undefined") return false;
|
|
36
|
+
return cached ??= detect();
|
|
37
|
+
}
|
|
38
|
+
const subscribe = () => () => {};
|
|
39
|
+
const serverSnapshot = () => false;
|
|
40
|
+
/**
|
|
41
|
+
* `false` on the server and for the hydrating render (so the markup matches),
|
|
42
|
+
* then the real answer. Never reads `window` at import time.
|
|
43
|
+
*/
|
|
44
|
+
function useFlagEmoji() {
|
|
45
|
+
return useSyncExternalStore(subscribe, supportsFlagEmoji, serverSnapshot);
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { supportsFlagEmoji, useFlagEmoji };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=flag-emoji.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flag-emoji.js","names":[],"sources":["../../src/lib/flag-emoji.ts"],"sourcesContent":["import { useSyncExternalStore } from \"react\";\n\n/**\n * Whether this platform draws flag emoji. Windows ships none: a flag there comes\n * out as two boxed letters, so the picker shows the plain ISO code instead.\n *\n * The test draws a flag in black on an offscreen canvas. A real flag glyph is\n * coloured whatever the fill colour is; the fallback letters take the fill and\n * stay black. Any saturated pixel means the platform can draw it.\n */\nfunction detect(): boolean {\n try {\n const canvas = document.createElement(\"canvas\");\n canvas.width = 48;\n canvas.height = 32;\n const context = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!context) return false;\n context.textBaseline = \"top\";\n context.font = \"24px sans-serif\";\n context.fillStyle = \"black\";\n context.fillText(\"\\u{1F1FA}\\u{1F1F8}\", 0, 2);\n const { data } = context.getImageData(0, 0, canvas.width, canvas.height);\n for (let i = 0; i < data.length; i += 4) {\n const spread = Math.max(data[i]!, data[i + 1]!, data[i + 2]!) - Math.min(data[i]!, data[i + 1]!, data[i + 2]!);\n if (data[i + 3]! > 0 && spread > 40) return true;\n }\n return false;\n } catch {\n return false;\n }\n}\n\nlet cached: boolean | undefined;\n\n/** The check runs once per page and its answer is kept. Browser only: it needs a canvas. */\nexport function supportsFlagEmoji(): boolean {\n if (typeof document === \"undefined\") return false;\n return (cached ??= detect());\n}\n\nconst subscribe = () => () => {};\nconst serverSnapshot = () => false;\n\n/**\n * `false` on the server and for the hydrating render (so the markup matches),\n * then the real answer. Never reads `window` at import time.\n */\nexport function useFlagEmoji(): boolean {\n return useSyncExternalStore(subscribe, supportsFlagEmoji, serverSnapshot);\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAS,SAAkB;CACzB,IAAI;EACF,MAAM,SAAS,SAAS,cAAc,QAAQ;EAC9C,OAAO,QAAQ;EACf,OAAO,SAAS;EAChB,MAAM,UAAU,OAAO,WAAW,MAAM,EAAE,oBAAoB,KAAK,CAAC;EACpE,IAAI,CAAC,SAAS,OAAO;EACrB,QAAQ,eAAe;EACvB,QAAQ,OAAO;EACf,QAAQ,YAAY;EACpB,QAAQ,SAAS,QAAsB,GAAG,CAAC;EAC3C,MAAM,EAAE,SAAS,QAAQ,aAAa,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;EACvE,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG;GACvC,MAAM,SAAS,KAAK,IAAI,KAAK,IAAK,KAAK,IAAI,IAAK,KAAK,IAAI,EAAG,IAAI,KAAK,IAAI,KAAK,IAAK,KAAK,IAAI,IAAK,KAAK,IAAI,EAAG;GAC7G,IAAI,KAAK,IAAI,KAAM,KAAK,SAAS,IAAI,OAAO;EAC9C;EACA,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,IAAI;;AAGJ,SAAgB,oBAA6B;CAC3C,IAAI,OAAO,aAAa,aAAa,OAAO;CAC5C,OAAQ,WAAW,OAAO;AAC5B;AAEA,MAAM,wBAAwB,CAAC;AAC/B,MAAM,uBAAuB;;;;;AAM7B,SAAgB,eAAwB;CACtC,OAAO,qBAAqB,WAAW,mBAAmB,cAAc;AAC1E"}
|
package/dist/lib/flip.js
CHANGED
|
@@ -16,19 +16,27 @@ import { useRef } from "react";
|
|
|
16
16
|
*
|
|
17
17
|
* @param duration CSS time of the slide, e.g. `var(--duration-standard)`.
|
|
18
18
|
* @param easing CSS easing of the slide, e.g. `var(--ease-enter)`.
|
|
19
|
+
* @param relative Measure positions from the container's own corner instead of the
|
|
20
|
+
* viewport's, so scrolling the page between two looks is not mistaken for the elements
|
|
21
|
+
* moving. For lists whose container stays put (rows of a table); content that also moves
|
|
22
|
+
* because its container does (centred text) wants the default.
|
|
19
23
|
*/
|
|
20
|
-
function useFlip(container, key, duration = "var(--duration-standard)", easing = "var(--ease-enter)") {
|
|
24
|
+
function useFlip(container, key, duration = "var(--duration-standard)", easing = "var(--ease-enter)", relative = false) {
|
|
21
25
|
const remembered = useRef(/* @__PURE__ */ new Map());
|
|
22
26
|
const measure = () => {
|
|
23
27
|
const root = container.current;
|
|
24
28
|
const next = /* @__PURE__ */ new Map();
|
|
25
29
|
if (!root) return next;
|
|
30
|
+
const origin = relative ? root.getBoundingClientRect() : {
|
|
31
|
+
left: 0,
|
|
32
|
+
top: 0
|
|
33
|
+
};
|
|
26
34
|
for (const el of root.querySelectorAll("[data-flip]")) {
|
|
27
35
|
if (el.closest("[data-leaving]")) continue;
|
|
28
36
|
const box = el.getBoundingClientRect();
|
|
29
37
|
next.set(el, {
|
|
30
|
-
left: box.left,
|
|
31
|
-
top: box.top
|
|
38
|
+
left: box.left - origin.left,
|
|
39
|
+
top: box.top - origin.top
|
|
32
40
|
});
|
|
33
41
|
}
|
|
34
42
|
return next;
|
package/dist/lib/flip.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flip.js","names":[],"sources":["../../src/lib/flip.ts"],"sourcesContent":["import { useRef, type RefObject } from \"react\";\n\nimport { prefersReducedMotion, useIsomorphicLayoutEffect } from \"./motion.js\";\n\n/**\n * Siblings that shift because something next to them changed size (a wider month\n * name pushes the day and year along) slide to their new place instead of\n * jumping: measure where each `[data-flip]` element was, let the new layout\n * commit, put each one back where it was with a translate, then release it on a\n * transition. CSS cannot do this alone, because the layout move is not a style\n * change.\n *\n * `key` says when to look. Positions are remembered after every look and\n * refreshed when the container resizes, so a move that is not a change of `key`\n * (a window resize) does not become the next flip. Reduced motion does not flip.\n * Elements inside a leaving copy (`[data-leaving]`) are ignored.\n *\n * @param duration CSS time of the slide, e.g. `var(--duration-standard)`.\n * @param easing CSS easing of the slide, e.g. `var(--ease-enter)`.\n */\nexport function useFlip(\n container: RefObject<HTMLElement | null>,\n key: unknown,\n duration = \"var(--duration-standard)\",\n easing = \"var(--ease-enter)\",\n) {\n const remembered = useRef(new Map<Element, { left: number; top: number }>());\n\n const measure = () => {\n const root = container.current;\n const next = new Map<Element, { left: number; top: number }>();\n if (!root) return next;\n for (const el of root.querySelectorAll<HTMLElement>(\"[data-flip]\")) {\n if (el.closest(\"[data-leaving]\")) continue;\n const box = el.getBoundingClientRect();\n next.set(el, { left: box.left, top: box.top });\n }\n return next;\n };\n\n useIsomorphicLayoutEffect(() => {\n const root = container.current;\n if (!root) return;\n const before = remembered.current;\n const after = measure();\n if (before.size > 0 && !prefersReducedMotion()) {\n const moved: HTMLElement[] = [];\n for (const [el, now] of after) {\n const was = before.get(el);\n if (!was) continue;\n const dx = was.left - now.left;\n const dy = was.top - now.top;\n if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) continue;\n const node = el as HTMLElement;\n node.style.transition = \"none\";\n node.style.translate = `${dx}px ${dy}px`;\n moved.push(node);\n }\n if (moved.length > 0) {\n void root.offsetWidth;\n for (const node of moved) {\n node.style.transition = `translate ${duration} ${easing}`;\n node.style.translate = \"\";\n node.addEventListener(\n \"transitionend\",\n () => {\n node.style.transition = \"\";\n },\n { once: true },\n );\n }\n }\n }\n remembered.current = after;\n // Positions are read once per change of `key`; a resize refreshes them below.\n }, [key]);\n\n useIsomorphicLayoutEffect(() => {\n const root = container.current;\n if (!root || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => {\n remembered.current = measure();\n });\n observer.observe(root);\n return () => observer.disconnect();\n }, []);\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"flip.js","names":[],"sources":["../../src/lib/flip.ts"],"sourcesContent":["import { useRef, type RefObject } from \"react\";\n\nimport { prefersReducedMotion, useIsomorphicLayoutEffect } from \"./motion.js\";\n\n/**\n * Siblings that shift because something next to them changed size (a wider month\n * name pushes the day and year along) slide to their new place instead of\n * jumping: measure where each `[data-flip]` element was, let the new layout\n * commit, put each one back where it was with a translate, then release it on a\n * transition. CSS cannot do this alone, because the layout move is not a style\n * change.\n *\n * `key` says when to look. Positions are remembered after every look and\n * refreshed when the container resizes, so a move that is not a change of `key`\n * (a window resize) does not become the next flip. Reduced motion does not flip.\n * Elements inside a leaving copy (`[data-leaving]`) are ignored.\n *\n * @param duration CSS time of the slide, e.g. `var(--duration-standard)`.\n * @param easing CSS easing of the slide, e.g. `var(--ease-enter)`.\n * @param relative Measure positions from the container's own corner instead of the\n * viewport's, so scrolling the page between two looks is not mistaken for the elements\n * moving. For lists whose container stays put (rows of a table); content that also moves\n * because its container does (centred text) wants the default.\n */\nexport function useFlip(\n container: RefObject<HTMLElement | null>,\n key: unknown,\n duration = \"var(--duration-standard)\",\n easing = \"var(--ease-enter)\",\n relative = false,\n) {\n const remembered = useRef(new Map<Element, { left: number; top: number }>());\n\n const measure = () => {\n const root = container.current;\n const next = new Map<Element, { left: number; top: number }>();\n if (!root) return next;\n const origin = relative ? root.getBoundingClientRect() : { left: 0, top: 0 };\n for (const el of root.querySelectorAll<HTMLElement>(\"[data-flip]\")) {\n if (el.closest(\"[data-leaving]\")) continue;\n const box = el.getBoundingClientRect();\n next.set(el, { left: box.left - origin.left, top: box.top - origin.top });\n }\n return next;\n };\n\n useIsomorphicLayoutEffect(() => {\n const root = container.current;\n if (!root) return;\n const before = remembered.current;\n const after = measure();\n if (before.size > 0 && !prefersReducedMotion()) {\n const moved: HTMLElement[] = [];\n for (const [el, now] of after) {\n const was = before.get(el);\n if (!was) continue;\n const dx = was.left - now.left;\n const dy = was.top - now.top;\n if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) continue;\n const node = el as HTMLElement;\n node.style.transition = \"none\";\n node.style.translate = `${dx}px ${dy}px`;\n moved.push(node);\n }\n if (moved.length > 0) {\n void root.offsetWidth;\n for (const node of moved) {\n node.style.transition = `translate ${duration} ${easing}`;\n node.style.translate = \"\";\n node.addEventListener(\n \"transitionend\",\n () => {\n node.style.transition = \"\";\n },\n { once: true },\n );\n }\n }\n }\n remembered.current = after;\n // Positions are read once per change of `key`; a resize refreshes them below.\n }, [key]);\n\n useIsomorphicLayoutEffect(() => {\n const root = container.current;\n if (!root || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => {\n remembered.current = measure();\n });\n observer.observe(root);\n return () => observer.disconnect();\n }, []);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,QACd,WACA,KACA,WAAW,4BACX,SAAS,qBACT,WAAW,OACX;CACA,MAAM,aAAa,uBAAO,IAAI,IAA4C,CAAC;CAE3E,MAAM,gBAAgB;EACpB,MAAM,OAAO,UAAU;EACvB,MAAM,uBAAO,IAAI,IAA4C;EAC7D,IAAI,CAAC,MAAM,OAAO;EAClB,MAAM,SAAS,WAAW,KAAK,sBAAsB,IAAI;GAAE,MAAM;GAAG,KAAK;EAAE;EAC3E,KAAK,MAAM,MAAM,KAAK,iBAA8B,aAAa,GAAG;GAClE,IAAI,GAAG,QAAQ,gBAAgB,GAAG;GAClC,MAAM,MAAM,GAAG,sBAAsB;GACrC,KAAK,IAAI,IAAI;IAAE,MAAM,IAAI,OAAO,OAAO;IAAM,KAAK,IAAI,MAAM,OAAO;GAAI,CAAC;EAC1E;EACA,OAAO;CACT;CAEA,gCAAgC;EAC9B,MAAM,OAAO,UAAU;EACvB,IAAI,CAAC,MAAM;EACX,MAAM,SAAS,WAAW;EAC1B,MAAM,QAAQ,QAAQ;EACtB,IAAI,OAAO,OAAO,KAAK,CAAC,qBAAqB,GAAG;GAC9C,MAAM,QAAuB,CAAC;GAC9B,KAAK,MAAM,CAAC,IAAI,QAAQ,OAAO;IAC7B,MAAM,MAAM,OAAO,IAAI,EAAE;IACzB,IAAI,CAAC,KAAK;IACV,MAAM,KAAK,IAAI,OAAO,IAAI;IAC1B,MAAM,KAAK,IAAI,MAAM,IAAI;IACzB,IAAI,KAAK,IAAI,EAAE,IAAI,MAAO,KAAK,IAAI,EAAE,IAAI,IAAK;IAC9C,MAAM,OAAO;IACb,KAAK,MAAM,aAAa;IACxB,KAAK,MAAM,YAAY,GAAG,GAAG,KAAK,GAAG;IACrC,MAAM,KAAK,IAAI;GACjB;GACA,IAAI,MAAM,SAAS,GAAG;IACpB,KAAU;IACV,KAAK,MAAM,QAAQ,OAAO;KACxB,KAAK,MAAM,aAAa,aAAa,SAAS,GAAG;KACjD,KAAK,MAAM,YAAY;KACvB,KAAK,iBACH,uBACM;MACJ,KAAK,MAAM,aAAa;KAC1B,GACA,EAAE,MAAM,KAAK,CACf;IACF;GACF;EACF;EACA,WAAW,UAAU;CAEvB,GAAG,CAAC,GAAG,CAAC;CAER,gCAAgC;EAC9B,MAAM,OAAO,UAAU;EACvB,IAAI,CAAC,QAAQ,OAAO,mBAAmB,aAAa;EACpD,MAAM,WAAW,IAAI,qBAAqB;GACxC,WAAW,UAAU,QAAQ;EAC/B,CAAC;EACD,SAAS,QAAQ,IAAI;EACrB,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,CAAC;AACP"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
//#region src/lib/menu-highlight.ts
|
|
3
|
+
const ROWS = "[role=\"menuitem\"], [role=\"menuitemradio\"], [role=\"menuitemcheckbox\"]";
|
|
4
|
+
/**
|
|
5
|
+
* One highlight for a whole menu, drawn by the menu's ::before (the
|
|
6
|
+
* `menu-highlight` utility) and moved from here: it appears in place on the
|
|
7
|
+
* first row, glides between rows after that (pointer and arrow keys alike,
|
|
8
|
+
* because React Aria focuses the row either way), and fades out shortly after
|
|
9
|
+
* focus leaves the rows. A row marked `data-destructive` tints it red. Returns
|
|
10
|
+
* the cleanup.
|
|
11
|
+
*/
|
|
12
|
+
function bindMenuHighlight(menu) {
|
|
13
|
+
let shown = false;
|
|
14
|
+
let timer = 0;
|
|
15
|
+
const hide = () => {
|
|
16
|
+
menu.removeAttribute("data-hl");
|
|
17
|
+
menu.removeAttribute("data-glide");
|
|
18
|
+
shown = false;
|
|
19
|
+
};
|
|
20
|
+
const onFocusIn = (event) => {
|
|
21
|
+
window.clearTimeout(timer);
|
|
22
|
+
const row = event.target.closest(ROWS);
|
|
23
|
+
if (!row || !menu.contains(row) || row.hasAttribute("data-disabled")) {
|
|
24
|
+
timer = window.setTimeout(hide, 70);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (shown) menu.setAttribute("data-glide", "");
|
|
28
|
+
else menu.removeAttribute("data-glide");
|
|
29
|
+
menu.style.setProperty("--hl-y", row.offsetTop + "px");
|
|
30
|
+
menu.style.setProperty("--hl-h", row.offsetHeight + "px");
|
|
31
|
+
if (row.hasAttribute("data-destructive")) menu.setAttribute("data-hl-tone", "destructive");
|
|
32
|
+
else menu.removeAttribute("data-hl-tone");
|
|
33
|
+
menu.setAttribute("data-hl", "");
|
|
34
|
+
shown = true;
|
|
35
|
+
};
|
|
36
|
+
const onFocusOut = (event) => {
|
|
37
|
+
if (menu.contains(event.relatedTarget)) return;
|
|
38
|
+
window.clearTimeout(timer);
|
|
39
|
+
hide();
|
|
40
|
+
};
|
|
41
|
+
menu.addEventListener("focusin", onFocusIn);
|
|
42
|
+
menu.addEventListener("focusout", onFocusOut);
|
|
43
|
+
return () => {
|
|
44
|
+
window.clearTimeout(timer);
|
|
45
|
+
menu.removeEventListener("focusin", onFocusIn);
|
|
46
|
+
menu.removeEventListener("focusout", onFocusOut);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A ref callback for the element that carries `menu-highlight`. A menu that
|
|
51
|
+
* mounts only while open (a popover) binds when it appears and unbinds when it
|
|
52
|
+
* goes, so this is a callback ref rather than an effect.
|
|
53
|
+
*/
|
|
54
|
+
function useMenuHighlight() {
|
|
55
|
+
const unbind = useRef(void 0);
|
|
56
|
+
return useCallback((node) => {
|
|
57
|
+
unbind.current?.();
|
|
58
|
+
unbind.current = node ? bindMenuHighlight(node) : void 0;
|
|
59
|
+
}, []);
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { useMenuHighlight };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=menu-highlight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-highlight.js","names":[],"sources":["../../src/lib/menu-highlight.ts"],"sourcesContent":["import { useCallback, useRef } from \"react\";\n\nconst ROWS = '[role=\"menuitem\"], [role=\"menuitemradio\"], [role=\"menuitemcheckbox\"]';\n\n/**\n * One highlight for a whole menu, drawn by the menu's ::before (the\n * `menu-highlight` utility) and moved from here: it appears in place on the\n * first row, glides between rows after that (pointer and arrow keys alike,\n * because React Aria focuses the row either way), and fades out shortly after\n * focus leaves the rows. A row marked `data-destructive` tints it red. Returns\n * the cleanup.\n */\nfunction bindMenuHighlight(menu: HTMLElement) {\n let shown = false;\n let timer = 0;\n const hide = () => {\n menu.removeAttribute(\"data-hl\");\n menu.removeAttribute(\"data-glide\");\n shown = false;\n };\n const onFocusIn = (event: FocusEvent) => {\n window.clearTimeout(timer);\n const row = (event.target as Element).closest<HTMLElement>(ROWS);\n // Between rows (a gap, the separator, the menu itself) it waits a moment, so it can glide across.\n if (!row || !menu.contains(row) || row.hasAttribute(\"data-disabled\")) {\n timer = window.setTimeout(hide, 70);\n return;\n }\n if (shown) menu.setAttribute(\"data-glide\", \"\");\n else menu.removeAttribute(\"data-glide\");\n menu.style.setProperty(\"--hl-y\", row.offsetTop + \"px\");\n menu.style.setProperty(\"--hl-h\", row.offsetHeight + \"px\");\n if (row.hasAttribute(\"data-destructive\")) menu.setAttribute(\"data-hl-tone\", \"destructive\");\n else menu.removeAttribute(\"data-hl-tone\");\n menu.setAttribute(\"data-hl\", \"\");\n shown = true;\n };\n const onFocusOut = (event: FocusEvent) => {\n if (menu.contains(event.relatedTarget as Node | null)) return;\n window.clearTimeout(timer);\n hide();\n };\n menu.addEventListener(\"focusin\", onFocusIn);\n menu.addEventListener(\"focusout\", onFocusOut);\n return () => {\n window.clearTimeout(timer);\n menu.removeEventListener(\"focusin\", onFocusIn);\n menu.removeEventListener(\"focusout\", onFocusOut);\n };\n}\n\n/**\n * A ref callback for the element that carries `menu-highlight`. A menu that\n * mounts only while open (a popover) binds when it appears and unbinds when it\n * goes, so this is a callback ref rather than an effect.\n */\nexport function useMenuHighlight() {\n const unbind = useRef<(() => void) | undefined>(undefined);\n return useCallback((node: HTMLElement | null) => {\n unbind.current?.();\n unbind.current = node ? bindMenuHighlight(node) : undefined;\n }, []);\n}\n"],"mappings":";;AAEA,MAAM,OAAO;;;;;;;;;AAUb,SAAS,kBAAkB,MAAmB;CAC5C,IAAI,QAAQ;CACZ,IAAI,QAAQ;CACZ,MAAM,aAAa;EACjB,KAAK,gBAAgB,SAAS;EAC9B,KAAK,gBAAgB,YAAY;EACjC,QAAQ;CACV;CACA,MAAM,aAAa,UAAsB;EACvC,OAAO,aAAa,KAAK;EACzB,MAAM,MAAO,MAAM,OAAmB,QAAqB,IAAI;EAE/D,IAAI,CAAC,OAAO,CAAC,KAAK,SAAS,GAAG,KAAK,IAAI,aAAa,eAAe,GAAG;GACpE,QAAQ,OAAO,WAAW,MAAM,EAAE;GAClC;EACF;EACA,IAAI,OAAO,KAAK,aAAa,cAAc,EAAE;OACxC,KAAK,gBAAgB,YAAY;EACtC,KAAK,MAAM,YAAY,UAAU,IAAI,YAAY,IAAI;EACrD,KAAK,MAAM,YAAY,UAAU,IAAI,eAAe,IAAI;EACxD,IAAI,IAAI,aAAa,kBAAkB,GAAG,KAAK,aAAa,gBAAgB,aAAa;OACpF,KAAK,gBAAgB,cAAc;EACxC,KAAK,aAAa,WAAW,EAAE;EAC/B,QAAQ;CACV;CACA,MAAM,cAAc,UAAsB;EACxC,IAAI,KAAK,SAAS,MAAM,aAA4B,GAAG;EACvD,OAAO,aAAa,KAAK;EACzB,KAAK;CACP;CACA,KAAK,iBAAiB,WAAW,SAAS;CAC1C,KAAK,iBAAiB,YAAY,UAAU;CAC5C,aAAa;EACX,OAAO,aAAa,KAAK;EACzB,KAAK,oBAAoB,WAAW,SAAS;EAC7C,KAAK,oBAAoB,YAAY,UAAU;CACjD;AACF;;;;;;AAOA,SAAgB,mBAAmB;CACjC,MAAM,SAAS,OAAiC,KAAA,CAAS;CACzD,OAAO,aAAa,SAA6B;EAC/C,OAAO,UAAU;EACjB,OAAO,UAAU,OAAO,kBAAkB,IAAI,IAAI,KAAA;CACpD,GAAG,CAAC,CAAC;AACP"}
|
package/dist/lib/motion.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
2
2
|
//#region src/lib/motion.ts
|
|
3
3
|
/** useLayoutEffect warns when rendered on the server in React 18, which this package supports; there it falls back to useEffect. */
|
|
4
4
|
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
5
5
|
const prefersReducedMotion = () => window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
6
|
+
const subscribeReducedMotion = (notify) => {
|
|
7
|
+
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
8
|
+
query.addEventListener("change", notify);
|
|
9
|
+
return () => query.removeEventListener("change", notify);
|
|
10
|
+
};
|
|
11
|
+
/** Whether the visitor prefers reduced motion, kept current. False on the server and until hydrated. */
|
|
12
|
+
const useReducedMotion = () => useSyncExternalStore(subscribeReducedMotion, prefersReducedMotion, () => false);
|
|
6
13
|
/**
|
|
7
14
|
* Springs the slot to the natural width of the incoming label when it changes,
|
|
8
15
|
* so a new label never snaps the button's size. Only a resize within 700ms of a
|
|
@@ -157,6 +164,6 @@ function useAriaBusy(pending) {
|
|
|
157
164
|
return ref;
|
|
158
165
|
}
|
|
159
166
|
//#endregion
|
|
160
|
-
export { anchorsLayer, prefersReducedMotion, pressScaleFor, useAriaBusy, useExitBackstop, useIsomorphicLayoutEffect, useLeavingItems, useMorphWidth };
|
|
167
|
+
export { anchorsLayer, prefersReducedMotion, pressScaleFor, useAriaBusy, useExitBackstop, useIsomorphicLayoutEffect, useLeavingItems, useMorphWidth, useReducedMotion };
|
|
161
168
|
|
|
162
169
|
//# sourceMappingURL=motion.js.map
|
package/dist/lib/motion.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"motion.js","names":[],"sources":["../../src/lib/motion.ts"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useRef, useState, type RefObject } from \"react\";\n\n/*\n * Motion helpers shared by every component with button-like motion: the press\n * depth, the width that springs to fit new content, and the backstop for an\n * exit animation that never ends. Plain React and CSS, no animation library;\n * the animations themselves are keyframes and transitions in\n * @seekalfred/styles motion.css and the button-* utilities in styles.css.\n */\n\n/** useLayoutEffect warns when rendered on the server in React 18, which this package supports; there it falls back to useEffect. */\nexport const useIsomorphicLayoutEffect =\n typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nexport const prefersReducedMotion = () =>\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n/**\n * Springs the slot to the natural width of the incoming label when it changes,\n * so a new label never snaps the button's size. Only a resize within 700ms of a\n * label change morphs; any other resize (a late web font, a parent reflow),\n * the first measurement and reduced motion jump straight to the new width, so\n * nothing wobbles on first paint. data-morphing turns on the slot's width\n * transition and its clip-path (button-slot).\n */\nexport function useMorphWidth(content: RefObject<HTMLElement | null>, key: string, enabled = true) {\n const lastKey = useRef(key);\n const armedUntil = useRef(0);\n useIsomorphicLayoutEffect(() => {\n if (lastKey.current === key) return;\n lastKey.current = key;\n armedUntil.current = performance.now() + 700;\n }, [key]);\n useEffect(() => {\n const node = content.current;\n const slot = node?.parentElement;\n if (!enabled || !node || !slot || typeof ResizeObserver === \"undefined\") return;\n let measured = false;\n let settle: ReturnType<typeof setTimeout> | undefined;\n const done = () => {\n clearTimeout(settle);\n delete slot.dataset.morphing;\n };\n const onEnd = (event: TransitionEvent) => {\n if (event.target === slot && event.propertyName === \"width\") done();\n };\n slot.addEventListener(\"transitionend\", onEnd);\n const apply = (next: number, at: number) => {\n if (!next || !measured || prefersReducedMotion() || at > armedUntil.current) {\n measured = next > 0;\n slot.style.width = next ? next + \"px\" : \"\";\n done();\n return;\n }\n slot.dataset.morphing = \"\";\n slot.style.width = next + \"px\";\n // A morph that ends without a width transition (nothing to animate) would\n // leave the clip-path on; this is the backstop.\n clearTimeout(settle);\n settle = setTimeout(done, 900);\n };\n // The write is deferred a frame. Setting the slot's width resizes whatever\n // contains it, and if that is observed too (a segment in a button group) doing\n // it inside this notification reports a ResizeObserver loop error.\n let frame = 0;\n const observer = new ResizeObserver(([entry]) => {\n if (!entry) return;\n const next = entry.contentRect.width;\n const at = performance.now();\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(() => apply(next, at));\n });\n observer.observe(node);\n return () => {\n cancelAnimationFrame(frame);\n observer.disconnect();\n slot.removeEventListener(\"transitionend\", onEnd);\n clearTimeout(settle);\n };\n }, [content, enabled]);\n}\n\n/**\n * Runs remove after a backstop delay, in case an exit animation never ends (a\n * consumer who disables animation). The delay must outlast the animation:\n * 600ms covers the quick exits, a spring that settles in 736ms needs more.\n */\nexport function useExitBackstop(active: boolean, remove: () => void, delay = 600) {\n useEffect(() => {\n if (!active) return;\n const timer = setTimeout(remove, delay);\n return () => clearTimeout(timer);\n }, [active, remove, delay]);\n}\n\n/** A press goes a little deeper on an icon button and a little less on a wide one, so every size reads as the same push. */\nexport function pressScaleFor(width: number) {\n return width > 220 ? \"0.985\" : width && width <= 48 ? \"0.96\" : \"0.97\";\n}\n\n/** True for a trigger that anchors a menu, popover or dialog; those never scale. */\nexport function anchorsLayer(props: object) {\n const { \"aria-haspopup\": popup, role, \"data-state\": state } = props as Record<string, unknown>;\n return (\n (popup !== undefined && popup !== false && popup !== \"false\") ||\n role === \"combobox\" ||\n state !== undefined\n );\n}\n\n/** Where a node sat inside its offset parent, in px. */\nexport interface Rect {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\nexport interface LeavingItem<T> {\n id: number;\n value: T;\n /** The place the node last occupied, so it can be popped out of flow right there. */\n rect: Rect;\n}\n\n/**\n * Keeps the outgoing version of something on screen while its replacement\n * enters, which CSS alone cannot do: a node React unmounts is gone.\n *\n * `key` says when it has changed. On a change the previous `value` joins\n * `leaving`, stamped with the place it last occupied (read from `measure`,\n * the CURRENT node, after every commit); the caller renders each leaving item\n * out of flow at that rect with an exit animation and calls `remove` when it\n * ends. `changed` turns true on the first change, so nothing animates on first\n * mount. It must be a layout effect: the old node's position is DOM state that\n * only exists until the next paint.\n */\nexport function useLeavingItems<T>(key: string, value: T, measure: () => HTMLElement | null) {\n const previous = useRef({ key, value });\n const rect = useRef<Rect>({ left: 0, top: 0, width: 0, height: 0 });\n const nextId = useRef(0);\n const [leaving, setLeaving] = useState<LeavingItem<T>[]>([]);\n const [changed, setChanged] = useState(false);\n\n // Declared before the effect below, which overwrites the record this reads.\n useIsomorphicLayoutEffect(() => {\n const old = previous.current;\n if (old.key === key) return;\n setLeaving((list) => [...list, { id: nextId.current++, value: old.value, rect: rect.current }]);\n setChanged(true);\n }, [key]);\n\n useIsomorphicLayoutEffect(() => {\n previous.current = { key, value };\n const node = measure();\n if (node)\n rect.current = {\n left: node.offsetLeft,\n top: node.offsetTop,\n width: node.offsetWidth,\n height: node.offsetHeight,\n };\n });\n\n const remove = useCallback(\n (id: number) => setLeaving((list) => list.filter((item) => item.id !== id)),\n [],\n );\n return { leaving, changed, remove };\n}\n\n/**\n * React Aria sets aria-disabled and data-pending on a pending button but\n * forwards no aria-busy, so a pending button was not announced as busy. Put the\n * returned ref on the button (merge it with any other); nothing else manages\n * this attribute on the element.\n */\nexport function useAriaBusy(pending: boolean) {\n const ref = useRef<HTMLElement | null>(null);\n useIsomorphicLayoutEffect(() => {\n if (pending) ref.current?.setAttribute(\"aria-busy\", \"true\");\n else ref.current?.removeAttribute(\"aria-busy\");\n }, [pending]);\n return ref;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"motion.js","names":[],"sources":["../../src/lib/motion.ts"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, type RefObject } from \"react\";\n\n/*\n * Motion helpers shared by every component with button-like motion: the press\n * depth, the width that springs to fit new content, and the backstop for an\n * exit animation that never ends. Plain React and CSS, no animation library;\n * the animations themselves are keyframes and transitions in\n * @seekalfred/styles motion.css and the button-* utilities in styles.css.\n */\n\n/** useLayoutEffect warns when rendered on the server in React 18, which this package supports; there it falls back to useEffect. */\nexport const useIsomorphicLayoutEffect =\n typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nexport const prefersReducedMotion = () =>\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\nconst subscribeReducedMotion = (notify: () => void) => {\n const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n query.addEventListener(\"change\", notify);\n return () => query.removeEventListener(\"change\", notify);\n};\n\n/** Whether the visitor prefers reduced motion, kept current. False on the server and until hydrated. */\nexport const useReducedMotion = () =>\n useSyncExternalStore(subscribeReducedMotion, prefersReducedMotion, () => false);\n\n/**\n * Springs the slot to the natural width of the incoming label when it changes,\n * so a new label never snaps the button's size. Only a resize within 700ms of a\n * label change morphs; any other resize (a late web font, a parent reflow),\n * the first measurement and reduced motion jump straight to the new width, so\n * nothing wobbles on first paint. data-morphing turns on the slot's width\n * transition and its clip-path (button-slot).\n */\nexport function useMorphWidth(content: RefObject<HTMLElement | null>, key: string, enabled = true) {\n const lastKey = useRef(key);\n const armedUntil = useRef(0);\n useIsomorphicLayoutEffect(() => {\n if (lastKey.current === key) return;\n lastKey.current = key;\n armedUntil.current = performance.now() + 700;\n }, [key]);\n useEffect(() => {\n const node = content.current;\n const slot = node?.parentElement;\n if (!enabled || !node || !slot || typeof ResizeObserver === \"undefined\") return;\n let measured = false;\n let settle: ReturnType<typeof setTimeout> | undefined;\n const done = () => {\n clearTimeout(settle);\n delete slot.dataset.morphing;\n };\n const onEnd = (event: TransitionEvent) => {\n if (event.target === slot && event.propertyName === \"width\") done();\n };\n slot.addEventListener(\"transitionend\", onEnd);\n const apply = (next: number, at: number) => {\n if (!next || !measured || prefersReducedMotion() || at > armedUntil.current) {\n measured = next > 0;\n slot.style.width = next ? next + \"px\" : \"\";\n done();\n return;\n }\n slot.dataset.morphing = \"\";\n slot.style.width = next + \"px\";\n // A morph that ends without a width transition (nothing to animate) would\n // leave the clip-path on; this is the backstop.\n clearTimeout(settle);\n settle = setTimeout(done, 900);\n };\n // The write is deferred a frame. Setting the slot's width resizes whatever\n // contains it, and if that is observed too (a segment in a button group) doing\n // it inside this notification reports a ResizeObserver loop error.\n let frame = 0;\n const observer = new ResizeObserver(([entry]) => {\n if (!entry) return;\n const next = entry.contentRect.width;\n const at = performance.now();\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(() => apply(next, at));\n });\n observer.observe(node);\n return () => {\n cancelAnimationFrame(frame);\n observer.disconnect();\n slot.removeEventListener(\"transitionend\", onEnd);\n clearTimeout(settle);\n };\n }, [content, enabled]);\n}\n\n/**\n * Runs remove after a backstop delay, in case an exit animation never ends (a\n * consumer who disables animation). The delay must outlast the animation:\n * 600ms covers the quick exits, a spring that settles in 736ms needs more.\n */\nexport function useExitBackstop(active: boolean, remove: () => void, delay = 600) {\n useEffect(() => {\n if (!active) return;\n const timer = setTimeout(remove, delay);\n return () => clearTimeout(timer);\n }, [active, remove, delay]);\n}\n\n/** A press goes a little deeper on an icon button and a little less on a wide one, so every size reads as the same push. */\nexport function pressScaleFor(width: number) {\n return width > 220 ? \"0.985\" : width && width <= 48 ? \"0.96\" : \"0.97\";\n}\n\n/** True for a trigger that anchors a menu, popover or dialog; those never scale. */\nexport function anchorsLayer(props: object) {\n const { \"aria-haspopup\": popup, role, \"data-state\": state } = props as Record<string, unknown>;\n return (\n (popup !== undefined && popup !== false && popup !== \"false\") ||\n role === \"combobox\" ||\n state !== undefined\n );\n}\n\n/** Where a node sat inside its offset parent, in px. */\nexport interface Rect {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\nexport interface LeavingItem<T> {\n id: number;\n value: T;\n /** The place the node last occupied, so it can be popped out of flow right there. */\n rect: Rect;\n}\n\n/**\n * Keeps the outgoing version of something on screen while its replacement\n * enters, which CSS alone cannot do: a node React unmounts is gone.\n *\n * `key` says when it has changed. On a change the previous `value` joins\n * `leaving`, stamped with the place it last occupied (read from `measure`,\n * the CURRENT node, after every commit); the caller renders each leaving item\n * out of flow at that rect with an exit animation and calls `remove` when it\n * ends. `changed` turns true on the first change, so nothing animates on first\n * mount. It must be a layout effect: the old node's position is DOM state that\n * only exists until the next paint.\n */\nexport function useLeavingItems<T>(key: string, value: T, measure: () => HTMLElement | null) {\n const previous = useRef({ key, value });\n const rect = useRef<Rect>({ left: 0, top: 0, width: 0, height: 0 });\n const nextId = useRef(0);\n const [leaving, setLeaving] = useState<LeavingItem<T>[]>([]);\n const [changed, setChanged] = useState(false);\n\n // Declared before the effect below, which overwrites the record this reads.\n useIsomorphicLayoutEffect(() => {\n const old = previous.current;\n if (old.key === key) return;\n setLeaving((list) => [...list, { id: nextId.current++, value: old.value, rect: rect.current }]);\n setChanged(true);\n }, [key]);\n\n useIsomorphicLayoutEffect(() => {\n previous.current = { key, value };\n const node = measure();\n if (node)\n rect.current = {\n left: node.offsetLeft,\n top: node.offsetTop,\n width: node.offsetWidth,\n height: node.offsetHeight,\n };\n });\n\n const remove = useCallback(\n (id: number) => setLeaving((list) => list.filter((item) => item.id !== id)),\n [],\n );\n return { leaving, changed, remove };\n}\n\n/**\n * React Aria sets aria-disabled and data-pending on a pending button but\n * forwards no aria-busy, so a pending button was not announced as busy. Put the\n * returned ref on the button (merge it with any other); nothing else manages\n * this attribute on the element.\n */\nexport function useAriaBusy(pending: boolean) {\n const ref = useRef<HTMLElement | null>(null);\n useIsomorphicLayoutEffect(() => {\n if (pending) ref.current?.setAttribute(\"aria-busy\", \"true\");\n else ref.current?.removeAttribute(\"aria-busy\");\n }, [pending]);\n return ref;\n}\n"],"mappings":"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"}
|
|
@@ -19,7 +19,7 @@ function RollingValue({ srValue, swapKey, dir, instanceKey, children }) {
|
|
|
19
19
|
swapKey,
|
|
20
20
|
dir,
|
|
21
21
|
motion: "roll",
|
|
22
|
-
className: "
|
|
22
|
+
className: "flex items-center [&>span:not([aria-hidden])]:max-w-full [&>span:not([aria-hidden])]:truncate",
|
|
23
23
|
children
|
|
24
24
|
}, instanceKey)
|
|
25
25
|
})]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rolling-value.js","names":[],"sources":["../../src/lib/rolling-value.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport { SwapText } from \"./swap-text.js\";\n\n/**\n * The value shown in a picker's trigger. The caller's own text stays, visually\n * hidden, as what a screen reader reads (`srValue`); the copy the eye sees rolls\n * when it changes: a later value rises in from below, an earlier one drops in from\n * above, and the old one lifts away. Nothing animates on first mount. Long text is\n * cut with an ellipsis. `instanceKey` remounts the swap, which is how a caller whose\n * first value arrives a render late makes that arrival the first mount.\n */\nexport function RollingValue({\n srValue,\n swapKey,\n dir,\n instanceKey,\n children,\n}: {\n srValue: ReactNode;\n swapKey: string;\n dir: number;\n instanceKey?: string;\n children: ReactNode;\n}) {\n return (\n <span className=\"grid min-w-0 flex-1\">\n {srValue}\n <span aria-hidden=\"true\" className=\"min-w-0 overflow-hidden whitespace-nowrap\">\n <SwapText\n key={instanceKey}\n swapKey={swapKey}\n dir={dir}\n motion=\"roll\"\n className=\"
|
|
1
|
+
{"version":3,"file":"rolling-value.js","names":[],"sources":["../../src/lib/rolling-value.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport { SwapText } from \"./swap-text.js\";\n\n/**\n * The value shown in a picker's trigger. The caller's own text stays, visually\n * hidden, as what a screen reader reads (`srValue`); the copy the eye sees rolls\n * when it changes: a later value rises in from below, an earlier one drops in from\n * above, and the old one lifts away. Nothing animates on first mount. Long text is\n * cut with an ellipsis. `instanceKey` remounts the swap, which is how a caller whose\n * first value arrives a render late makes that arrival the first mount.\n */\nexport function RollingValue({\n srValue,\n swapKey,\n dir,\n instanceKey,\n children,\n}: {\n srValue: ReactNode;\n swapKey: string;\n dir: number;\n instanceKey?: string;\n children: ReactNode;\n}) {\n return (\n <span className=\"grid min-w-0 flex-1\">\n {srValue}\n <span aria-hidden=\"true\" className=\"min-w-0 overflow-hidden whitespace-nowrap\">\n <SwapText\n key={instanceKey}\n swapKey={swapKey}\n dir={dir}\n motion=\"roll\"\n // A flex row, not a block of inline boxes: a truncated inline-block sits on the\n // line's baseline, which stretches the line and leaves the text above centre.\n className=\"flex items-center [&>span:not([aria-hidden])]:max-w-full [&>span:not([aria-hidden])]:truncate\"\n >\n {children}\n </SwapText>\n </span>\n </span>\n );\n}\n"],"mappings":";;;;;;;;;;;AAYA,SAAgB,aAAa,EAC3B,SACA,SACA,KACA,aACA,YAOC;CACD,OACE,qBAAC,QAAD;EAAM,WAAU;EAAhB,UAAA,CACG,SACD,oBAAC,QAAD;GAAM,eAAY;GAAO,WAAU;GACjC,UAAA,oBAAC,UAAD;IAEW;IACJ;IACL,QAAO;IAGP,WAAU;IAET;GACO,GATH,WASG;EACN,CAAA,CACF;;AAEV"}
|