@seekalfred/primitives 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -11
- package/dist/activity-heatmap/activity-heatmap.d.ts +92 -0
- package/dist/activity-heatmap/activity-heatmap.js +699 -0
- package/dist/activity-heatmap/activity-heatmap.js.map +1 -0
- package/dist/activity-heatmap/index.d.ts +2 -0
- package/dist/activity-heatmap/index.js +2 -0
- package/dist/alert/alert.d.ts +5 -1
- package/dist/alert/alert.js +68 -7
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert-dialog/alert-dialog.d.ts +45 -18
- package/dist/alert-dialog/alert-dialog.js +128 -9
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/alert-dialog/index.d.ts +2 -2
- package/dist/alert-dialog/index.js +2 -2
- package/dist/animated-counter/animated-counter.d.ts +38 -0
- package/dist/animated-counter/animated-counter.js +230 -0
- package/dist/animated-counter/animated-counter.js.map +1 -0
- package/dist/animated-counter/index.d.ts +2 -0
- package/dist/animated-counter/index.js +2 -0
- package/dist/announcement-bar/announcement-bar.d.ts +67 -0
- package/dist/announcement-bar/announcement-bar.js +364 -0
- package/dist/announcement-bar/announcement-bar.js.map +1 -0
- package/dist/announcement-bar/index.d.ts +2 -0
- package/dist/announcement-bar/index.js +2 -0
- package/dist/badge/badge.d.ts +1 -1
- package/dist/bar-chart/bar-chart.d.ts +51 -0
- package/dist/bar-chart/bar-chart.js +565 -0
- package/dist/bar-chart/bar-chart.js.map +1 -0
- package/dist/bar-chart/index.d.ts +2 -0
- package/dist/bar-chart/index.js +2 -0
- package/dist/breadcrumb/breadcrumb.d.ts +2 -2
- package/dist/breadcrumb/breadcrumb.js +12 -3
- package/dist/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/brush-chart/brush-chart.d.ts +64 -0
- package/dist/brush-chart/brush-chart.js +1056 -0
- package/dist/brush-chart/brush-chart.js.map +1 -0
- package/dist/brush-chart/index.d.ts +2 -0
- package/dist/brush-chart/index.js +2 -0
- package/dist/button/button.d.ts +2 -2
- package/dist/button/button.js +31 -21
- package/dist/button/button.js.map +1 -1
- package/dist/button-group/button-group.js +3 -4
- package/dist/button-group/button-group.js.map +1 -1
- package/dist/card/card.js +1 -1
- package/dist/card/card.js.map +1 -1
- package/dist/card-stack/card-stack.d.ts +54 -0
- package/dist/card-stack/card-stack.js +492 -0
- package/dist/card-stack/card-stack.js.map +1 -0
- package/dist/card-stack/index.d.ts +2 -0
- package/dist/card-stack/index.js +2 -0
- package/dist/carousel/carousel.d.ts +56 -0
- package/dist/carousel/carousel.js +389 -0
- package/dist/carousel/carousel.js.map +1 -0
- package/dist/carousel/index.d.ts +2 -0
- package/dist/carousel/index.js +2 -0
- package/dist/checkbox/checkbox.js +4 -4
- package/dist/checkbox/checkbox.js.map +1 -1
- package/dist/chip-group/chip-group.d.ts +37 -0
- package/dist/chip-group/chip-group.js +148 -0
- package/dist/chip-group/chip-group.js.map +1 -0
- package/dist/chip-group/index.d.ts +2 -0
- package/dist/chip-group/index.js +2 -0
- package/dist/countdown/countdown.d.ts +45 -0
- package/dist/countdown/countdown.js +298 -0
- package/dist/countdown/countdown.js.map +1 -0
- package/dist/countdown/index.d.ts +2 -0
- package/dist/countdown/index.js +2 -0
- package/dist/date-picker/picker-parts.js +1 -1
- package/dist/date-picker/picker-parts.js.map +1 -1
- package/dist/dialog/dialog.js +1 -1
- package/dist/dialog/dialog.js.map +1 -1
- package/dist/donut-chart/donut-chart.d.ts +64 -0
- package/dist/donut-chart/donut-chart.js +576 -0
- package/dist/donut-chart/donut-chart.js.map +1 -0
- package/dist/donut-chart/index.d.ts +2 -0
- package/dist/donut-chart/index.js +2 -0
- package/dist/empty-state/empty-state.d.ts +21 -10
- package/dist/empty-state/empty-state.js +32 -26
- package/dist/empty-state/empty-state.js.map +1 -1
- package/dist/empty-state/index.d.ts +2 -2
- package/dist/empty-state/index.js +2 -2
- package/dist/file-drop-zone/file-drop-zone.js +1 -1
- package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
- package/dist/filter-bar/filter-bar.d.ts +47 -26
- package/dist/filter-bar/filter-bar.js +272 -156
- package/dist/filter-bar/filter-bar.js.map +1 -1
- package/dist/gauge/gauge.d.ts +45 -0
- package/dist/gauge/gauge.js +219 -0
- package/dist/gauge/gauge.js.map +1 -0
- package/dist/gauge/index.d.ts +2 -0
- package/dist/gauge/index.js +2 -0
- package/dist/in-view-title/in-view-title.d.ts +33 -0
- package/dist/in-view-title/in-view-title.js +119 -0
- package/dist/in-view-title/in-view-title.js.map +1 -0
- package/dist/in-view-title/index.d.ts +2 -0
- package/dist/in-view-title/index.js +2 -0
- package/dist/index.d.ts +49 -6
- package/dist/index.js +48 -8
- package/dist/lib/cn.js +11 -1
- package/dist/lib/cn.js.map +1 -1
- package/dist/lib/field-message.js +28 -10
- package/dist/lib/field-message.js.map +1 -1
- package/dist/lib/field-surface.js +9 -8
- package/dist/lib/field-surface.js.map +1 -1
- package/dist/lib/flag-emoji.js +50 -0
- package/dist/lib/flag-emoji.js.map +1 -0
- package/dist/lib/flip.js +11 -3
- package/dist/lib/flip.js.map +1 -1
- package/dist/lib/menu-highlight.js +64 -0
- package/dist/lib/menu-highlight.js.map +1 -0
- package/dist/lib/motion.js +9 -2
- package/dist/lib/motion.js.map +1 -1
- package/dist/lib/rolling-value.js +1 -1
- package/dist/lib/rolling-value.js.map +1 -1
- package/dist/lib/spring-tween.js +500 -0
- package/dist/lib/spring-tween.js.map +1 -0
- package/dist/lib/swap-text.js +3 -1
- package/dist/lib/swap-text.js.map +1 -1
- package/dist/line-chart/index.d.ts +2 -0
- package/dist/line-chart/index.js +2 -0
- package/dist/line-chart/line-chart.d.ts +76 -0
- package/dist/line-chart/line-chart.js +611 -0
- package/dist/line-chart/line-chart.js.map +1 -0
- package/dist/mention-input/index.d.ts +2 -0
- package/dist/mention-input/index.js +2 -0
- package/dist/mention-input/mention-input.d.ts +98 -0
- package/dist/mention-input/mention-input.js +576 -0
- package/dist/mention-input/mention-input.js.map +1 -0
- package/dist/menu/menu.d.ts +2 -0
- package/dist/menu/menu.js +23 -12
- package/dist/menu/menu.js.map +1 -1
- package/dist/money-input/index.d.ts +2 -0
- package/dist/money-input/index.js +2 -0
- package/dist/money-input/money-input.d.ts +65 -0
- package/dist/money-input/money-input.js +607 -0
- package/dist/money-input/money-input.js.map +1 -0
- package/dist/morph-select/index.d.ts +2 -0
- package/dist/morph-select/index.js +2 -0
- package/dist/morph-select/morph-select.d.ts +71 -0
- package/dist/morph-select/morph-select.js +693 -0
- package/dist/morph-select/morph-select.js.map +1 -0
- package/dist/multi-select/index.d.ts +2 -0
- package/dist/multi-select/index.js +2 -0
- package/dist/multi-select/multi-select.d.ts +36 -0
- package/dist/multi-select/multi-select.js +152 -0
- package/dist/multi-select/multi-select.js.map +1 -0
- package/dist/number-field/index.d.ts +2 -0
- package/dist/number-field/index.js +2 -0
- package/dist/number-field/number-field.d.ts +42 -0
- package/dist/number-field/number-field.js +199 -0
- package/dist/number-field/number-field.js.map +1 -0
- package/dist/onboarding-checklist/index.d.ts +2 -0
- package/dist/onboarding-checklist/index.js +2 -0
- package/dist/onboarding-checklist/onboarding-checklist.d.ts +55 -0
- package/dist/onboarding-checklist/onboarding-checklist.js +577 -0
- package/dist/onboarding-checklist/onboarding-checklist.js.map +1 -0
- package/dist/password-field/password-field.js +2 -2
- package/dist/password-field/password-field.js.map +1 -1
- package/dist/phone-input/index.d.ts +3 -0
- package/dist/phone-input/index.js +3 -0
- package/dist/phone-input/phone-countries.d.ts +29 -0
- package/dist/phone-input/phone-countries.js +170 -0
- package/dist/phone-input/phone-countries.js.map +1 -0
- package/dist/phone-input/phone-input.d.ts +66 -0
- package/dist/phone-input/phone-input.js +440 -0
- package/dist/phone-input/phone-input.js.map +1 -0
- package/dist/popover/popover.d.ts +12 -0
- package/dist/popover/popover.js +31 -7
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/progress.js +1 -1
- package/dist/radio-group/radio-group.js +1 -1
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/reactions/index.d.ts +2 -0
- package/dist/reactions/index.js +2 -0
- package/dist/reactions/reactions.d.ts +50 -0
- package/dist/reactions/reactions.js +315 -0
- package/dist/reactions/reactions.js.map +1 -0
- package/dist/rich-text-editor/index.d.ts +3 -0
- package/dist/rich-text-editor/index.js +2 -0
- package/dist/rich-text-editor/rich-text-editor.d.ts +70 -0
- package/dist/rich-text-editor/rich-text-editor.js +909 -0
- package/dist/rich-text-editor/rich-text-editor.js.map +1 -0
- package/dist/rich-text-editor/rich-text-engine.d.ts +16 -0
- package/dist/rich-text-editor/rich-text-engine.js +366 -0
- package/dist/rich-text-editor/rich-text-engine.js.map +1 -0
- package/dist/rich-text-editor/rich-text-html.js +174 -0
- package/dist/rich-text-editor/rich-text-html.js.map +1 -0
- package/dist/rich-text-editor/rich-text-icons.js +43 -0
- package/dist/rich-text-editor/rich-text-icons.js.map +1 -0
- package/dist/ridgeline/index.d.ts +2 -0
- package/dist/ridgeline/index.js +2 -0
- package/dist/ridgeline/ridgeline.d.ts +55 -0
- package/dist/ridgeline/ridgeline.js +701 -0
- package/dist/ridgeline/ridgeline.js.map +1 -0
- package/dist/scroll-area/index.d.ts +2 -2
- package/dist/scroll-area/index.js +2 -2
- package/dist/scroll-area/scroll-area.d.ts +68 -31
- package/dist/scroll-area/scroll-area.js +208 -59
- package/dist/scroll-area/scroll-area.js.map +1 -1
- package/dist/scrub-input/evaluate.d.ts +14 -0
- package/dist/scrub-input/evaluate.js +72 -0
- package/dist/scrub-input/evaluate.js.map +1 -0
- package/dist/scrub-input/index.d.ts +3 -0
- package/dist/scrub-input/index.js +3 -0
- package/dist/scrub-input/scrub-input.d.ts +74 -0
- package/dist/scrub-input/scrub-input.js +486 -0
- package/dist/scrub-input/scrub-input.js.map +1 -0
- package/dist/search-field/search-field.d.ts +2 -1
- package/dist/search-field/search-field.js +4 -4
- package/dist/search-field/search-field.js.map +1 -1
- package/dist/select/select.d.ts +3 -1
- package/dist/select/select.js +20 -8
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +1 -1
- package/dist/sheet/sheet.js +2 -1
- package/dist/sheet/sheet.js.map +1 -1
- package/dist/shortcut-recorder/index.d.ts +3 -0
- package/dist/shortcut-recorder/index.js +3 -0
- package/dist/shortcut-recorder/shortcut-model.d.ts +27 -0
- package/dist/shortcut-recorder/shortcut-model.js +206 -0
- package/dist/shortcut-recorder/shortcut-model.js.map +1 -0
- package/dist/shortcut-recorder/shortcut-recorder.d.ts +93 -0
- package/dist/shortcut-recorder/shortcut-recorder.js +489 -0
- package/dist/shortcut-recorder/shortcut-recorder.js.map +1 -0
- package/dist/slider/index.d.ts +2 -2
- package/dist/slider/slider.d.ts +46 -14
- package/dist/slider/slider.js +151 -67
- package/dist/slider/slider.js.map +1 -1
- package/dist/slope-chart/index.d.ts +2 -0
- package/dist/slope-chart/index.js +2 -0
- package/dist/slope-chart/slope-chart.d.ts +54 -0
- package/dist/slope-chart/slope-chart.js +568 -0
- package/dist/slope-chart/slope-chart.js.map +1 -0
- package/dist/sparkline/index.d.ts +2 -0
- package/dist/sparkline/index.js +2 -0
- package/dist/sparkline/sparkline.d.ts +46 -0
- package/dist/sparkline/sparkline.js +390 -0
- package/dist/sparkline/sparkline.js.map +1 -0
- package/dist/split-bar/index.d.ts +2 -0
- package/dist/split-bar/index.js +2 -0
- package/dist/split-bar/split-bar.d.ts +62 -0
- package/dist/split-bar/split-bar.js +513 -0
- package/dist/split-bar/split-bar.js.map +1 -0
- package/dist/split-button/split-button.d.ts +1 -1
- package/dist/split-button/split-button.js +12 -11
- package/dist/split-button/split-button.js.map +1 -1
- package/dist/stat-card/index.d.ts +2 -2
- package/dist/stat-card/index.js +2 -2
- package/dist/stat-card/stat-card.d.ts +24 -4
- package/dist/stat-card/stat-card.js +114 -17
- package/dist/stat-card/stat-card.js.map +1 -1
- package/dist/stepper/index.d.ts +2 -0
- package/dist/stepper/index.js +2 -0
- package/dist/stepper/stepper.d.ts +51 -0
- package/dist/stepper/stepper.js +215 -0
- package/dist/stepper/stepper.js.map +1 -0
- package/dist/streamgraph/index.d.ts +2 -0
- package/dist/streamgraph/index.js +2 -0
- package/dist/streamgraph/streamgraph.d.ts +70 -0
- package/dist/streamgraph/streamgraph.js +613 -0
- package/dist/streamgraph/streamgraph.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/swipe-actions/index.d.ts +2 -0
- package/dist/swipe-actions/index.js +2 -0
- package/dist/swipe-actions/swipe-actions.d.ts +67 -0
- package/dist/swipe-actions/swipe-actions.js +552 -0
- package/dist/swipe-actions/swipe-actions.js.map +1 -0
- package/dist/switch/switch.js +2 -2
- package/dist/switch/switch.js.map +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +2 -2
- package/dist/table/table.d.ts +56 -4
- package/dist/table/table.js +340 -165
- package/dist/table/table.js.map +1 -1
- package/dist/tabs/tabs.js +2 -2
- package/dist/tabs/tabs.js.map +1 -1
- package/dist/tag-input/index.d.ts +2 -0
- package/dist/tag-input/index.js +2 -0
- package/dist/tag-input/tag-input.d.ts +40 -0
- package/dist/tag-input/tag-input.js +269 -0
- package/dist/tag-input/tag-input.js.map +1 -0
- package/dist/text-morph/index.d.ts +2 -0
- package/dist/text-morph/index.js +2 -0
- package/dist/text-morph/text-morph.d.ts +23 -0
- package/dist/text-morph/text-morph.js +21 -0
- package/dist/text-morph/text-morph.js.map +1 -0
- package/dist/text-reveal/index.d.ts +2 -0
- package/dist/text-reveal/index.js +2 -0
- package/dist/text-reveal/text-reveal.d.ts +27 -0
- package/dist/text-reveal/text-reveal.js +51 -0
- package/dist/text-reveal/text-reveal.js.map +1 -0
- package/dist/text-shimmer/index.d.ts +2 -0
- package/dist/text-shimmer/index.js +2 -0
- package/dist/text-shimmer/text-shimmer.d.ts +29 -0
- package/dist/text-shimmer/text-shimmer.js +29 -0
- package/dist/text-shimmer/text-shimmer.js.map +1 -0
- package/dist/time-picker/time-picker.js +2 -2
- package/dist/time-picker/time-picker.js.map +1 -1
- package/dist/toast/index.d.ts +2 -2
- package/dist/toast/index.js +2 -2
- package/dist/toast/toast.d.ts +6 -0
- package/dist/toast/toast.js +82 -40
- package/dist/toast/toast.js.map +1 -1
- package/dist/tooltip/tooltip.d.ts +9 -2
- package/dist/tooltip/tooltip.js +7 -7
- package/dist/tooltip/tooltip.js.map +1 -1
- package/dist/treemap/index.d.ts +2 -0
- package/dist/treemap/index.js +2 -0
- package/dist/treemap/treemap.d.ts +53 -0
- package/dist/treemap/treemap.js +699 -0
- package/dist/treemap/treemap.js.map +1 -0
- package/dist/usage-meter/index.d.ts +2 -0
- package/dist/usage-meter/index.js +2 -0
- package/dist/usage-meter/usage-meter.d.ts +50 -0
- package/dist/usage-meter/usage-meter.js +440 -0
- package/dist/usage-meter/usage-meter.js.map +1 -0
- package/dist/waffle-chart/index.d.ts +2 -0
- package/dist/waffle-chart/index.js +2 -0
- package/dist/waffle-chart/waffle-chart.d.ts +57 -0
- package/dist/waffle-chart/waffle-chart.js +426 -0
- package/dist/waffle-chart/waffle-chart.js.map +1 -0
- package/package.json +165 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shortcut-recorder.js","names":["AriaButton"],"sources":["../../src/shortcut-recorder/shortcut-recorder.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n useSyncExternalStore,\n type HTMLAttributes,\n type KeyboardEvent as ReactKeyboardEvent,\n type ReactNode,\n} from \"react\";\nimport { Button as AriaButton } from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { FieldShell, useFieldWiring } from \"../lib/field-shell.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\nimport {\n formatShortcut,\n keyId,\n modifiersFromEvent,\n normalizeShortcut,\n shortcutFromEvent,\n shortcutTokens,\n splitShortcut,\n type Platform,\n} from \"./shortcut-model.js\";\n\n/*\n * Shortcut keys and a recorder for them. The model (parsing, normalising,\n * platform labels, matching) lives in shortcut-model.ts; this file is the\n * three parts that draw it: a key cap, a row of caps, and the recording field.\n */\n\nconst subscribe = () => () => {};\n\nconst detectPlatform = (): Platform => {\n if (typeof navigator === \"undefined\") return \"mac\";\n const hint =\n (navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData?.platform ??\n navigator.platform ??\n navigator.userAgent;\n return /mac|iphone|ipad|ipod/i.test(hint) ? \"mac\" : \"other\";\n};\n\n/** The viewer's platform. A server render assumes a Mac and settles on the first client render. */\nexport function usePlatform(override?: Platform): Platform {\n const detected = useSyncExternalStore<Platform>(subscribe, detectPlatform, () => \"mac\");\n return override ?? detected;\n}\n\nconst EMPTY: ReadonlySet<string> = new Set();\n\n/**\n * The keys held down right now, by physical id (feed it to `ShortcutKeys` to light the\n * caps as they are pressed). Clears when the window loses focus, and after Command lifts\n * (macOS drops the other keyups).\n */\nexport function usePressedKeys(enabled = true): ReadonlySet<string> {\n const [held, setHeld] = useState<ReadonlySet<string>>(() => new Set());\n useEffect(() => {\n if (!enabled) return;\n const update = (change: (next: Set<string>) => void) =>\n setHeld((current) => {\n const next = new Set(current);\n change(next);\n return next.size === current.size && [...next].every((key) => current.has(key))\n ? current\n : next;\n });\n const down = (event: KeyboardEvent) => {\n const id = keyId(event);\n if (id) update((next) => next.add(id));\n };\n const up = (event: KeyboardEvent) => {\n const id = keyId(event);\n update((next) => {\n if (id) next.delete(id);\n if (id === \"meta\")\n for (const key of [...next])\n if (![\"ctrl\", \"alt\", \"shift\"].includes(key)) next.delete(key);\n if (!event.metaKey) next.delete(\"meta\");\n if (!event.ctrlKey) next.delete(\"ctrl\");\n if (!event.altKey) next.delete(\"alt\");\n if (!event.shiftKey) next.delete(\"shift\");\n });\n };\n const clear = () => setHeld((current) => (current.size ? new Set() : current));\n // Capture phase, so keys still register while a focused recorder keeps the chord from bubbling.\n window.addEventListener(\"keydown\", down, true);\n window.addEventListener(\"keyup\", up, true);\n window.addEventListener(\"blur\", clear);\n document.addEventListener(\"visibilitychange\", clear);\n return () => {\n window.removeEventListener(\"keydown\", down, true);\n window.removeEventListener(\"keyup\", up, true);\n window.removeEventListener(\"blur\", clear);\n document.removeEventListener(\"visibilitychange\", clear);\n };\n }, [enabled]);\n return enabled ? held : EMPTY;\n}\n\n/* ---------------------------------------------- ShortcutKbd and ShortcutKeys ---------------------------------------------- */\n\nexport interface ShortcutKbdProps extends Omit<HTMLAttributes<HTMLElement>, \"className\"> {\n children: ReactNode;\n /** Draws the key pushed down and lit, such as while it is held. */\n pressed?: boolean;\n /** `warning` tints the cap, for a combination that is taken. */\n tone?: \"warning\";\n size?: \"sm\" | \"md\";\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\n/** One key cap. Use it inline in copy, or through `ShortcutKeys` for a whole combination. */\nexport const ShortcutKbd = forwardRef<HTMLElement, ShortcutKbdProps>(function ShortcutKbd(\n { children, pressed = false, tone, size = \"md\", className, ...props },\n ref,\n) {\n return (\n <kbd\n ref={ref}\n {...props}\n data-slot=\"shortcut-kbd\"\n data-size={size}\n data-pressed={pressed || undefined}\n data-tone={tone}\n className={cn(\n cnAll(\n // A one-pixel edge with a heavier bottom reads as a key without a shadow. Held, it drops a pixel and takes the accent.\n \"inline-grid h-6.5 min-w-6.5 place-items-center rounded-sm border-(length:--border-width) border-b-2 border-border-strong bg-surface px-1.75 pb-px font-sans text-xs leading-none font-medium whitespace-nowrap text-foreground tabular-nums\",\n \"data-[size=sm]:h-5.5 data-[size=sm]:min-w-5.5 data-[size=sm]:px-1.25\",\n \"motion-safe:transition-[translate,border-color,background-color,color] motion-safe:duration-(--duration-instant) motion-safe:ease-settle\",\n \"data-[pressed]:translate-y-px data-[pressed]:border-b-(length:--border-width) data-[pressed]:border-primary data-[pressed]:bg-primary-subtle data-[pressed]:text-primary-strong\",\n \"data-[tone=warning]:border-warning-strong data-[tone=warning]:bg-warning-subtle data-[tone=warning]:text-foreground\",\n ),\n className,\n )}\n >\n {children}\n </kbd>\n );\n});\n\nexport interface ShortcutKeysProps {\n /** A shortcut such as `\"mod+shift+k\"`. `mod` is Command on a Mac and Control elsewhere. */\n shortcut: string;\n /** Defaults to the viewer's platform. */\n platform?: Platform;\n /** Held key ids from `usePressedKeys`: the matching caps light up. */\n pressed?: ReadonlySet<string>;\n size?: \"sm\" | \"md\";\n /** Extra classes. Merged last. */\n className?: string;\n}\n\n/** A shortcut as key caps, with one spoken label (\"Command Shift K\") for assistive technology. */\nexport function ShortcutKeys({\n shortcut,\n platform: platformProp,\n pressed,\n size = \"md\",\n className,\n}: ShortcutKeysProps) {\n const platform = usePlatform(platformProp);\n const tokens = shortcutTokens(shortcut, platform);\n return (\n <span\n data-slot=\"shortcut-keys\"\n role=\"img\"\n aria-label={tokens.map((token) => token.spoken).join(\" \")}\n className={cn(\"inline-flex flex-none items-center gap-1\", className)}\n >\n {tokens.map((token, index) => (\n <ShortcutKbd\n key={`${index}-${token.id}`}\n size={size}\n pressed={pressed?.has(token.id)}\n aria-hidden=\"true\"\n >\n {token.label}\n </ShortcutKbd>\n ))}\n </span>\n );\n}\n\n/* ---------------------------------------------- ShortcutRecorder ---------------------------------------------- */\n\nexport interface ShortcutBinding {\n shortcut: string;\n /** What the shortcut does, shown when a recording collides with it. */\n label: string;\n}\n\nexport interface ShortcutRecorderProps {\n /** What the shortcut is for, and the field's accessible name. */\n label: string;\n /** Hides the label visually; it stays the accessible name. */\n hideLabel?: boolean;\n /** The shortcut, such as `\"mod+shift+k\"`, or `null` for none. Controlled when given. */\n value?: string | null;\n defaultValue?: string | null;\n /** `replaced` is the binding a shortcut was taken from after \"Use anyway\", so the caller can unbind it. */\n onValueChange?: (value: string | null, details: { replaced?: ShortcutBinding }) => void;\n /** What the reset button restores. Defaults to `defaultValue`. */\n resetValue?: string | null;\n /** Shortcuts already in use. Recording one of them asks before taking it. */\n bindings?: ShortcutBinding[];\n /** Also warn about combinations a browser or system keeps, such as Command W and Command C. @default true */\n warnReserved?: boolean;\n /** Require Command, Control or Option (function keys are always allowed), so a bare letter cannot be recorded. @default true */\n requireModifier?: boolean;\n /** Defaults to the viewer's platform. */\n platform?: Platform;\n /** Shown while there is no shortcut. @default \"Record shortcut\" */\n placeholder?: string;\n /** Helper text under the field. */\n description?: string;\n disabled?: boolean;\n id?: string;\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\nconst RESERVED: ShortcutBinding[] = [\n { shortcut: \"mod+w\", label: \"Close tab\" },\n { shortcut: \"mod+t\", label: \"New tab\" },\n { shortcut: \"mod+n\", label: \"New window\" },\n { shortcut: \"mod+q\", label: \"Quit\" },\n { shortcut: \"mod+l\", label: \"Address bar\" },\n { shortcut: \"mod+r\", label: \"Reload\" },\n { shortcut: \"mod+shift+t\", label: \"Reopen tab\" },\n { shortcut: \"mod+c\", label: \"Copy\" },\n { shortcut: \"mod+v\", label: \"Paste\" },\n { shortcut: \"mod+x\", label: \"Cut\" },\n { shortcut: \"mod+z\", label: \"Undo\" },\n { shortcut: \"mod+a\", label: \"Select all\" },\n];\n\ninterface Pending {\n shortcut: string;\n conflict: ShortcutBinding;\n reserved: boolean;\n}\n\n/** A no-break space, appended to a repeated announcement so a screen reader says it again. */\nconst NBSP = String.fromCharCode(160);\n\n/** Stagger between the caps of a finished shortcut as they settle in (ms). */\nconst CAP_STAGGER_MS = 35;\n\n/** How long the caps stay lit after a shortcut is set (ms). */\nconst SETTLE_MS = 700;\n\nconst controlClass = cnAll(\n fieldSurfaceVariants(),\n \"relative flex h-(--density-control-h-md) items-center\",\n \"data-[state=idle]:hover:border-border-strong\",\n \"data-[state=recording]:border-primary data-[state=recording]:bg-primary-subtle\",\n \"data-[state=conflict]:border-warning-strong\",\n // The recorder is a button, so the control shows the ring whenever it has keyboard focus.\n \"has-[[data-slot=shortcut-recorder-button]:focus-visible]:border-border-strong has-[[data-slot=shortcut-recorder-button]:focus-visible]:outline-solid has-[[data-slot=shortcut-recorder-button]:focus-visible]:outline-(length:--focus-ring-width) has-[[data-slot=shortcut-recorder-button]:focus-visible]:outline-offset-(--focus-ring-offset) has-[[data-slot=shortcut-recorder-button]:focus-visible]:outline-(--color-ring)\",\n);\n\nconst actionClass =\n \"grid size-8 flex-none cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-text-secondary outline-none motion-safe:transition-[background-color,color,scale] motion-safe:duration-(--duration-quick) motion-safe:ease-settle data-[hovered]:bg-muted data-[hovered]:text-foreground data-[pressed]:scale-95 data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width) data-[focus-visible]:outline-(--color-ring) [&_svg]:pointer-events-none [&_svg]:size-4\";\n\nconst linkClass =\n \"inline-flex min-h-6 cursor-pointer items-center rounded-xs border-0 bg-transparent px-1 font-sans text-xs font-medium text-foreground underline decoration-border-strong underline-offset-3 outline-none motion-safe:transition-[text-decoration-color,color] motion-safe:duration-(--duration-quick) data-[hovered]:decoration-current data-[focus-visible]:decoration-current data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width) data-[focus-visible]:outline-(--color-ring)\";\n\n/** A small icon button inside the field. One that appears after mount scales in; one there at first render does not. */\nfunction FieldAction({\n label,\n arrives,\n onPress,\n children,\n}: {\n label: string;\n arrives: boolean;\n onPress: () => void;\n children: ReactNode;\n}) {\n const [animated] = useState(arrives);\n return (\n <AriaButton\n aria-label={label}\n onPress={onPress}\n data-slot=\"shortcut-recorder-action\"\n className={cn(\n actionClass,\n animated && \"motion-safe:animate-field-clear-in motion-reduce:animate-fade-in\",\n )}\n >\n {children}\n </AriaButton>\n );\n}\n\n/** One cap of the row. Decided once, at its own mount: caps there at first render stay still. */\nfunction Cap({\n arrives,\n delay,\n children,\n}: {\n arrives: boolean;\n delay: number;\n children: ReactNode;\n}) {\n const [animated] = useState(arrives);\n return (\n <span\n className={cn(\n \"inline-flex\",\n animated && \"motion-safe:animate-chip-reveal motion-reduce:animate-fade-in\",\n )}\n style={animated && delay ? { animationDelay: `${delay}ms` } : undefined}\n >\n {children}\n </span>\n );\n}\n\n/**\n * A field that records a keyboard shortcut. Press it (or focus it and press Enter or\n * Space), then press the combination: held modifiers show as caps while you build the\n * chord, the finished shortcut settles into place and is announced, and a combination\n * that is already taken asks before it is used.\n *\n * Keyboard: Escape cancels, Backspace or Delete clears, and Tab leaves the field\n * (it is never swallowed, so recording cannot trap focus). Escape and Tab can still be\n * recorded with a modifier held. Outside recording, Backspace or Delete clears the\n * shortcut, and the reset button restores the default. Caps follow the platform:\n * Command symbols on a Mac, Ctrl and Alt elsewhere.\n *\n * While recording, the chord belongs to the recorder: its keys are kept from the page's\n * own shortcuts. Nothing animates on first render.\n *\n * @example\n * <ShortcutRecorder label=\"Open command menu\" defaultValue=\"mod+k\" onValueChange={save} />\n */\nexport function ShortcutRecorder({\n label,\n hideLabel = false,\n value,\n defaultValue = null,\n onValueChange,\n resetValue,\n bindings = [],\n warnReserved = true,\n requireModifier = true,\n platform: platformProp,\n placeholder = \"Record shortcut\",\n description,\n disabled = false,\n id,\n className,\n}: ShortcutRecorderProps) {\n const platform = usePlatform(platformProp);\n const conflictId = useId();\n\n const [inner, setInner] = useState<string | null>(defaultValue);\n const current = value !== undefined ? value : inner;\n const restoreTo = resetValue !== undefined ? resetValue : defaultValue;\n\n const [recording, setRecording] = useState(false);\n const [live, setLive] = useState<string[]>([]);\n const [pending, setPending] = useState<Pending | null>(null);\n const [nudge, setNudge] = useState<string | null>(null);\n const [settled, setSettled] = useState(0);\n const [announcement, setAnnouncement] = useState(\"\");\n const buttonRef = useRef<HTMLButtonElement>(null);\n const swallowClick = useRef(false);\n\n // Caps and buttons that mount from the next frame on arrive animated.\n const [ready, setReady] = useState(false);\n useEffect(() => {\n const frame = requestAnimationFrame(() => setReady(true));\n return () => cancelAnimationFrame(frame);\n }, []);\n\n const say = (message: string) =>\n setAnnouncement((previous) => (previous === message ? message + NBSP : message));\n const spoken = (shortcut: string | null) =>\n shortcut ? formatShortcut(shortcut, platform).spoken : \"none\";\n\n const commit = useCallback(\n (next: string | null, replaced?: ShortcutBinding) => {\n setPending(null);\n setNudge(null);\n if (value === undefined) setInner(next);\n onValueChange?.(next, { replaced });\n setSettled((count) => count + 1);\n say(next ? `Shortcut set to ${formatShortcut(next, platform).spoken}` : \"Shortcut cleared\");\n },\n [onValueChange, platform, value],\n );\n\n useEffect(() => {\n if (!settled) return;\n const timer = window.setTimeout(() => setSettled(0), SETTLE_MS);\n return () => window.clearTimeout(timer);\n }, [settled]);\n\n const start = () => {\n if (disabled) return;\n setPending(null);\n setNudge(null);\n setLive([]);\n setRecording(true);\n say(\"Recording. Press the new shortcut, or Escape to cancel.\");\n };\n const stop = () => {\n setRecording(false);\n setLive([]);\n };\n\n const conflictFor = (shortcut: string): Pending | null => {\n const normal = normalizeShortcut(shortcut, platform);\n if (current && normalizeShortcut(current, platform) === normal) return null;\n const taken = bindings.find(\n (binding) => normalizeShortcut(binding.shortcut, platform) === normal,\n );\n if (taken) return { shortcut, conflict: taken, reserved: false };\n const reserved = warnReserved\n ? RESERVED.find((binding) => normalizeShortcut(binding.shortcut, platform) === normal)\n : undefined;\n return reserved ? { shortcut, conflict: reserved, reserved: true } : null;\n };\n\n function onKeyDown(event: ReactKeyboardEvent<HTMLButtonElement>) {\n const bare = !event.metaKey && !event.ctrlKey && !event.altKey;\n if (!recording) {\n if (\n bare &&\n !event.shiftKey &&\n (event.key === \"Backspace\" || event.key === \"Delete\") &&\n current\n ) {\n event.preventDefault();\n commit(null);\n } else if (bare && !event.shiftKey && event.key === \"Escape\" && pending) {\n event.preventDefault();\n setPending(null);\n say(\"Kept the previous shortcut\");\n }\n return;\n }\n // Tab (with or without Shift) is how a keyboard user leaves: it stops recording and moves focus on.\n if (event.key === \"Tab\" && bare) {\n stop();\n return;\n }\n // The chord belongs to the recorder: keep it from the page's own shortcuts, such as a command menu on Command K.\n event.preventDefault();\n event.stopPropagation();\n if (event.key === \" \") swallowClick.current = true;\n const noMods = bare && !event.shiftKey;\n if (noMods && event.key === \"Escape\") {\n stop();\n say(\"Recording canceled\");\n return;\n }\n if (noMods && (event.key === \"Backspace\" || event.key === \"Delete\")) {\n stop();\n commit(null);\n return;\n }\n const shortcut = shortcutFromEvent(event.nativeEvent, platform);\n if (!shortcut) {\n setLive(modifiersFromEvent(event, platform));\n return;\n }\n const { mods, key } = splitShortcut(shortcut);\n const strong = mods.has(\"mod\") || mods.has(\"ctrl\") || mods.has(\"alt\") || mods.has(\"meta\");\n if (requireModifier && !strong && !/^f\\d+$/.test(key)) {\n const hint = platform === \"mac\" ? \"Include ⌘, ⌃, or ⌥\" : \"Include Ctrl or Alt\";\n setNudge(hint);\n say(hint);\n setLive(modifiersFromEvent(event, platform));\n return;\n }\n stop();\n const conflict = conflictFor(shortcut);\n if (conflict) {\n setPending(conflict);\n say(\n `${formatShortcut(shortcut, platform).spoken} is ${conflict.reserved ? \"reserved for\" : \"used by\"} ${conflict.conflict.label}. Use anyway, or record another.`,\n );\n return;\n }\n commit(shortcut);\n }\n\n function onKeyUp(event: ReactKeyboardEvent<HTMLButtonElement>) {\n if (recording) setLive(modifiersFromEvent(event, platform));\n // A Space the recorder took must not click the button when it comes back up.\n if (swallowClick.current) setTimeout(() => (swallowClick.current = false), 0);\n }\n\n const shown = pending?.shortcut ?? current;\n const tokens = recording\n ? shortcutTokens(live.join(\"+\"), platform)\n : shown\n ? shortcutTokens(shown, platform)\n : [];\n const state = recording ? \"recording\" : pending ? \"conflict\" : \"idle\";\n const canReset = !recording && (pending !== null || (current ?? null) !== (restoreTo ?? null));\n const canClear = !recording && !!current && !pending;\n const hint = pending\n ? undefined\n : (nudge ?? (recording ? \"Esc to cancel, Backspace to clear\" : description));\n\n const field = useFieldWiring({\n enabled: true,\n id,\n describedBy: undefined,\n invalid: undefined,\n description: hint,\n error: undefined,\n });\n const describedBy = [pending ? conflictId : undefined, field.hintId].filter(Boolean).join(\" \");\n\n return (\n <div data-slot=\"shortcut-recorder\" className={cn(\"w-full min-w-0 font-sans\", className)}>\n <FieldShell\n label={hideLabel ? null : label}\n controlId={field.controlId}\n description={hint}\n hintId={field.hintId}\n error={undefined}\n errorId={undefined}\n numbers={false}\n >\n <div\n data-slot=\"shortcut-recorder-control\"\n data-state={state}\n data-disabled={disabled || undefined}\n className={controlClass}\n >\n <button\n ref={buttonRef}\n id={field.controlId}\n aria-describedby={describedBy || undefined}\n type=\"button\"\n data-slot=\"shortcut-recorder-button\"\n disabled={disabled}\n aria-label={`${label}, ${recording ? \"recording\" : spoken(shown)}`}\n aria-pressed={recording}\n onClick={(event) => {\n if (swallowClick.current) return;\n if (recording) {\n // A pointer click cancels; a keyboard click cannot reach here, as the keydown was taken.\n if (event.detail !== 0) {\n stop();\n say(\"Recording canceled\");\n }\n return;\n }\n start();\n }}\n onKeyDown={onKeyDown}\n onKeyUp={onKeyUp}\n onBlur={() => {\n if (recording) {\n stop();\n setNudge(null);\n }\n }}\n className=\"flex h-full min-w-0 flex-1 cursor-pointer touch-manipulation items-center rounded-control border-0 bg-transparent ps-2.5 pe-1.5 text-start font-sans text-foreground outline-none disabled:cursor-not-allowed\"\n >\n <span aria-hidden=\"true\" className=\"relative flex min-w-0 items-center gap-1\">\n {recording ? (\n <span className=\"me-1.5 ms-0.75 size-1.75 flex-none rounded-full bg-primary motion-safe:animate-shortcut-recording\" />\n ) : null}\n {tokens.map((token, index) => (\n <Cap key={token.id} arrives={ready} delay={recording ? 0 : index * CAP_STAGGER_MS}>\n <ShortcutKbd\n pressed={recording || (settled > 0 && !pending)}\n tone={pending ? \"warning\" : undefined}\n >\n {token.label}\n </ShortcutKbd>\n </Cap>\n ))}\n {tokens.length === 0 ? (\n <span\n key={recording ? \"prompt\" : \"placeholder\"}\n className={cnAll(\n \"ps-0.75 text-sm whitespace-nowrap\",\n recording ? \"text-text-secondary\" : \"text-muted-foreground\",\n ready && \"motion-safe:animate-field-word-in motion-reduce:animate-fade-in\",\n )}\n >\n {recording ? \"Press keys\" : placeholder}\n </span>\n ) : null}\n </span>\n </button>\n <span className=\"flex flex-none items-center gap-0.5 pe-1\">\n {canReset ? (\n <FieldAction\n key=\"reset\"\n arrives={ready}\n label={`Reset to ${spoken(restoreTo ?? null)}`}\n onPress={() => {\n commit(restoreTo ?? null);\n buttonRef.current?.focus();\n }}\n >\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M3 12a9 9 0 1 0 3-6.7L3 8\" />\n <path d=\"M3 3v5h5\" />\n </svg>\n </FieldAction>\n ) : null}\n {canClear ? (\n <FieldAction\n key=\"clear\"\n arrives={ready}\n label=\"Clear shortcut\"\n onPress={() => {\n commit(null);\n buttonRef.current?.focus();\n }}\n >\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </FieldAction>\n ) : null}\n </span>\n </div>\n {/* The conflict: its copy settles in; it goes away at once, as its buttons must not stay reachable while it fades. */}\n {pending ? (\n <div\n id={conflictId}\n data-slot=\"shortcut-recorder-conflict\"\n className=\"flex flex-wrap items-baseline justify-between gap-x-3 pt-1.5 font-sans text-xs leading-(--leading-normal) text-text-secondary motion-safe:animate-field-word-in motion-reduce:animate-fade-in\"\n >\n <span>\n <span\n aria-hidden=\"true\"\n className=\"me-1.5 ms-px mb-px inline-block size-1.5 rounded-full bg-warning-strong align-middle\"\n />\n {pending.reserved ? \"Reserved for\" : \"Already used by\"}{\" \"}\n <strong className=\"font-medium text-foreground\">{pending.conflict.label}</strong>\n </span>\n <span className=\"inline-flex gap-2\">\n <AriaButton\n className={linkClass}\n onPress={() => {\n commit(pending.shortcut, pending.reserved ? undefined : pending.conflict);\n buttonRef.current?.focus();\n }}\n >\n Use anyway\n </AriaButton>\n <AriaButton\n className={cn(linkClass, \"font-normal text-text-secondary no-underline\")}\n onPress={() => {\n setPending(null);\n say(\"Kept the previous shortcut\");\n buttonRef.current?.focus();\n }}\n >\n Cancel\n </AriaButton>\n </span>\n </div>\n ) : null}\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {announcement}\n </span>\n </FieldShell>\n </div>\n );\n}\n"],"mappings":"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"}
|
package/dist/slider/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Slider, SliderProps, sliderBarVariants, sliderFillVariants, sliderThumbVariants, sliderTrackVariants, sliderVariants } from "./slider.js";
|
|
2
|
-
export { Slider, type SliderProps, sliderBarVariants, sliderFillVariants, sliderThumbVariants, sliderTrackVariants, sliderVariants };
|
|
1
|
+
import { Slider, SliderMark, SliderProps, SliderValue, sliderBarVariants, sliderFillVariants, sliderThumbVariants, sliderTrackVariants, sliderVariants } from "./slider.js";
|
|
2
|
+
export { Slider, type SliderMark, type SliderProps, type SliderValue, sliderBarVariants, sliderFillVariants, sliderThumbVariants, sliderTrackVariants, sliderVariants };
|
package/dist/slider/slider.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReactNode } from "react";
|
|
1
|
+
import { ReactElement, ReactNode, RefAttributes } from "react";
|
|
2
2
|
import { SliderProps } from "react-aria-components";
|
|
3
3
|
//#region src/slider/slider.d.ts
|
|
4
4
|
/**
|
|
@@ -12,18 +12,21 @@ import { SliderProps } from "react-aria-components";
|
|
|
12
12
|
*
|
|
13
13
|
* `label` IS REQUIRED here, the same answer Progress and Dialog give.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `border` track is 1.01:1. The two halves would be the same grey and the
|
|
21
|
-
* value would be invisible to anyone not reading the thumb position.
|
|
15
|
+
* LOOK (UX owner's colours): a thin, rounded track in primary 200 (light
|
|
16
|
+
* #FEEAE0, dark #441B00, role `slider-track`, no outline) with a fill in
|
|
17
|
+
* `primary-strong` (the dark orange, so it reads against the pale track) and a
|
|
18
|
+
* solid light thumb (`slider-thumb`) with a soft shadow. The label sits left
|
|
19
|
+
* and the value right. The fill's LENGTH is the value.
|
|
22
20
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
21
|
+
* Contrast, from contrast-pairs.json (build-failing): fill against the track
|
|
22
|
+
* 4.9:1 light / 8.3:1 dark; thumb against what it meets at its edge (the fill in
|
|
23
|
+
* light, a 2px ring in the page colour in dark -- see `shadow.slider-thumb`)
|
|
24
|
+
* 5.7:1 / 18.6:1. A near-white thumb on a light fill (dark) would otherwise
|
|
25
|
+
* melt into it, and the old dark thumb (the card colour) all but vanished on the
|
|
26
|
+
* page. The pale track is 1.12:1 / 1.3:1 on the page, under the 3:1 non-text
|
|
27
|
+
* floor, by the same accepted trade-off as `--border`; the state is the EDGE OF
|
|
28
|
+
* THE FILL (>= 4.9:1) plus the value text beside the label. The thumb is a 24px
|
|
29
|
+
* target and rests on the track with a shadow.
|
|
27
30
|
*
|
|
28
31
|
* Three legacy defects go with it: the focus ring was `ring-1`, a 1px
|
|
29
32
|
* box-shadow that forced-colors discards entirely; the thumb border was
|
|
@@ -37,14 +40,22 @@ export declare const sliderTrackVariants: (props?: import("class-variance-author
|
|
|
37
40
|
export declare const sliderBarVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
38
41
|
export declare const sliderFillVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
39
42
|
export declare const sliderThumbVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
43
|
+
/** One thumb is a number; two thumbs are a `[low, high]` pair. */
|
|
44
|
+
export type SliderValue = number | [number, number];
|
|
45
|
+
/** A tick on the track, with an optional label printed under it. */
|
|
46
|
+
export interface SliderMark {
|
|
47
|
+
value: number;
|
|
48
|
+
label?: ReactNode;
|
|
49
|
+
}
|
|
40
50
|
/**
|
|
41
51
|
* A value along a range.
|
|
42
52
|
*
|
|
43
53
|
* @example
|
|
44
54
|
* <Slider label="Input volume" defaultValue={40} />
|
|
45
55
|
* <Slider label="Playback position" value={t} onChange={seek} formatOptions={{ style: "percent" }} />
|
|
56
|
+
* <Slider label="Budget" defaultValue={[2200, 8000]} minValue={0} maxValue={10000} step={100} />
|
|
46
57
|
*/
|
|
47
|
-
interface SliderProps$1 extends Omit<SliderProps<number>, "className" | "style" | "children"> {
|
|
58
|
+
interface SliderProps$1<T extends SliderValue = number> extends Omit<SliderProps<number | number[]>, "className" | "style" | "children" | "value" | "defaultValue" | "onChange" | "onChangeEnd"> {
|
|
48
59
|
/**
|
|
49
60
|
* What the slider controls, and its accessible name.
|
|
50
61
|
*
|
|
@@ -61,10 +72,31 @@ interface SliderProps$1 extends Omit<SliderProps<number>, "className" | "style"
|
|
|
61
72
|
* distinguishable only by the thumb.
|
|
62
73
|
*/
|
|
63
74
|
showValue?: boolean;
|
|
75
|
+
/** A number for one thumb, a `[low, high]` pair for a range with two thumbs. */
|
|
76
|
+
value?: T;
|
|
77
|
+
/** The uncontrolled value; its shape (a number or a pair) also picks one or two thumbs. */
|
|
78
|
+
defaultValue?: T;
|
|
79
|
+
onChange?: (value: T) => void;
|
|
80
|
+
/** Runs once a drag is released or a key changes the value, for work too heavy for every step. */
|
|
81
|
+
onChangeEnd?: (value: T) => void;
|
|
82
|
+
/** Grey helper text under the control. It also describes the slider to a screen reader. */
|
|
83
|
+
description?: ReactNode;
|
|
84
|
+
/**
|
|
85
|
+
* Tick dots on the track. A number draws a tick; a mark with a `label` also
|
|
86
|
+
* prints it under the track. Marks are decoration: the slider still reads out
|
|
87
|
+
* its own value, so pair them with `step` to make the ticks reachable.
|
|
88
|
+
*/
|
|
89
|
+
marks?: (number | SliderMark)[];
|
|
90
|
+
/** Accessible names for the two range thumbs. @default ["Minimum", "Maximum"] */
|
|
91
|
+
thumbLabels?: [string, string];
|
|
92
|
+
/** Content before the track, such as an icon button that mutes. */
|
|
93
|
+
start?: ReactNode;
|
|
94
|
+
/** Content after the track. */
|
|
95
|
+
end?: ReactNode;
|
|
64
96
|
/** Extra classes. Merged last, so they win conflicts with ours. */
|
|
65
97
|
className?: string;
|
|
66
98
|
}
|
|
67
|
-
declare const Slider$1:
|
|
99
|
+
declare const Slider$1: <T extends SliderValue = number>(props: SliderProps$1<T> & RefAttributes<HTMLDivElement>) => ReactElement | null;
|
|
68
100
|
//#endregion
|
|
69
101
|
export { Slider$1 as Slider, SliderProps$1 as SliderProps };
|
|
70
102
|
//# sourceMappingURL=slider.d.ts.map
|
package/dist/slider/slider.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { cn, cnAll } from "../lib/cn.js";
|
|
3
3
|
import { SwapText } from "../lib/swap-text.js";
|
|
4
4
|
import { cva } from "class-variance-authority";
|
|
5
|
-
import { forwardRef, useState } from "react";
|
|
6
|
-
import { Label, Slider, SliderOutput, SliderThumb, SliderTrack } from "react-aria-components";
|
|
5
|
+
import { Fragment, forwardRef, useContext, useId, useState } from "react";
|
|
6
|
+
import { Label, Slider, SliderOutput, SliderStateContext, SliderThumb, SliderTrack } from "react-aria-components";
|
|
7
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
//#region src/slider/slider.tsx
|
|
9
9
|
/**
|
|
@@ -17,18 +17,21 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
|
17
17
|
*
|
|
18
18
|
* `label` IS REQUIRED here, the same answer Progress and Dialog give.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* `border` track is 1.01:1. The two halves would be the same grey and the
|
|
26
|
-
* value would be invisible to anyone not reading the thumb position.
|
|
20
|
+
* LOOK (UX owner's colours): a thin, rounded track in primary 200 (light
|
|
21
|
+
* #FEEAE0, dark #441B00, role `slider-track`, no outline) with a fill in
|
|
22
|
+
* `primary-strong` (the dark orange, so it reads against the pale track) and a
|
|
23
|
+
* solid light thumb (`slider-thumb`) with a soft shadow. The label sits left
|
|
24
|
+
* and the value right. The fill's LENGTH is the value.
|
|
27
25
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* Contrast, from contrast-pairs.json (build-failing): fill against the track
|
|
27
|
+
* 4.9:1 light / 8.3:1 dark; thumb against what it meets at its edge (the fill in
|
|
28
|
+
* light, a 2px ring in the page colour in dark -- see `shadow.slider-thumb`)
|
|
29
|
+
* 5.7:1 / 18.6:1. A near-white thumb on a light fill (dark) would otherwise
|
|
30
|
+
* melt into it, and the old dark thumb (the card colour) all but vanished on the
|
|
31
|
+
* page. The pale track is 1.12:1 / 1.3:1 on the page, under the 3:1 non-text
|
|
32
|
+
* floor, by the same accepted trade-off as `--border`; the state is the EDGE OF
|
|
33
|
+
* THE FILL (>= 4.9:1) plus the value text beside the label. The thumb is a 24px
|
|
34
|
+
* target and rests on the track with a shadow.
|
|
32
35
|
*
|
|
33
36
|
* Three legacy defects go with it: the focus ring was `ring-1`, a 1px
|
|
34
37
|
* box-shadow that forced-colors discards entirely; the thumb border was
|
|
@@ -37,17 +40,18 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
|
37
40
|
* form elements -- Radix renders a `<span role="slider">`, so a disabled
|
|
38
41
|
* slider looked exactly like an enabled one.
|
|
39
42
|
*/
|
|
40
|
-
const sliderVariants = cva(["flex w-full flex-col gap-2 box-border font-sans"]);
|
|
43
|
+
const sliderVariants = cva(["flex w-full flex-col gap-2 box-border font-sans", "data-[disabled]:opacity-(--opacity-disabled)"]);
|
|
41
44
|
const sliderTrackVariants = cva([
|
|
42
|
-
"relative flex
|
|
45
|
+
"relative mx-3 flex items-center",
|
|
43
46
|
"h-6",
|
|
44
47
|
"slider-track"
|
|
45
48
|
]);
|
|
46
|
-
const sliderBarVariants = cva(["absolute h-
|
|
47
|
-
const sliderFillVariants = cva(["absolute h-
|
|
49
|
+
const sliderBarVariants = cva(["absolute h-1.5 w-full rounded-pill bg-slider-track"]);
|
|
50
|
+
const sliderFillVariants = cva(["absolute h-1.5 rounded-pill bg-primary-strong", "slider-fill"]);
|
|
48
51
|
const sliderThumbVariants = cva([
|
|
49
|
-
"size-6 rounded-
|
|
50
|
-
"
|
|
52
|
+
"size-6 rounded-circle box-border",
|
|
53
|
+
"top-1/2",
|
|
54
|
+
"bg-slider-thumb",
|
|
51
55
|
"cursor-grab data-[dragging]:cursor-grabbing",
|
|
52
56
|
"slider-thumb",
|
|
53
57
|
"outline-none",
|
|
@@ -57,6 +61,10 @@ const sliderThumbVariants = cva([
|
|
|
57
61
|
"data-[focus-visible]:outline-(--color-ring)",
|
|
58
62
|
"data-[disabled]:cursor-not-allowed"
|
|
59
63
|
]);
|
|
64
|
+
/** A tick dot on the track: dim on the unfilled side, light where the fill passes under it. */
|
|
65
|
+
const sliderTickClasses = "pointer-events-none absolute top-1/2 size-1 -translate-1/2 rounded-pill bg-foreground/30 data-[on]:bg-background/60 motion-safe:transition-colors motion-safe:duration-(--duration-quick)";
|
|
66
|
+
/** A step label under its tick. */
|
|
67
|
+
const sliderMarkLabelClasses = "slider-mark-label absolute top-0 whitespace-nowrap text-xs leading-(--leading-normal) text-text-secondary tabular-nums data-[on]:text-foreground motion-safe:transition-colors motion-safe:duration-(--duration-quick)";
|
|
60
68
|
/**
|
|
61
69
|
* Text that rolls when its value changes: a larger value rises in from below, a
|
|
62
70
|
* smaller one drops in from above. The caller's own text stays for a screen
|
|
@@ -86,60 +94,136 @@ function RollingText({ value, text }) {
|
|
|
86
94
|
})
|
|
87
95
|
})] });
|
|
88
96
|
}
|
|
89
|
-
const
|
|
97
|
+
const RANGE_DASH = " – ";
|
|
98
|
+
/** Step labels under the track, each centred on its tick (the end ones hold inside the component). */
|
|
99
|
+
function SliderMarkLabels({ marks, percentOf, minValue, maxValue }) {
|
|
100
|
+
const values = useContext(SliderStateContext)?.values ?? [];
|
|
101
|
+
const isOn = (v) => values.length > 1 ? v >= values[0] && v <= values[values.length - 1] : v <= (values[0] ?? minValue);
|
|
102
|
+
return /* @__PURE__ */ jsx("div", {
|
|
103
|
+
"aria-hidden": "true",
|
|
104
|
+
"data-slot": "slider-marks",
|
|
105
|
+
className: "relative col-start-2 row-start-2 mx-3 mt-1 h-5",
|
|
106
|
+
children: marks.map((mark) => mark.label == null ? null : /* @__PURE__ */ jsx("span", {
|
|
107
|
+
"data-slot": "slider-mark-label",
|
|
108
|
+
"data-on": isOn(mark.value) || void 0,
|
|
109
|
+
"data-edge": mark.value <= minValue ? "start" : mark.value >= maxValue ? "end" : void 0,
|
|
110
|
+
className: sliderMarkLabelClasses,
|
|
111
|
+
style: { left: `${percentOf(mark.value)}%` },
|
|
112
|
+
children: mark.label
|
|
113
|
+
}, mark.value))
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const Slider$1 = forwardRef(function Slider$2({ label, labelHidden, showValue = true, description, marks, thumbLabels = ["Minimum", "Maximum"], start, end, className, "aria-describedby": describedByProp, ...props }, ref) {
|
|
117
|
+
const descriptionId = useId();
|
|
118
|
+
const describedBy = [describedByProp, description == null ? null : descriptionId].filter(Boolean).join(" ");
|
|
119
|
+
const minValue = props.minValue ?? 0;
|
|
120
|
+
const maxValue = props.maxValue ?? 100;
|
|
121
|
+
const markList = (marks ?? []).map((mark) => typeof mark === "number" ? { value: mark } : mark);
|
|
122
|
+
const percentOf = (v) => maxValue === minValue ? 0 : (v - minValue) / (maxValue - minValue) * 100;
|
|
90
123
|
return /* @__PURE__ */ jsxs(Slider, {
|
|
91
124
|
ref,
|
|
92
125
|
...props,
|
|
93
126
|
"data-slot": "slider",
|
|
94
127
|
className: cn(sliderVariants(), className),
|
|
95
|
-
children: [
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
text: state.getThumbValueLabel(0)
|
|
107
|
-
})
|
|
108
|
-
}) : null]
|
|
109
|
-
}), /* @__PURE__ */ jsx(SliderTrack, {
|
|
110
|
-
"data-slot": "slider-track",
|
|
111
|
-
className: sliderTrackVariants(),
|
|
112
|
-
children: ({ state }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
113
|
-
/* @__PURE__ */ jsx("div", {
|
|
114
|
-
"data-slot": "slider-bar",
|
|
115
|
-
className: sliderBarVariants()
|
|
116
|
-
}),
|
|
117
|
-
/* @__PURE__ */ jsx("div", {
|
|
118
|
-
"data-slot": "slider-fill",
|
|
119
|
-
className: sliderFillVariants(),
|
|
120
|
-
"data-dragging": state.isThumbDragging(0) || void 0,
|
|
121
|
-
style: { width: `${state.getThumbPercent(0) * 100}%` }
|
|
122
|
-
}),
|
|
123
|
-
/* @__PURE__ */ jsx(SliderThumb, {
|
|
124
|
-
"data-slot": "slider-thumb",
|
|
125
|
-
className: sliderThumbVariants(),
|
|
126
|
-
children: ({ isDragging, isFocusVisible, state: thumbState }) => /* @__PURE__ */ jsx("span", {
|
|
128
|
+
children: [
|
|
129
|
+
/* @__PURE__ */ jsxs("div", {
|
|
130
|
+
className: cnAll("flex items-baseline justify-between gap-4", labelHidden && "sr-only"),
|
|
131
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
132
|
+
"data-slot": "slider-label",
|
|
133
|
+
className: "text-sm leading-(--leading-normal) font-medium text-foreground",
|
|
134
|
+
children: label
|
|
135
|
+
}), showValue ? /* @__PURE__ */ jsx(SliderOutput, {
|
|
136
|
+
"data-slot": "slider-output",
|
|
137
|
+
className: "text-sm leading-(--leading-normal) whitespace-nowrap text-text-secondary tabular-nums",
|
|
138
|
+
children: ({ state }) => state.values.map((_, i) => /* @__PURE__ */ jsxs(Fragment, { children: [i > 0 ? /* @__PURE__ */ jsx("span", {
|
|
127
139
|
"aria-hidden": "true",
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
children: RANGE_DASH
|
|
141
|
+
}) : null, /* @__PURE__ */ jsx(RollingText, {
|
|
142
|
+
value: state.getThumbValue(i),
|
|
143
|
+
text: state.getThumbValueLabel(i)
|
|
144
|
+
})] }, i))
|
|
145
|
+
}) : null]
|
|
146
|
+
}),
|
|
147
|
+
/* @__PURE__ */ jsxs("div", {
|
|
148
|
+
className: "grid grid-cols-[auto_minmax(0,1fr)_auto] items-center",
|
|
149
|
+
children: [
|
|
150
|
+
start ? /* @__PURE__ */ jsx("div", {
|
|
151
|
+
className: "col-start-1 row-start-1 me-3 flex",
|
|
152
|
+
children: start
|
|
153
|
+
}) : null,
|
|
154
|
+
/* @__PURE__ */ jsx(SliderTrack, {
|
|
155
|
+
"data-slot": "slider-track",
|
|
156
|
+
className: cnAll(sliderTrackVariants(), "col-start-2 row-start-1"),
|
|
157
|
+
children: ({ state }) => {
|
|
158
|
+
const count = state.values.length;
|
|
159
|
+
const low = count > 1 ? state.getThumbPercent(0) * 100 : 0;
|
|
160
|
+
const high = state.getThumbPercent(count - 1) * 100;
|
|
161
|
+
const dragging = state.values.some((_, i) => state.isThumbDragging(i));
|
|
162
|
+
const isOn = (v) => count > 1 ? v >= state.values[0] && v <= state.values[count - 1] : v <= state.values[0];
|
|
163
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
164
|
+
/* @__PURE__ */ jsx("div", {
|
|
165
|
+
"data-slot": "slider-bar",
|
|
166
|
+
className: sliderBarVariants()
|
|
167
|
+
}),
|
|
168
|
+
/* @__PURE__ */ jsx("div", {
|
|
169
|
+
"data-slot": "slider-fill",
|
|
170
|
+
className: sliderFillVariants(),
|
|
171
|
+
"data-dragging": dragging || void 0,
|
|
172
|
+
style: {
|
|
173
|
+
left: `${low}%`,
|
|
174
|
+
width: `${high - low}%`
|
|
175
|
+
}
|
|
176
|
+
}),
|
|
177
|
+
markList.map((mark) => /* @__PURE__ */ jsx("span", {
|
|
178
|
+
"aria-hidden": "true",
|
|
179
|
+
"data-slot": "slider-tick",
|
|
180
|
+
"data-on": isOn(mark.value) || void 0,
|
|
181
|
+
className: sliderTickClasses,
|
|
182
|
+
style: { left: `${percentOf(mark.value)}%` }
|
|
183
|
+
}, mark.value)),
|
|
184
|
+
state.values.map((_, i) => /* @__PURE__ */ jsx(SliderThumb, {
|
|
185
|
+
index: i,
|
|
186
|
+
"aria-label": count > 1 ? thumbLabels[i] : void 0,
|
|
187
|
+
"aria-describedby": describedBy || void 0,
|
|
188
|
+
"data-slot": "slider-thumb",
|
|
189
|
+
className: sliderThumbVariants(),
|
|
190
|
+
children: ({ isDragging, isFocusVisible, state: thumbState }) => /* @__PURE__ */ jsx("span", {
|
|
191
|
+
"aria-hidden": "true",
|
|
192
|
+
className: "slider-bubble-anchor pointer-events-none absolute bottom-full left-1/2 mb-2",
|
|
193
|
+
style: { "--at": thumbState.getThumbPercent(i) * 100 },
|
|
194
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
195
|
+
"data-slot": "slider-bubble",
|
|
196
|
+
"data-shown": isDragging || isFocusVisible || void 0,
|
|
197
|
+
className: "slider-bubble block whitespace-nowrap rounded-pill bg-foreground px-3 py-1 text-sm leading-none font-medium text-background shadow-surface",
|
|
198
|
+
children: /* @__PURE__ */ jsx(RollingText, {
|
|
199
|
+
value: thumbState.getThumbValue(i),
|
|
200
|
+
text: thumbState.getThumbValueLabel(i)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
}, i))
|
|
205
|
+
] });
|
|
206
|
+
}
|
|
207
|
+
}),
|
|
208
|
+
end ? /* @__PURE__ */ jsx("div", {
|
|
209
|
+
className: "col-start-3 row-start-1 ms-3 flex",
|
|
210
|
+
children: end
|
|
211
|
+
}) : null,
|
|
212
|
+
markList.some((mark) => mark.label != null) ? /* @__PURE__ */ jsx(SliderMarkLabels, {
|
|
213
|
+
marks: markList,
|
|
214
|
+
percentOf,
|
|
215
|
+
minValue,
|
|
216
|
+
maxValue
|
|
217
|
+
}) : null
|
|
218
|
+
]
|
|
219
|
+
}),
|
|
220
|
+
description != null ? /* @__PURE__ */ jsx("p", {
|
|
221
|
+
id: descriptionId,
|
|
222
|
+
"data-slot": "slider-description",
|
|
223
|
+
className: "m-0 text-sm leading-(--leading-normal) text-text-secondary",
|
|
224
|
+
children: description
|
|
225
|
+
}) : null
|
|
226
|
+
]
|
|
143
227
|
});
|
|
144
228
|
});
|
|
145
229
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slider.js","names":["Slider","AriaSlider","AriaLabel","AriaSliderOutput","AriaSliderTrack","AriaSliderThumb"],"sources":["../../src/slider/slider.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useState, type ReactNode } from \"react\";\nimport {\n Label as AriaLabel,\n Slider as AriaSlider,\n SliderOutput as AriaSliderOutput,\n SliderThumb as AriaSliderThumb,\n SliderTrack as AriaSliderTrack,\n type SliderProps as AriaSliderProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Slider, ported from the legacy shadcn/Radix component.\n *\n * Census: 4 call sites, all single-thumb, none of them named. Radix only\n * generates thumb labels at two or more thumbs, so all four are announced\n * as an unnamed slider -- `AudioSettings.tsx` even renders a\n * `<Label>Input Volume</Label>` directly above one, with no `htmlFor` and\n * no `id`, so it is associated with nothing at all.\n *\n * `label` IS REQUIRED here, the same answer Progress and Dialog give.\n *\n * THE PALETTE DELIBERATELY DOES NOT MATCH SWITCH, and the reason is worth\n * stating because the two look like the same problem. A Switch has no\n * fill: its track is one colour and the thumb tells you the state, so\n * `border` unchecked and `primary-strong` checked works. A Slider has a\n * filled portion whose LENGTH is the value -- and `primary-strong` on a\n * `border` track is 1.01:1. The two halves would be the same grey and the\n * value would be invisible to anyone not reading the thumb position.\n *\n * So the track is `muted` with a `border` boundary and the fill is\n * `primary-strong`: fill against track 4.61:1, track against page 5.25:1,\n * thumb against fill 5.20:1, thumb edge against track 4.65:1. All four\n * pairs are in the build-failing contrast contract.\n *\n * Three legacy defects go with it: the focus ring was `ring-1`, a 1px\n * box-shadow that forced-colors discards entirely; the thumb border was\n * `border-primary/50`, an alpha edge with no measurable contrast; and the\n * disabled styling used `disabled:`, a pseudo-class that only applies to\n * form elements -- Radix renders a `<span role=\"slider\">`, so a disabled\n * slider looked exactly like an enabled one.\n */\nexport const sliderVariants = cva([\"flex w-full flex-col gap-2 box-border font-sans\"]);\n\nexport const sliderTrackVariants = cva([\n \"relative flex w-full items-center\",\n // h-6 is the TRACK ROW, not the visible bar -- it gives the 24px\n // pointer target WCAG 2.5.8 wants. The bar itself is drawn inside.\n \"h-6\",\n // A size container, so the value bubble can hold inside it (styles.css).\n \"slider-track\",\n]);\n\nexport const sliderBarVariants = cva([\n \"absolute h-2 w-full rounded-pill box-border\",\n \"bg-muted border-(length:--border-width) border-border\",\n]);\n\nexport const sliderFillVariants = cva([\"absolute h-2 rounded-pill bg-primary-strong\", \"slider-fill\"]);\n\nexport const sliderThumbVariants = cva([\n // size-6 is 24px: WCAG 2.5.8 target size. The legacy thumb was 16px at\n // all four call sites, and one of them crams the whole control into 80px.\n \"size-6 rounded-full box-border\",\n \"bg-background border-(length:--border-width) border-border shadow-surface\",\n \"cursor-grab data-[dragging]:cursor-grabbing\",\n // Follows an outside value on a spring, swells while held (styles.css).\n \"slider-thumb\",\n // A real outline, not the legacy ring-1 box-shadow.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n // data-[disabled], NOT disabled:. The legacy used the CSS :disabled\n // pseudo-class, which applies only to form elements -- React Aria and\n // Radix both render a span, so that styling never fired.\n \"data-[disabled]:cursor-not-allowed\",\n]);\n\n/**\n * Text that rolls when its value changes: a larger value rises in from below, a\n * smaller one drops in from above. The caller's own text stays for a screen\n * reader; the copy the eye sees is aria-hidden.\n */\nfunction RollingText({ value, text }: { value: number; text: string }) {\n const [trail, setTrail] = useState({ value, dir: 1 });\n if (trail.value !== value) setTrail({ value, dir: value > trail.value ? 1 : -1 });\n const dir = trail.value === value ? trail.dir : value > trail.value ? 1 : -1;\n return (\n <>\n <span className=\"sr-only\">{text}</span>\n <span aria-hidden=\"true\">\n <SwapText swapKey={text} dir={dir} motion=\"roll\" className=\"inline-block\">\n {text}\n </SwapText>\n </span>\n </>\n );\n}\n\n/**\n * A value along a range.\n *\n * @example\n * <Slider label=\"Input volume\" defaultValue={40} />\n * <Slider label=\"Playback position\" value={t} onChange={seek} formatOptions={{ style: \"percent\" }} />\n */\nexport interface SliderProps\n extends Omit<AriaSliderProps<number>, \"className\" | \"style\" | \"children\"> {\n /**\n * What the slider controls, and its accessible name.\n *\n * **Required.** All four legacy call sites are unnamed sliders.\n */\n label: ReactNode;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /**\n * Show the current value as text beside the label.\n *\n * On by default. It is the text equivalent of the fill length, which\n * matters because in greyscale the filled and unfilled halves are\n * distinguishable only by the thumb.\n */\n showValue?: boolean;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Slider = forwardRef<HTMLDivElement, SliderProps>(function Slider(\n { label, labelHidden, showValue = true, className, ...props },\n ref,\n) {\n return (\n <AriaSlider\n ref={ref}\n {...props}\n data-slot=\"slider\"\n className={cn(sliderVariants(), className)}\n >\n <div className={cnAll(\"flex items-center justify-between gap-2\", labelHidden && \"sr-only\")}>\n <AriaLabel\n data-slot=\"slider-label\"\n className=\"text-sm leading-(--leading-normal) text-foreground\"\n >\n {label}\n </AriaLabel>\n {showValue ? (\n <AriaSliderOutput\n data-slot=\"slider-output\"\n className=\"text-sm leading-(--leading-normal) text-muted-foreground\"\n >\n {({ state }) => (\n <RollingText value={state.getThumbValue(0)} text={state.getThumbValueLabel(0)} />\n )}\n </AriaSliderOutput>\n ) : null}\n </div>\n\n <AriaSliderTrack data-slot=\"slider-track\" className={sliderTrackVariants()}>\n {({ state }) => (\n <>\n <div data-slot=\"slider-bar\" className={sliderBarVariants()} />\n <div\n data-slot=\"slider-fill\"\n className={sliderFillVariants()}\n data-dragging={state.isThumbDragging(0) || undefined}\n style={{ width: `${state.getThumbPercent(0) * 100}%` }}\n />\n <AriaSliderThumb data-slot=\"slider-thumb\" className={sliderThumbVariants()}>\n {({ isDragging, isFocusVisible, state: thumbState }) => (\n // Decorative: the output beside the label already carries the value.\n <span\n aria-hidden=\"true\"\n className=\"slider-bubble-anchor pointer-events-none absolute bottom-full left-1/2 mb-2\"\n style={{ \"--at\": thumbState.getThumbPercent(0) * 100 } as React.CSSProperties}\n >\n <span\n data-slot=\"slider-bubble\"\n data-shown={isDragging || isFocusVisible || undefined}\n className=\"slider-bubble block whitespace-nowrap rounded-pill bg-foreground px-3 py-1 text-sm leading-none font-medium text-background shadow-surface\"\n >\n <RollingText\n value={thumbState.getThumbValue(0)}\n text={thumbState.getThumbValueLabel(0)}\n />\n </span>\n </span>\n )}\n </AriaSliderThumb>\n </>\n )}\n </AriaSliderTrack>\n </AriaSlider>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,MAAa,iBAAiB,IAAI,CAAC,iDAAiD,CAAC;AAErF,MAAa,sBAAsB,IAAI;CACrC;CAGA;CAEA;AACF,CAAC;AAED,MAAa,oBAAoB,IAAI,CACnC,+CACA,uDACF,CAAC;AAED,MAAa,qBAAqB,IAAI,CAAC,+CAA+C,aAAa,CAAC;AAEpG,MAAa,sBAAsB,IAAI;CAGrC;CACA;CACA;CAEA;CAEA;CACA;CACA;CACA;CACA;CAIA;AACF,CAAC;;;;;;AAOD,SAAS,YAAY,EAAE,OAAO,QAAyC;CACrE,MAAM,CAAC,OAAO,YAAY,SAAS;EAAE;EAAO,KAAK;CAAE,CAAC;CACpD,IAAI,MAAM,UAAU,OAAO,SAAS;EAAE;EAAO,KAAK,QAAQ,MAAM,QAAQ,IAAI;CAAG,CAAC;CAChF,MAAM,MAAM,MAAM,UAAU,QAAQ,MAAM,MAAM,QAAQ,MAAM,QAAQ,IAAI;CAC1E,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,QAAD;EAAM,WAAU;EAAW,UAAA;CAAW,CAAA,GACtC,oBAAC,QAAD;EAAM,eAAY;EAChB,UAAA,oBAAC,UAAD;GAAU,SAAS;GAAW;GAAK,QAAO;GAAO,WAAU;GACxD,UAAA;EACO,CAAA;CACN,CAAA,CACN,EAAA,CAAA;AAEN;AA+BA,MAAaA,WAAS,WAAwC,SAASA,SACrE,EAAE,OAAO,aAAa,YAAY,MAAM,WAAW,GAAG,SACtD,KACA;CACA,OACE,qBAACC,QAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,eAAe,GAAG,SAAS;EAJ3C,UAAA,CAME,qBAAC,OAAD;GAAK,WAAW,MAAM,2CAA2C,eAAe,SAAS;GAAzF,UAAA,CACE,oBAACC,OAAD;IACE,aAAU;IACV,WAAU;IAET,UAAA;GACQ,CAAA,GACV,YACC,oBAACC,cAAD;IACE,aAAU;IACV,WAAU;IAER,WAAA,EAAE,YACF,oBAAC,aAAD;KAAa,OAAO,MAAM,cAAc,CAAC;KAAG,MAAM,MAAM,mBAAmB,CAAC;IAAI,CAAA;GAElE,CAAA,IAChB,IACD;EAEL,CAAA,GAAA,oBAACC,aAAD;GAAiB,aAAU;GAAe,WAAW,oBAAoB;GACrE,WAAA,EAAE,YACF,qBAAA,YAAA,EAAA,UAAA;IACE,oBAAC,OAAD;KAAK,aAAU;KAAa,WAAW,kBAAkB;IAAI,CAAA;IAC7D,oBAAC,OAAD;KACE,aAAU;KACV,WAAW,mBAAmB;KAC9B,iBAAe,MAAM,gBAAgB,CAAC,KAAK,KAAA;KAC3C,OAAO,EAAE,OAAO,GAAG,MAAM,gBAAgB,CAAC,IAAI,IAAI,GAAG;IACtD,CAAA;IACD,oBAACC,aAAD;KAAiB,aAAU;KAAe,WAAW,oBAAoB;KACrE,WAAA,EAAE,YAAY,gBAAgB,OAAO,iBAErC,oBAAC,QAAD;MACE,eAAY;MACZ,WAAU;MACV,OAAO,EAAE,QAAQ,WAAW,gBAAgB,CAAC,IAAI,IAAI;MAErD,UAAA,oBAAC,QAAD;OACE,aAAU;OACV,cAAY,cAAc,kBAAkB,KAAA;OAC5C,WAAU;OAEV,UAAA,oBAAC,aAAD;QACE,OAAO,WAAW,cAAc,CAAC;QACjC,MAAM,WAAW,mBAAmB,CAAC;OACtC,CAAA;MACG,CAAA;KACF,CAAA;IAEO,CAAA;GACjB,EAAA,CAAA;EAEW,CAAA,CACP;;AAEhB,CAAC"}
|
|
1
|
+
{"version":3,"file":"slider.js","names":["Slider","AriaSlider","AriaLabel","AriaSliderOutput","AriaSliderTrack","AriaSliderThumb"],"sources":["../../src/slider/slider.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n Fragment,\n forwardRef,\n useContext,\n useId,\n useState,\n type ReactElement,\n type ReactNode,\n type RefAttributes,\n} from \"react\";\nimport {\n Label as AriaLabel,\n Slider as AriaSlider,\n SliderOutput as AriaSliderOutput,\n SliderStateContext,\n SliderThumb as AriaSliderThumb,\n SliderTrack as AriaSliderTrack,\n type SliderProps as AriaSliderProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Slider, ported from the legacy shadcn/Radix component.\n *\n * Census: 4 call sites, all single-thumb, none of them named. Radix only\n * generates thumb labels at two or more thumbs, so all four are announced\n * as an unnamed slider -- `AudioSettings.tsx` even renders a\n * `<Label>Input Volume</Label>` directly above one, with no `htmlFor` and\n * no `id`, so it is associated with nothing at all.\n *\n * `label` IS REQUIRED here, the same answer Progress and Dialog give.\n *\n * LOOK (UX owner's colours): a thin, rounded track in primary 200 (light\n * #FEEAE0, dark #441B00, role `slider-track`, no outline) with a fill in\n * `primary-strong` (the dark orange, so it reads against the pale track) and a\n * solid light thumb (`slider-thumb`) with a soft shadow. The label sits left\n * and the value right. The fill's LENGTH is the value.\n *\n * Contrast, from contrast-pairs.json (build-failing): fill against the track\n * 4.9:1 light / 8.3:1 dark; thumb against what it meets at its edge (the fill in\n * light, a 2px ring in the page colour in dark -- see `shadow.slider-thumb`)\n * 5.7:1 / 18.6:1. A near-white thumb on a light fill (dark) would otherwise\n * melt into it, and the old dark thumb (the card colour) all but vanished on the\n * page. The pale track is 1.12:1 / 1.3:1 on the page, under the 3:1 non-text\n * floor, by the same accepted trade-off as `--border`; the state is the EDGE OF\n * THE FILL (>= 4.9:1) plus the value text beside the label. The thumb is a 24px\n * target and rests on the track with a shadow.\n *\n * Three legacy defects go with it: the focus ring was `ring-1`, a 1px\n * box-shadow that forced-colors discards entirely; the thumb border was\n * `border-primary/50`, an alpha edge with no measurable contrast; and the\n * disabled styling used `disabled:`, a pseudo-class that only applies to\n * form elements -- Radix renders a `<span role=\"slider\">`, so a disabled\n * slider looked exactly like an enabled one.\n */\nexport const sliderVariants = cva([\n \"flex w-full flex-col gap-2 box-border font-sans\",\n \"data-[disabled]:opacity-(--opacity-disabled)\",\n]);\n\nexport const sliderTrackVariants = cva([\n // The track is inset by half a thumb (mx-3), so a thumb at either limit stays\n // inside the component and mark labels at the ends have room.\n \"relative mx-3 flex items-center\",\n // h-6 is the TRACK ROW, not the visible bar -- it gives the 24px\n // pointer target WCAG 2.5.8 wants. The bar itself is drawn inside.\n \"h-6\",\n // A size container, so the value bubble can hold inside it (styles.css).\n \"slider-track\",\n]);\n\nexport const sliderBarVariants = cva([\"absolute h-1.5 w-full rounded-pill bg-slider-track\"]);\n\nexport const sliderFillVariants = cva([\"absolute h-1.5 rounded-pill bg-primary-strong\", \"slider-fill\"]);\n\nexport const sliderThumbVariants = cva([\n // size-6 is 24px: WCAG 2.5.8 target size. The legacy thumb was 16px at\n // all four call sites, and one of them crams the whole control into 80px.\n \"size-6 rounded-circle box-border\",\n // React Aria sets translate(-50%, -50%) and leaves `top` to us: without it the thumb sits on the track's top edge.\n \"top-1/2\",\n // A solid white circle with a soft shadow (the slider-thumb utility; in dark also a ring), no outline.\n \"bg-slider-thumb\",\n \"cursor-grab data-[dragging]:cursor-grabbing\",\n // Follows an outside value on a spring, swells while held (styles.css).\n \"slider-thumb\",\n // A real outline, not the legacy ring-1 box-shadow.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n // data-[disabled], NOT disabled:. The legacy used the CSS :disabled\n // pseudo-class, which applies only to form elements -- React Aria and\n // Radix both render a span, so that styling never fired.\n \"data-[disabled]:cursor-not-allowed\",\n]);\n\n/** A tick dot on the track: dim on the unfilled side, light where the fill passes under it. */\nconst sliderTickClasses =\n \"pointer-events-none absolute top-1/2 size-1 -translate-1/2 rounded-pill bg-foreground/30 data-[on]:bg-background/60 motion-safe:transition-colors motion-safe:duration-(--duration-quick)\";\n\n/** A step label under its tick. */\nconst sliderMarkLabelClasses =\n \"slider-mark-label absolute top-0 whitespace-nowrap text-xs leading-(--leading-normal) text-text-secondary tabular-nums data-[on]:text-foreground motion-safe:transition-colors motion-safe:duration-(--duration-quick)\";\n\n/**\n * Text that rolls when its value changes: a larger value rises in from below, a\n * smaller one drops in from above. The caller's own text stays for a screen\n * reader; the copy the eye sees is aria-hidden.\n */\nfunction RollingText({ value, text }: { value: number; text: string }) {\n const [trail, setTrail] = useState({ value, dir: 1 });\n if (trail.value !== value) setTrail({ value, dir: value > trail.value ? 1 : -1 });\n const dir = trail.value === value ? trail.dir : value > trail.value ? 1 : -1;\n return (\n <>\n <span className=\"sr-only\">{text}</span>\n <span aria-hidden=\"true\">\n <SwapText swapKey={text} dir={dir} motion=\"roll\" className=\"inline-block\">\n {text}\n </SwapText>\n </span>\n </>\n );\n}\n\n/** One thumb is a number; two thumbs are a `[low, high]` pair. */\nexport type SliderValue = number | [number, number];\n\n/** A tick on the track, with an optional label printed under it. */\nexport interface SliderMark {\n value: number;\n label?: ReactNode;\n}\n\n/**\n * A value along a range.\n *\n * @example\n * <Slider label=\"Input volume\" defaultValue={40} />\n * <Slider label=\"Playback position\" value={t} onChange={seek} formatOptions={{ style: \"percent\" }} />\n * <Slider label=\"Budget\" defaultValue={[2200, 8000]} minValue={0} maxValue={10000} step={100} />\n */\nexport interface SliderProps<T extends SliderValue = number>\n extends Omit<\n AriaSliderProps<number | number[]>,\n \"className\" | \"style\" | \"children\" | \"value\" | \"defaultValue\" | \"onChange\" | \"onChangeEnd\"\n > {\n /**\n * What the slider controls, and its accessible name.\n *\n * **Required.** All four legacy call sites are unnamed sliders.\n */\n label: ReactNode;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /**\n * Show the current value as text beside the label.\n *\n * On by default. It is the text equivalent of the fill length, which\n * matters because in greyscale the filled and unfilled halves are\n * distinguishable only by the thumb.\n */\n showValue?: boolean;\n /** A number for one thumb, a `[low, high]` pair for a range with two thumbs. */\n value?: T;\n /** The uncontrolled value; its shape (a number or a pair) also picks one or two thumbs. */\n defaultValue?: T;\n onChange?: (value: T) => void;\n /** Runs once a drag is released or a key changes the value, for work too heavy for every step. */\n onChangeEnd?: (value: T) => void;\n /** Grey helper text under the control. It also describes the slider to a screen reader. */\n description?: ReactNode;\n /**\n * Tick dots on the track. A number draws a tick; a mark with a `label` also\n * prints it under the track. Marks are decoration: the slider still reads out\n * its own value, so pair them with `step` to make the ticks reachable.\n */\n marks?: (number | SliderMark)[];\n /** Accessible names for the two range thumbs. @default [\"Minimum\", \"Maximum\"] */\n thumbLabels?: [string, string];\n /** Content before the track, such as an icon button that mutes. */\n start?: ReactNode;\n /** Content after the track. */\n end?: ReactNode;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nconst RANGE_DASH = \" – \";\n\n/** Step labels under the track, each centred on its tick (the end ones hold inside the component). */\nfunction SliderMarkLabels({\n marks,\n percentOf,\n minValue,\n maxValue,\n}: {\n marks: SliderMark[];\n percentOf: (value: number) => number;\n minValue: number;\n maxValue: number;\n}) {\n const state = useContext(SliderStateContext);\n const values = state?.values ?? [];\n const isOn = (v: number) =>\n values.length > 1 ? v >= values[0]! && v <= values[values.length - 1]! : v <= (values[0] ?? minValue);\n return (\n <div\n aria-hidden=\"true\"\n data-slot=\"slider-marks\"\n className=\"relative col-start-2 row-start-2 mx-3 mt-1 h-5\"\n >\n {marks.map((mark) =>\n mark.label == null ? null : (\n <span\n key={mark.value}\n data-slot=\"slider-mark-label\"\n data-on={isOn(mark.value) || undefined}\n data-edge={mark.value <= minValue ? \"start\" : mark.value >= maxValue ? \"end\" : undefined}\n className={sliderMarkLabelClasses}\n style={{ left: `${percentOf(mark.value)}%` }}\n >\n {mark.label}\n </span>\n ),\n )}\n </div>\n );\n}\n\nexport const Slider = forwardRef<HTMLDivElement, SliderProps<SliderValue>>(function Slider(\n {\n label,\n labelHidden,\n showValue = true,\n description,\n marks,\n thumbLabels = [\"Minimum\", \"Maximum\"],\n start,\n end,\n className,\n \"aria-describedby\": describedByProp,\n ...props\n },\n ref,\n) {\n const descriptionId = useId();\n const describedBy = [describedByProp, description == null ? null : descriptionId]\n .filter(Boolean)\n .join(\" \");\n const minValue = props.minValue ?? 0;\n const maxValue = props.maxValue ?? 100;\n const markList = (marks ?? []).map((mark) => (typeof mark === \"number\" ? { value: mark } : mark));\n const percentOf = (v: number) =>\n maxValue === minValue ? 0 : ((v - minValue) / (maxValue - minValue)) * 100;\n\n return (\n <AriaSlider\n ref={ref}\n {...(props as AriaSliderProps<number | number[]>)}\n data-slot=\"slider\"\n className={cn(sliderVariants(), className)}\n >\n <div className={cnAll(\"flex items-baseline justify-between gap-4\", labelHidden && \"sr-only\")}>\n <AriaLabel\n data-slot=\"slider-label\"\n className=\"text-sm leading-(--leading-normal) font-medium text-foreground\"\n >\n {label}\n </AriaLabel>\n {showValue ? (\n <AriaSliderOutput\n data-slot=\"slider-output\"\n className=\"text-sm leading-(--leading-normal) whitespace-nowrap text-text-secondary tabular-nums\"\n >\n {({ state }) =>\n state.values.map((_, i) => (\n <Fragment key={i}>\n {i > 0 ? <span aria-hidden=\"true\">{RANGE_DASH}</span> : null}\n <RollingText value={state.getThumbValue(i)} text={state.getThumbValueLabel(i)} />\n </Fragment>\n ))\n }\n </AriaSliderOutput>\n ) : null}\n </div>\n\n <div className=\"grid grid-cols-[auto_minmax(0,1fr)_auto] items-center\">\n {start ? <div className=\"col-start-1 row-start-1 me-3 flex\">{start}</div> : null}\n\n <AriaSliderTrack\n data-slot=\"slider-track\"\n className={cnAll(sliderTrackVariants(), \"col-start-2 row-start-1\")}\n >\n {({ state }) => {\n const count = state.values.length;\n const low = count > 1 ? state.getThumbPercent(0) * 100 : 0;\n const high = state.getThumbPercent(count - 1) * 100;\n const dragging = state.values.some((_, i) => state.isThumbDragging(i));\n const isOn = (v: number) =>\n count > 1 ? v >= state.values[0]! && v <= state.values[count - 1]! : v <= state.values[0]!;\n return (\n <>\n <div data-slot=\"slider-bar\" className={sliderBarVariants()} />\n <div\n data-slot=\"slider-fill\"\n className={sliderFillVariants()}\n data-dragging={dragging || undefined}\n style={{ left: `${low}%`, width: `${high - low}%` }}\n />\n {markList.map((mark) => (\n <span\n key={mark.value}\n aria-hidden=\"true\"\n data-slot=\"slider-tick\"\n data-on={isOn(mark.value) || undefined}\n className={sliderTickClasses}\n style={{ left: `${percentOf(mark.value)}%` }}\n />\n ))}\n {state.values.map((_, i) => (\n <AriaSliderThumb\n key={i}\n index={i}\n aria-label={count > 1 ? thumbLabels[i] : undefined}\n // On the focusable inputs: the group element does not carry it.\n aria-describedby={describedBy || undefined}\n data-slot=\"slider-thumb\"\n className={sliderThumbVariants()}\n >\n {({ isDragging, isFocusVisible, state: thumbState }) => (\n // Decorative: the output beside the label already carries the value.\n <span\n aria-hidden=\"true\"\n className=\"slider-bubble-anchor pointer-events-none absolute bottom-full left-1/2 mb-2\"\n style={{ \"--at\": thumbState.getThumbPercent(i) * 100 } as React.CSSProperties}\n >\n <span\n data-slot=\"slider-bubble\"\n data-shown={isDragging || isFocusVisible || undefined}\n className=\"slider-bubble block whitespace-nowrap rounded-pill bg-foreground px-3 py-1 text-sm leading-none font-medium text-background shadow-surface\"\n >\n <RollingText\n value={thumbState.getThumbValue(i)}\n text={thumbState.getThumbValueLabel(i)}\n />\n </span>\n </span>\n )}\n </AriaSliderThumb>\n ))}\n </>\n );\n }}\n </AriaSliderTrack>\n\n {end ? <div className=\"col-start-3 row-start-1 ms-3 flex\">{end}</div> : null}\n\n {markList.some((mark) => mark.label != null) ? (\n <SliderMarkLabels\n marks={markList}\n percentOf={percentOf}\n minValue={minValue}\n maxValue={maxValue}\n />\n ) : null}\n </div>\n\n {description != null ? (\n <p\n id={descriptionId}\n data-slot=\"slider-description\"\n className=\"m-0 text-sm leading-(--leading-normal) text-text-secondary\"\n >\n {description}\n </p>\n ) : null}\n </AriaSlider>\n );\n}) as <T extends SliderValue = number>(\n props: SliderProps<T> & RefAttributes<HTMLDivElement>,\n) => ReactElement | null;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/slope-chart/slope-chart.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* SlopeChart: how several items changed between exactly two moments, and how
|
|
4
|
+
* their order changed. Each item is one line from its first value to its second
|
|
5
|
+
* on a shared axis, with its name and value at each end and the change in rank
|
|
6
|
+
* beside the second value.
|
|
7
|
+
*
|
|
8
|
+
* The lines draw left to right once the chart is in view (a beat apart), the end
|
|
9
|
+
* dots land as each pen arrives, and the labels fade in behind them. New data
|
|
10
|
+
* moves every line, dot, leader and label from where it is on screen. Hovering,
|
|
11
|
+
* touching or the arrow keys bring one item forward, fade the others' lines and
|
|
12
|
+
* show its numbers; the arrow keys walk the items in the second period's order.
|
|
13
|
+
*
|
|
14
|
+
* Plain SVG written straight to the DOM by one small spring tween; labels are
|
|
15
|
+
* real text (they truncate), so they sit beside the SVG and move with it.
|
|
16
|
+
*/
|
|
17
|
+
export interface SlopeItem {
|
|
18
|
+
/** Stable identity. An item keeps its line across datasets, so switching morphs it. */
|
|
19
|
+
key: string;
|
|
20
|
+
/** Name beside the start value, in the tooltip and in the table. */
|
|
21
|
+
label: string;
|
|
22
|
+
/** Value in the first period. */
|
|
23
|
+
start: number;
|
|
24
|
+
/** Value in the second period. */
|
|
25
|
+
end: number;
|
|
26
|
+
}
|
|
27
|
+
export interface SlopeChartProps {
|
|
28
|
+
data: SlopeItem[];
|
|
29
|
+
/** What is measured, such as "Conversion rate by channel". Names the chart for assistive technology. */
|
|
30
|
+
label: string;
|
|
31
|
+
/** Heading of the first column, such as "Q1". */
|
|
32
|
+
startLabel: string;
|
|
33
|
+
/** Heading of the second column, such as "Q2". */
|
|
34
|
+
endLabel: string;
|
|
35
|
+
formatValue?: (value: number) => string;
|
|
36
|
+
/** Formats the change in the tooltip. Defaults to the difference, in formatValue's terms. */
|
|
37
|
+
formatChange?: (change: number, item: SlopeItem) => string;
|
|
38
|
+
/** Plot height in pixels. Defaults to 44 pixels per item, and at least 200. */
|
|
39
|
+
height?: number;
|
|
40
|
+
/** The item drawn in the accent, such as the one the story is about. */
|
|
41
|
+
highlightKey?: string | null;
|
|
42
|
+
/** Controlled item in focus. The others' lines fade while one is in focus. */
|
|
43
|
+
activeKey?: string | null;
|
|
44
|
+
onActiveChange?: (key: string | null) => void;
|
|
45
|
+
/** Rank movement beside each end value. @default true */
|
|
46
|
+
ranks?: boolean;
|
|
47
|
+
/** @default "No data" */
|
|
48
|
+
emptyLabel?: string;
|
|
49
|
+
/** Extra classes for the root. Merged last. */
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
export declare const SlopeChart: import("react").ForwardRefExoticComponent<SlopeChartProps & import("react").RefAttributes<HTMLElement>>;
|
|
53
|
+
//#endregion
|
|
54
|
+
//# sourceMappingURL=slope-chart.d.ts.map
|