@teamofsilicons/silicon-ui 0.0.0-stage → 0.1.0
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/LICENSE +22 -0
- package/README.md +63 -2
- package/components/accordion/accordion.d.ts +13 -0
- package/components/accordion/accordion.js +35 -0
- package/components/accordion/accordion.module.css +15 -0
- package/components/action-button/action-button.d.ts +11 -0
- package/components/action-button/action-button.js +57 -0
- package/components/action-button/action-button.module.css +15 -0
- package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
- package/components/activity-heatmap/activity-heatmap.js +303 -0
- package/components/activity-heatmap/activity-heatmap.module.css +83 -0
- package/components/alert/alert.d.ts +13 -0
- package/components/alert/alert.js +79 -0
- package/components/alert/alert.module.css +15 -0
- package/components/animated-counter/animated-counter.d.ts +13 -0
- package/components/animated-counter/animated-counter.js +73 -0
- package/components/animated-counter/animated-counter.module.css +12 -0
- package/components/announcement-bar/announcement-bar.d.ts +53 -0
- package/components/announcement-bar/announcement-bar.js +187 -0
- package/components/announcement-bar/announcement-bar.module.css +60 -0
- package/components/avatar/avatar.d.ts +8 -0
- package/components/avatar/avatar.js +18 -0
- package/components/avatar/avatar.module.css +60 -0
- package/components/avatar-group/avatar-group.d.ts +13 -0
- package/components/avatar-group/avatar-group.js +24 -0
- package/components/avatar-group/avatar-group.module.css +27 -0
- package/components/badge/badge.d.ts +10 -0
- package/components/badge/badge.js +66 -0
- package/components/badge/badge.module.css +52 -0
- package/components/bar-chart/bar-chart.d.ts +32 -0
- package/components/bar-chart/bar-chart.js +327 -0
- package/components/bar-chart/bar-chart.module.css +49 -0
- package/components/billing-toggle/billing-toggle.d.ts +35 -0
- package/components/billing-toggle/billing-toggle.js +83 -0
- package/components/billing-toggle/billing-toggle.module.css +139 -0
- package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
- package/components/bottom-sheet/bottom-sheet.js +386 -0
- package/components/bottom-sheet/bottom-sheet.module.css +69 -0
- package/components/breadcrumb/breadcrumb.d.ts +13 -0
- package/components/breadcrumb/breadcrumb.js +19 -0
- package/components/breadcrumb/breadcrumb.module.css +12 -0
- package/components/brush-chart/brush-chart.d.ts +45 -0
- package/components/brush-chart/brush-chart.js +426 -0
- package/components/brush-chart/brush-chart.module.css +69 -0
- package/components/button/button.d.ts +10 -0
- package/components/button/button.js +97 -0
- package/components/button/button.module.css +39 -0
- package/components/button-group/button-group.d.ts +59 -0
- package/components/button-group/button-group.js +280 -0
- package/components/button-group/button-group.module.css +87 -0
- package/components/button-link/button-link.d.ts +24 -0
- package/components/button-link/button-link.js +11 -0
- package/components/button-link/button-link.module.css +7 -0
- package/components/calendar/calendar.d.ts +22 -0
- package/components/calendar/calendar.js +253 -0
- package/components/calendar/calendar.module.css +51 -0
- package/components/card/card.d.ts +19 -0
- package/components/card/card.js +108 -0
- package/components/card/card.module.css +51 -0
- package/components/card-stack/card-stack.d.ts +34 -0
- package/components/card-stack/card-stack.js +291 -0
- package/components/card-stack/card-stack.module.css +63 -0
- package/components/carousel/carousel.d.ts +28 -0
- package/components/carousel/carousel.js +332 -0
- package/components/carousel/carousel.module.css +42 -0
- package/components/chat-thread/chat-thread.d.ts +135 -0
- package/components/chat-thread/chat-thread.js +406 -0
- package/components/chat-thread/chat-thread.module.css +242 -0
- package/components/checkbox/checkbox.d.ts +7 -0
- package/components/checkbox/checkbox.js +23 -0
- package/components/checkbox/checkbox.module.css +15 -0
- package/components/chip-group/chip-group.d.ts +24 -0
- package/components/chip-group/chip-group.js +196 -0
- package/components/chip-group/chip-group.module.css +38 -0
- package/components/code-block/code-block.d.ts +11 -0
- package/components/code-block/code-block.js +133 -0
- package/components/code-block/code-block.module.css +43 -0
- package/components/color-picker/color-picker.d.ts +40 -0
- package/components/color-picker/color-picker.js +449 -0
- package/components/color-picker/color-picker.module.css +111 -0
- package/components/combobox/combobox.d.ts +19 -0
- package/components/combobox/combobox.js +167 -0
- package/components/combobox/combobox.module.css +28 -0
- package/components/command-palette/command-palette.d.ts +20 -0
- package/components/command-palette/command-palette.js +151 -0
- package/components/command-palette/command-palette.module.css +94 -0
- package/components/comment-thread/comment-thread.d.ts +120 -0
- package/components/comment-thread/comment-thread.js +343 -0
- package/components/comment-thread/comment-thread.module.css +195 -0
- package/components/confirm-morph/confirm-morph.d.ts +65 -0
- package/components/confirm-morph/confirm-morph.js +355 -0
- package/components/confirm-morph/confirm-morph.module.css +146 -0
- package/components/context-menu/context-menu.d.ts +18 -0
- package/components/context-menu/context-menu.js +148 -0
- package/components/context-menu/context-menu.module.css +17 -0
- package/components/copy-button/copy-button.d.ts +10 -0
- package/components/copy-button/copy-button.js +69 -0
- package/components/copy-button/copy-button.module.css +21 -0
- package/components/countdown/countdown.d.ts +31 -0
- package/components/countdown/countdown.js +177 -0
- package/components/countdown/countdown.module.css +49 -0
- package/components/date-picker/date-picker.d.ts +17 -0
- package/components/date-picker/date-picker.js +100 -0
- package/components/date-picker/date-picker.module.css +34 -0
- package/components/date-range-picker/date-range-picker.d.ts +44 -0
- package/components/date-range-picker/date-range-picker.js +534 -0
- package/components/date-range-picker/date-range-picker.module.css +214 -0
- package/components/dialog/dialog.d.ts +11 -0
- package/components/dialog/dialog.js +52 -0
- package/components/dialog/dialog.module.css +36 -0
- package/components/donut-chart/donut-chart.d.ts +46 -0
- package/components/donut-chart/donut-chart.js +382 -0
- package/components/donut-chart/donut-chart.module.css +48 -0
- package/components/drawer/drawer.d.ts +20 -0
- package/components/drawer/drawer.js +111 -0
- package/components/drawer/drawer.module.css +97 -0
- package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
- package/components/dropdown-menu/dropdown-menu.js +39 -0
- package/components/dropdown-menu/dropdown-menu.module.css +38 -0
- package/components/elastic-slider/elastic-slider.d.ts +39 -0
- package/components/elastic-slider/elastic-slider.js +261 -0
- package/components/elastic-slider/elastic-slider.module.css +40 -0
- package/components/empty-state/empty-state.d.ts +12 -0
- package/components/empty-state/empty-state.js +69 -0
- package/components/empty-state/empty-state.module.css +13 -0
- package/components/expandable-card/expandable-card.d.ts +13 -0
- package/components/expandable-card/expandable-card.js +89 -0
- package/components/expandable-card/expandable-card.module.css +22 -0
- package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
- package/components/expanding-button-group/expanding-button-group.js +258 -0
- package/components/expanding-button-group/expanding-button-group.module.css +44 -0
- package/components/expanding-search/expanding-search.d.ts +38 -0
- package/components/expanding-search/expanding-search.js +242 -0
- package/components/expanding-search/expanding-search.module.css +52 -0
- package/components/file-dropzone/file-dropzone.d.ts +44 -0
- package/components/file-dropzone/file-dropzone.js +379 -0
- package/components/file-dropzone/file-dropzone.module.css +115 -0
- package/components/file-upload/file-upload.d.ts +22 -0
- package/components/file-upload/file-upload.js +192 -0
- package/components/file-upload/file-upload.module.css +49 -0
- package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
- package/components/filter-toolbar/filter-toolbar.js +446 -0
- package/components/filter-toolbar/filter-toolbar.module.css +70 -0
- package/components/floating-button-group/floating-button-group.d.ts +51 -0
- package/components/floating-button-group/floating-button-group.js +215 -0
- package/components/floating-button-group/floating-button-group.module.css +74 -0
- package/components/gauge/gauge.d.ts +18 -0
- package/components/gauge/gauge.js +101 -0
- package/components/gauge/gauge.module.css +22 -0
- package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
- package/components/hold-to-confirm/hold-to-confirm.js +165 -0
- package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
- package/components/hover-card/hover-card.d.ts +38 -0
- package/components/hover-card/hover-card.js +145 -0
- package/components/hover-card/hover-card.module.css +20 -0
- package/components/image-compare/image-compare.d.ts +27 -0
- package/components/image-compare/image-compare.js +267 -0
- package/components/image-compare/image-compare.module.css +25 -0
- package/components/in-view-title/in-view-title.d.ts +17 -0
- package/components/in-view-title/in-view-title.js +70 -0
- package/components/in-view-title/in-view-title.module.css +13 -0
- package/components/inline-edit/inline-edit.d.ts +27 -0
- package/components/inline-edit/inline-edit.js +315 -0
- package/components/inline-edit/inline-edit.module.css +73 -0
- package/components/input/input.d.ts +7 -0
- package/components/input/input.js +60 -0
- package/components/input/input.module.css +22 -0
- package/components/json-viewer/json-viewer.d.ts +48 -0
- package/components/json-viewer/json-viewer.js +463 -0
- package/components/json-viewer/json-viewer.module.css +92 -0
- package/components/line-chart/line-chart.d.ts +58 -0
- package/components/line-chart/line-chart.js +383 -0
- package/components/line-chart/line-chart.module.css +60 -0
- package/components/mention-input/mention-input.d.ts +72 -0
- package/components/mention-input/mention-input.js +417 -0
- package/components/mention-input/mention-input.module.css +134 -0
- package/components/metric-card/metric-card.d.ts +8 -0
- package/components/metric-card/metric-card.js +44 -0
- package/components/metric-card/metric-card.module.css +18 -0
- package/components/money-input/money-input.d.ts +48 -0
- package/components/money-input/money-input.js +503 -0
- package/components/money-input/money-input.module.css +77 -0
- package/components/morph-select/morph-select.d.ts +50 -0
- package/components/morph-select/morph-select.js +395 -0
- package/components/morph-select/morph-select.module.css +88 -0
- package/components/multi-select/multi-select.d.ts +18 -0
- package/components/multi-select/multi-select.js +105 -0
- package/components/multi-select/multi-select.module.css +37 -0
- package/components/notification-center/notification-center.d.ts +24 -0
- package/components/notification-center/notification-center.js +146 -0
- package/components/notification-center/notification-center.module.css +79 -0
- package/components/number-field/number-field.d.ts +41 -0
- package/components/number-field/number-field.js +455 -0
- package/components/number-field/number-field.module.css +71 -0
- package/components/otp-input/otp-input.d.ts +13 -0
- package/components/otp-input/otp-input.js +129 -0
- package/components/otp-input/otp-input.module.css +28 -0
- package/components/pagination/pagination.d.ts +7 -0
- package/components/pagination/pagination.js +60 -0
- package/components/pagination/pagination.module.css +16 -0
- package/components/password-field/password-field.d.ts +14 -0
- package/components/password-field/password-field.js +51 -0
- package/components/password-field/password-field.module.css +27 -0
- package/components/password-strength/password-strength.d.ts +107 -0
- package/components/password-strength/password-strength.js +161 -0
- package/components/password-strength/password-strength.module.css +57 -0
- package/components/phone-input/phone-input.d.ts +73 -0
- package/components/phone-input/phone-input.js +612 -0
- package/components/phone-input/phone-input.module.css +98 -0
- package/components/popover/popover.d.ts +6 -0
- package/components/popover/popover.js +19 -0
- package/components/popover/popover.module.css +32 -0
- package/components/progress/progress.d.ts +8 -0
- package/components/progress/progress.js +42 -0
- package/components/progress/progress.module.css +1 -0
- package/components/radio-cards/radio-cards.d.ts +35 -0
- package/components/radio-cards/radio-cards.js +110 -0
- package/components/radio-cards/radio-cards.module.css +84 -0
- package/components/radio-group/radio-group.d.ts +13 -0
- package/components/radio-group/radio-group.js +55 -0
- package/components/radio-group/radio-group.module.css +30 -0
- package/components/resizable-panels/resizable-panels.d.ts +37 -0
- package/components/resizable-panels/resizable-panels.js +338 -0
- package/components/resizable-panels/resizable-panels.module.css +40 -0
- package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
- package/components/rich-text-editor/rich-text-editor.js +1733 -0
- package/components/rich-text-editor/rich-text-editor.module.css +201 -0
- package/components/ridgeline/ridgeline.d.ts +37 -0
- package/components/ridgeline/ridgeline.js +346 -0
- package/components/ridgeline/ridgeline.module.css +58 -0
- package/components/scroll-area/scroll-area.d.ts +40 -0
- package/components/scroll-area/scroll-area.js +153 -0
- package/components/scroll-area/scroll-area.module.css +79 -0
- package/components/search-field/search-field.d.ts +7 -0
- package/components/search-field/search-field.js +20 -0
- package/components/search-field/search-field.module.css +17 -0
- package/components/segmented-control/segmented-control.d.ts +16 -0
- package/components/segmented-control/segmented-control.js +56 -0
- package/components/segmented-control/segmented-control.module.css +15 -0
- package/components/select/select.d.ts +15 -0
- package/components/select/select.js +36 -0
- package/components/select/select.module.css +35 -0
- package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
- package/components/shortcut-recorder/shortcut-recorder.js +380 -0
- package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
- package/components/signature-pad/signature-pad.d.ts +72 -0
- package/components/signature-pad/signature-pad.js +450 -0
- package/components/signature-pad/signature-pad.module.css +84 -0
- package/components/skeleton/skeleton.d.ts +13 -0
- package/components/skeleton/skeleton.js +53 -0
- package/components/skeleton/skeleton.module.css +11 -0
- package/components/slider/slider.d.ts +45 -0
- package/components/slider/slider.js +347 -0
- package/components/slider/slider.module.css +45 -0
- package/components/slope-chart/slope-chart.d.ts +38 -0
- package/components/slope-chart/slope-chart.js +183 -0
- package/components/slope-chart/slope-chart.module.css +51 -0
- package/components/slot-text/slot-text.d.ts +26 -0
- package/components/slot-text/slot-text.js +119 -0
- package/components/slot-text/slot-text.module.css +19 -0
- package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
- package/components/sortable-data-table/sortable-data-table.js +240 -0
- package/components/sortable-data-table/sortable-data-table.module.css +116 -0
- package/components/sparkline/sparkline.d.ts +19 -0
- package/components/sparkline/sparkline.js +239 -0
- package/components/sparkline/sparkline.module.css +26 -0
- package/components/split-button/split-button.d.ts +19 -0
- package/components/split-button/split-button.js +33 -0
- package/components/split-button/split-button.module.css +51 -0
- package/components/stepper/stepper.d.ts +36 -0
- package/components/stepper/stepper.js +108 -0
- package/components/stepper/stepper.module.css +74 -0
- package/components/streamgraph/streamgraph.d.ts +51 -0
- package/components/streamgraph/streamgraph.js +376 -0
- package/components/streamgraph/streamgraph.module.css +55 -0
- package/components/swipe-actions/swipe-actions.d.ts +35 -0
- package/components/swipe-actions/swipe-actions.js +330 -0
- package/components/swipe-actions/swipe-actions.module.css +64 -0
- package/components/switch/switch.d.ts +7 -0
- package/components/switch/switch.js +45 -0
- package/components/switch/switch.module.css +12 -0
- package/components/tabs/tabs.d.ts +8 -0
- package/components/tabs/tabs.js +138 -0
- package/components/tabs/tabs.module.css +27 -0
- package/components/tag-input/tag-input.d.ts +15 -0
- package/components/tag-input/tag-input.js +150 -0
- package/components/tag-input/tag-input.module.css +34 -0
- package/components/text-morph/text-morph.d.ts +13 -0
- package/components/text-morph/text-morph.js +69 -0
- package/components/text-morph/text-morph.module.css +8 -0
- package/components/text-reveal/text-reveal.d.ts +14 -0
- package/components/text-reveal/text-reveal.js +20 -0
- package/components/text-reveal/text-reveal.module.css +13 -0
- package/components/text-shimmer/text-shimmer.d.ts +18 -0
- package/components/text-shimmer/text-shimmer.js +90 -0
- package/components/text-shimmer/text-shimmer.module.css +9 -0
- package/components/textarea/textarea.d.ts +7 -0
- package/components/textarea/textarea.js +56 -0
- package/components/textarea/textarea.module.css +22 -0
- package/components/theme-switch/theme-switch.d.ts +11 -0
- package/components/theme-switch/theme-switch.js +37 -0
- package/components/theme-switch/theme-switch.module.css +31 -0
- package/components/time-picker/time-picker.d.ts +13 -0
- package/components/time-picker/time-picker.js +95 -0
- package/components/time-picker/time-picker.module.css +23 -0
- package/components/timeline/timeline.d.ts +47 -0
- package/components/timeline/timeline.js +171 -0
- package/components/timeline/timeline.module.css +51 -0
- package/components/toast/toast.d.ts +8 -0
- package/components/toast/toast.js +118 -0
- package/components/toast/toast.module.css +100 -0
- package/components/toast-stack/toast-stack.d.ts +57 -0
- package/components/toast-stack/toast-stack.js +399 -0
- package/components/toast-stack/toast-stack.module.css +61 -0
- package/components/tooltip/tooltip.d.ts +8 -0
- package/components/tooltip/tooltip.js +62 -0
- package/components/tooltip/tooltip.module.css +18 -0
- package/components/tree-view/tree-view.d.ts +17 -0
- package/components/tree-view/tree-view.js +147 -0
- package/components/tree-view/tree-view.module.css +22 -0
- package/components/treemap/treemap.d.ts +35 -0
- package/components/treemap/treemap.js +375 -0
- package/components/treemap/treemap.module.css +59 -0
- package/components/usage-meter/usage-meter.d.ts +24 -0
- package/components/usage-meter/usage-meter.js +296 -0
- package/components/usage-meter/usage-meter.module.css +61 -0
- package/components/user-menu/user-menu.d.ts +62 -0
- package/components/user-menu/user-menu.js +316 -0
- package/components/user-menu/user-menu.module.css +97 -0
- package/components/waffle-chart/waffle-chart.d.ts +39 -0
- package/components/waffle-chart/waffle-chart.js +244 -0
- package/components/waffle-chart/waffle-chart.module.css +52 -0
- package/components.json +1330 -0
- package/lib/escape.d.ts +19 -0
- package/lib/escape.js +50 -0
- package/lib/media.d.ts +353 -0
- package/lib/media.js +65 -0
- package/lib/menu-highlight.d.ts +28 -0
- package/lib/menu-highlight.js +43 -0
- package/lib/motion-tokens.d.ts +60 -0
- package/lib/motion-tokens.js +25 -0
- package/lib/return-focus.d.ts +9 -0
- package/lib/return-focus.js +53 -0
- package/lib/silicon-provider.d.ts +60 -0
- package/lib/silicon-provider.js +47 -0
- package/lib/use-copy-feedback.d.ts +8 -0
- package/lib/use-copy-feedback.js +36 -0
- package/lib/use-today.d.ts +2 -0
- package/lib/use-today.js +29 -0
- package/package.json +66 -4
- package/styles.css +218 -0
- package/styles.css.d.ts +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type SliderValue = number | [number, number];
|
|
3
|
+
export interface SliderMark {
|
|
4
|
+
value: number;
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SliderProps<T extends SliderValue = number> {
|
|
8
|
+
/** Visible label. Range thumbs are named from it unless `thumbLabels` says otherwise. */
|
|
9
|
+
label: string;
|
|
10
|
+
/** A number for one thumb, a pair for a range. */
|
|
11
|
+
value?: T;
|
|
12
|
+
defaultValue?: T;
|
|
13
|
+
onValueChange?: (value: T) => void;
|
|
14
|
+
/** Runs once a drag is released or a key changes the value, for work too heavy for every step. */
|
|
15
|
+
onValueCommit?: (value: T) => void;
|
|
16
|
+
min?: number;
|
|
17
|
+
max?: number;
|
|
18
|
+
step?: number;
|
|
19
|
+
/** PageUp, PageDown, and Shift with an arrow move this far. Defaults to a tenth of the range. */
|
|
20
|
+
largeStep?: number;
|
|
21
|
+
/** Ticks on the track. A number draws a tick; a mark with a label also prints it under the track, and clicking it moves there. */
|
|
22
|
+
marks?: (number | SliderMark)[];
|
|
23
|
+
/** The closest two range thumbs may sit, in steps. */
|
|
24
|
+
minStepsBetweenThumbs?: number;
|
|
25
|
+
/** Formats the readout, the bubble, and the spoken value. */
|
|
26
|
+
format?: (value: number) => string;
|
|
27
|
+
/** Show the value beside the label. The bubble over the thumb still appears while dragging or focused. */
|
|
28
|
+
showValue?: boolean;
|
|
29
|
+
/** Accessible names for the two range thumbs. */
|
|
30
|
+
thumbLabels?: [string, string];
|
|
31
|
+
/** Content beside the track, such as an icon or a mute button. */
|
|
32
|
+
start?: ReactNode;
|
|
33
|
+
end?: ReactNode;
|
|
34
|
+
/** Submits the value with a form, one hidden input per thumb. */
|
|
35
|
+
name?: string;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A value or a range on a track. The thumb follows the pointer 1:1, rubber-bands past its limits, and settles on the step grid with
|
|
41
|
+
* the velocity of the release; pressing the track springs the nearest thumb there. A bubble with rolling digits rides the thumb while
|
|
42
|
+
* it is dragged or keyboard focused. Arrows, Shift+arrows, PageUp, PageDown, Home, and End work as in a native range input.
|
|
43
|
+
*/
|
|
44
|
+
export declare function Slider<T extends SliderValue = number>({ label, value, defaultValue, onValueChange, onValueCommit, min, max, step: stepProp, largeStep, marks, minStepsBetweenThumbs, format, showValue, thumbLabels, start, end, name, disabled, className }: SliderProps<T>): import("react").JSX.Element;
|
|
45
|
+
export default Slider;
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, useTransform } from "motion/react";
|
|
5
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
6
|
+
import styles from "./slider.module.css";
|
|
7
|
+
const { spring, duration, blur } = motionTokens;
|
|
8
|
+
const enterEase = [...motionTokens.ease.enter];
|
|
9
|
+
const exitEase = [...motionTokens.ease.standard];
|
|
10
|
+
const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
11
|
+
/** Motion drops velocity on time-defined springs, so anything a gesture hands off to runs the same spring written as stiffness and damping.
|
|
12
|
+
* Positions are percentages of the track, so the rest thresholds are set far below a pixel. */
|
|
13
|
+
const physical = ({ visualDuration, bounce }) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta: .002, restSpeed: .02 }; };
|
|
14
|
+
const thumbSpring = physical(spring.snappy);
|
|
15
|
+
const kickSpring = physical(spring.morph);
|
|
16
|
+
/** Pixels of rubber-band travel past a limit, the velocity of a key press at a limit, and the pixel size of a step that release momentum may choose. */
|
|
17
|
+
const STRETCH = 9, BUMP_PX = 150, SNAP_PX = 12;
|
|
18
|
+
/** iOS style resistance: travel past a limit gives less and less, and never more than `limit` pixels. */
|
|
19
|
+
const rubber = (distance, limit = STRETCH) => Math.sign(distance) * (1 - 1 / (Math.abs(distance) * .55 / limit + 1)) * limit;
|
|
20
|
+
/** Where a release is heading, from a scroll-like deceleration (rate .99), in the same units as the velocity. */
|
|
21
|
+
const project = (velocity) => velocity * .099;
|
|
22
|
+
const decimalsOf = (value) => (String(value).split(".")[1] ?? "").length;
|
|
23
|
+
const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
24
|
+
const isDigit = (char) => char >= "0" && char <= "9";
|
|
25
|
+
/** Digits are keyed by place value and the text around them by side, so $950 → $1,000 opens a column and a comma while the rest roll. */
|
|
26
|
+
function partsOf(text) {
|
|
27
|
+
const chars = [...text];
|
|
28
|
+
const first = chars.findIndex(isDigit);
|
|
29
|
+
const last = chars.length - 1 - [...chars].reverse().findIndex(isDigit);
|
|
30
|
+
let place = chars.filter(isDigit).length;
|
|
31
|
+
return chars.map((char, index) => {
|
|
32
|
+
if (first < 0 || index < first)
|
|
33
|
+
return { key: `p${index}${char}`, text: char };
|
|
34
|
+
if (index > last)
|
|
35
|
+
return { key: `s${chars.length - index}${char}`, text: char };
|
|
36
|
+
if (isDigit(char))
|
|
37
|
+
return { key: `d${--place}`, digit: Number(char) };
|
|
38
|
+
return { key: `g${place}${char}`, text: char };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/* A new column opens its width while it rises in the direction of change; a leaving one closes on a spring that never passes zero. */
|
|
42
|
+
const slot = {
|
|
43
|
+
enter: (direction) => ({ width: 0, scale: .6, opacity: 0, y: `${direction * .3}em`, filter: `blur(${blur.soft}px)` }),
|
|
44
|
+
center: { width: "auto", scale: 1, opacity: 1, y: 0, filter: "blur(0px)", transitionEnd: { filter: "none" }, transition: { width: spring.morph, scale: spring.morph, opacity: spring.morph, y: spring.snappy, filter: { duration: duration.standard, ease: enterEase } } },
|
|
45
|
+
exit: (direction) => ({ width: 0, scale: .6, opacity: 0, y: `${direction * -.3}em`, filter: `blur(${blur.subtle}px)`, transition: { width: spring.smooth, scale: spring.smooth, y: { duration: duration.fast, ease: exitEase }, opacity: { duration: duration.instant }, filter: { duration: duration.instant } } }),
|
|
46
|
+
};
|
|
47
|
+
/* Reduced motion keeps a short fade and no travel. Its resting state matches `slot`, so either branch hydrates the same markup. */
|
|
48
|
+
const still = { enter: { width: "auto", scale: 1, opacity: 0, y: 0, filter: "none" }, center: { width: "auto", scale: 1, opacity: 1, y: 0, filter: "none", transition: { duration: duration.instant } }, exit: { width: 0, opacity: 0, transition: { duration: 0 } } };
|
|
49
|
+
/** One digit on the wheel. Its distance from the wheel position sets where it sits, how clear it is, and whether it shows. */
|
|
50
|
+
function Glyph({ position, digit }) {
|
|
51
|
+
const offset = (current) => ((((digit - current) % 10) + 15) % 10) - 5;
|
|
52
|
+
const y = useTransform(position, current => `${offset(current) * 1.05}em`);
|
|
53
|
+
const opacity = useTransform(position, current => Math.max(0, 1 - Math.abs(offset(current)) ** 1.5 * 1.1));
|
|
54
|
+
const visibility = useTransform(position, current => Math.abs(offset(current)) >= 1 ? "hidden" : "visible");
|
|
55
|
+
const filter = useTransform(position, current => { const distance = Math.abs(offset(current)); return distance < .02 || distance >= 1 ? "none" : `blur(${(distance * blur.soft * .75).toFixed(2)}px)`; });
|
|
56
|
+
return _jsx(motion.span, { className: styles.glyph, style: { y, opacity, filter, visibility }, children: digit });
|
|
57
|
+
}
|
|
58
|
+
/** An odometer wheel. It turns the way the value moved, wraps 9 → 0, and retargets mid spin while a drag keeps it busy. */
|
|
59
|
+
function Wheel({ digit, direction }) {
|
|
60
|
+
const reduced = useReducedMotion();
|
|
61
|
+
const position = useMotionValue(digit);
|
|
62
|
+
const wheel = useRef({ digit, target: digit });
|
|
63
|
+
useLayoutEffect(() => {
|
|
64
|
+
const state = wheel.current;
|
|
65
|
+
if (state.digit === digit)
|
|
66
|
+
return;
|
|
67
|
+
state.target += direction > 0 ? (digit - state.digit + 10) % 10 : -((state.digit - digit + 10) % 10);
|
|
68
|
+
state.digit = digit;
|
|
69
|
+
if (reduced)
|
|
70
|
+
position.jump(state.target);
|
|
71
|
+
else
|
|
72
|
+
animate(position, state.target, thumbSpring);
|
|
73
|
+
}, [digit, direction, position, reduced]);
|
|
74
|
+
return _jsxs(_Fragment, { children: [_jsx("span", { className: styles.sizer, children: "0" }), DIGITS.map(item => _jsx(Glyph, { position: position, digit: item }, item))] });
|
|
75
|
+
}
|
|
76
|
+
/** A formatted value whose digits roll in the direction it moved and whose width follows new columns on a spring. */
|
|
77
|
+
function RollingNumber({ value, text }) {
|
|
78
|
+
const reduced = useReducedMotion();
|
|
79
|
+
const [trail, setTrail] = useState({ value, direction: 1 });
|
|
80
|
+
if (trail.value !== value)
|
|
81
|
+
setTrail({ value, direction: value > trail.value ? 1 : -1 });
|
|
82
|
+
const direction = trail.value === value ? trail.direction : value > trail.value ? 1 : -1;
|
|
83
|
+
return _jsx("span", { className: styles.number, children: _jsx(AnimatePresence, { initial: false, custom: direction, children: partsOf(text).map(part => _jsx(motion.span, { className: "digit" in part ? styles.column : styles.symbol, custom: direction, variants: reduced ? still : slot, initial: "enter", animate: "center", exit: "exit", children: "digit" in part ? _jsx(Wheel, { digit: part.digit, direction: direction }) : part.text }, part.key)) }) });
|
|
84
|
+
}
|
|
85
|
+
const bubbleIn = { opacity: 0, scale: .6, y: 6, filter: `blur(${blur.subtle}px)` };
|
|
86
|
+
const bubbleRest = { opacity: 1, scale: 1, y: 0, filter: "blur(0px)", transitionEnd: { filter: "none" } };
|
|
87
|
+
const bubbleOut = { opacity: 0, scale: .8, y: 4, filter: `blur(${blur.subtle}px)`, transition: { duration: duration.instant, ease: exitEase } };
|
|
88
|
+
const bubbleEnter = { ...spring.snappy, opacity: { duration: duration.fast, ease: enterEase }, filter: { duration: duration.fast, ease: enterEase } };
|
|
89
|
+
/** The thumb rides a layer as wide as the track, so a percentage transform places it with no measuring, on the server too. */
|
|
90
|
+
function Thumb({ quiet, index, shown, value, text, low, high, ariaLabel, labelledBy, active, lifted, bubble, disabled, onKeyDown, onFocus, onBlur }) {
|
|
91
|
+
const reduced = useReducedMotion();
|
|
92
|
+
const x = useTransform(shown, current => `${current - 100}%`);
|
|
93
|
+
return _jsxs(motion.div, { className: styles.thumbLayer, style: { x, zIndex: active ? 2 : 1 }, children: [_jsx(motion.div, { role: "slider", "data-thumb": "", "data-index": index, "data-active": lifted || undefined, "data-quiet": quiet || undefined, className: styles.thumb, tabIndex: disabled ? -1 : 0, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : labelledBy, "aria-valuemin": low, "aria-valuemax": high, "aria-valuenow": value, "aria-valuetext": text, "aria-orientation": "horizontal", "aria-disabled": disabled || undefined, initial: false, animate: { scale: lifted ? 1.16 : 1 }, transition: reduced ? { duration: 0 } : spring.snappy, onKeyDown: onKeyDown, onFocus: onFocus, onBlur: onBlur }), _jsx(motion.span, { className: styles.bubbleAnchor, style: { "--at": shown }, children: _jsx(AnimatePresence, { children: bubble && _jsx(motion.span, { className: styles.bubble, "aria-hidden": "true", initial: reduced ? { opacity: 0 } : bubbleIn, animate: bubbleRest, exit: reduced ? { opacity: 0, transition: { duration: duration.instant } } : bubbleOut, transition: reduced ? { duration: duration.instant } : bubbleEnter, children: _jsx(RollingNumber, { value: value, text: text }) }, "bubble") }) })] });
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A value or a range on a track. The thumb follows the pointer 1:1, rubber-bands past its limits, and settles on the step grid with
|
|
97
|
+
* the velocity of the release; pressing the track springs the nearest thumb there. A bubble with rolling digits rides the thumb while
|
|
98
|
+
* it is dragged or keyboard focused. Arrows, Shift+arrows, PageUp, PageDown, Home, and End work as in a native range input.
|
|
99
|
+
*/
|
|
100
|
+
export function Slider({ label, value, defaultValue, onValueChange, onValueCommit, min = 0, max = 100, step: stepProp = 1, largeStep, marks, minStepsBetweenThumbs = 0, format, showValue = true, thumbLabels, start, end, name, disabled, className }) {
|
|
101
|
+
const reduced = useReducedMotion();
|
|
102
|
+
const labelId = useId();
|
|
103
|
+
const step = stepProp > 0 ? stepProp : 1;
|
|
104
|
+
const span = max - min || 1;
|
|
105
|
+
const isRange = Array.isArray(value ?? defaultValue);
|
|
106
|
+
const toArray = (input) => input === undefined ? [min, max].slice(0, isRange ? 2 : 1) : Array.isArray(input) ? [input[0], input[1]] : [input];
|
|
107
|
+
const [internal, setInternal] = useState(() => toArray(defaultValue));
|
|
108
|
+
const values = value === undefined ? internal : toArray(value);
|
|
109
|
+
const decimals = Math.max(decimalsOf(step), decimalsOf(min));
|
|
110
|
+
const formatValue = format ?? ((input) => input.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }));
|
|
111
|
+
const gap = isRange ? minStepsBetweenThumbs * step : 0;
|
|
112
|
+
const pct = (input) => ((input - min) / span) * 100;
|
|
113
|
+
const valueAt = (percent) => min + (percent / 100) * span;
|
|
114
|
+
const snap = (input) => Number((min + Math.round((input - min) / step) * step).toFixed(decimals));
|
|
115
|
+
const lowOf = (index, current) => index === 1 ? current[0] + gap : min;
|
|
116
|
+
const highOf = (index, current) => index === 0 && isRange ? current[1] - gap : max;
|
|
117
|
+
const settle = (index, input, current) => clamp(snap(clamp(input, min, max)), lowOf(index, current), highOf(index, current));
|
|
118
|
+
const trackRef = useRef(null);
|
|
119
|
+
const thumbsRef = useRef(null);
|
|
120
|
+
const latest = useRef(values);
|
|
121
|
+
const goal = useRef(values.map(pct));
|
|
122
|
+
const drag = useRef(null);
|
|
123
|
+
const pointerFocus = useRef(false);
|
|
124
|
+
const lingerTimer = useRef(undefined);
|
|
125
|
+
const [dragging, setDragging] = useState(null);
|
|
126
|
+
const [keyFocus, setKeyFocus] = useState(null);
|
|
127
|
+
const [linger, setLinger] = useState(null);
|
|
128
|
+
const [lastActive, setLastActive] = useState(isRange ? 1 : 0);
|
|
129
|
+
const [quiet, setQuiet] = useState(null);
|
|
130
|
+
// Each thumb is a committed position plus a catch-up offset: a pressed track springs the offset to zero while the pointer moves the
|
|
131
|
+
// position 1:1, so the thumb glides to the finger and never lags behind it.
|
|
132
|
+
const pos0 = useMotionValue(pct(values[0])), pos1 = useMotionValue(pct(values[1] ?? values[0]));
|
|
133
|
+
const lag0 = useMotionValue(0), lag1 = useMotionValue(0);
|
|
134
|
+
const shown0 = useTransform(() => pos0.get() + lag0.get());
|
|
135
|
+
const shown1 = useTransform(() => pos1.get() + lag1.get());
|
|
136
|
+
const clipPath = useTransform(() => { const from = isRange ? clamp(shown0.get(), 0, 100) : 0; const to = clamp((isRange ? shown1 : shown0).get(), 0, 100); return `inset(0 ${(100 - to).toFixed(3)}% 0 ${from.toFixed(3)}% round 999px)`; });
|
|
137
|
+
const thumbs = [{ pos: pos0, lag: lag0, shown: shown0 }, { pos: pos1, lag: lag1, shown: shown1 }];
|
|
138
|
+
const emit = (next) => (isRange ? [next[0], next[1]] : next[0]);
|
|
139
|
+
function commit(index, next) {
|
|
140
|
+
const current = latest.current;
|
|
141
|
+
if (current[index] === next)
|
|
142
|
+
return;
|
|
143
|
+
const updated = current.map((item, at) => at === index ? next : item);
|
|
144
|
+
latest.current = updated;
|
|
145
|
+
if (value === undefined)
|
|
146
|
+
setInternal(updated);
|
|
147
|
+
onValueChange?.(emit(updated));
|
|
148
|
+
}
|
|
149
|
+
// Values that arrive from outside a drag (keys, props, a clicked mark) spring the thumb to their place.
|
|
150
|
+
useLayoutEffect(() => {
|
|
151
|
+
latest.current = values;
|
|
152
|
+
values.forEach((item, index) => {
|
|
153
|
+
const target = pct(item), thumb = thumbs[index];
|
|
154
|
+
if (drag.current?.index === index || goal.current[index] === target)
|
|
155
|
+
return;
|
|
156
|
+
goal.current[index] = target;
|
|
157
|
+
const from = thumb.pos.get() + thumb.lag.get();
|
|
158
|
+
thumb.lag.jump(0);
|
|
159
|
+
thumb.pos.jump(from);
|
|
160
|
+
if (reduced)
|
|
161
|
+
thumb.pos.jump(target);
|
|
162
|
+
else
|
|
163
|
+
animate(thumb.pos, target, thumbSpring);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
useEffect(() => () => window.clearTimeout(lingerTimer.current), []);
|
|
167
|
+
const thumbNode = (index) => thumbsRef.current?.querySelector(`[data-index="${index}"]`);
|
|
168
|
+
/** Focus that follows a pointer keeps the thumb quiet: no ring and no bubble until a key is pressed. */
|
|
169
|
+
function focusFromPointer(index) {
|
|
170
|
+
const node = thumbNode(index);
|
|
171
|
+
if (!node || document.activeElement === node)
|
|
172
|
+
return;
|
|
173
|
+
setQuiet(index);
|
|
174
|
+
pointerFocus.current = true;
|
|
175
|
+
node.focus({ preventScroll: true });
|
|
176
|
+
pointerFocus.current = false;
|
|
177
|
+
}
|
|
178
|
+
const trackWidth = () => trackRef.current?.getBoundingClientRect().width || 1;
|
|
179
|
+
function nearest(at, current) {
|
|
180
|
+
if (!isRange)
|
|
181
|
+
return 0;
|
|
182
|
+
const low = Math.abs(at - pct(current[0])), high = Math.abs(at - pct(current[1]));
|
|
183
|
+
return low === high ? (at < pct(current[0]) ? 0 : 1) : low < high ? 0 : 1;
|
|
184
|
+
}
|
|
185
|
+
function holdBubble(index) {
|
|
186
|
+
window.clearTimeout(lingerTimer.current);
|
|
187
|
+
setLinger(index);
|
|
188
|
+
lingerTimer.current = window.setTimeout(() => setLinger(null), 700);
|
|
189
|
+
}
|
|
190
|
+
/** Starts tracking one thumb. A press on the track springs the thumb over from where it was; a grabbed thumb keeps its grab offset. */
|
|
191
|
+
function begin(state, index, at, press) {
|
|
192
|
+
const thumb = thumbs[index];
|
|
193
|
+
const from = thumb.pos.get() + thumb.lag.get();
|
|
194
|
+
state.index = index;
|
|
195
|
+
state.grab = press ? 0 : at - from;
|
|
196
|
+
thumb.lag.jump(0);
|
|
197
|
+
thumb.pos.jump(from);
|
|
198
|
+
setDragging(index);
|
|
199
|
+
setLastActive(index);
|
|
200
|
+
window.clearTimeout(lingerTimer.current);
|
|
201
|
+
setLinger(null);
|
|
202
|
+
focusFromPointer(index);
|
|
203
|
+
if (press)
|
|
204
|
+
follow(state, at, 0, true);
|
|
205
|
+
}
|
|
206
|
+
/** Places the dragged thumb under the pointer, with resistance past its limits, and commits the stepped value beneath it. */
|
|
207
|
+
function follow(state, at, time, press = false) {
|
|
208
|
+
const index = state.index;
|
|
209
|
+
if (index === null)
|
|
210
|
+
return;
|
|
211
|
+
const thumb = thumbs[index], current = latest.current, width = trackWidth();
|
|
212
|
+
const raw = at - state.grab;
|
|
213
|
+
const low = pct(lowOf(index, current)), high = pct(highOf(index, current));
|
|
214
|
+
const edge = clamp(raw, low, high);
|
|
215
|
+
const placed = reduced ? edge : edge + (rubber(((raw - edge) / 100) * width) / width) * 100;
|
|
216
|
+
if (press && !reduced) {
|
|
217
|
+
const from = thumb.pos.get();
|
|
218
|
+
thumb.pos.jump(placed);
|
|
219
|
+
thumb.lag.jump(from - placed);
|
|
220
|
+
animate(thumb.lag, 0, thumbSpring);
|
|
221
|
+
}
|
|
222
|
+
else
|
|
223
|
+
thumb.pos.set(placed);
|
|
224
|
+
state.raw = raw;
|
|
225
|
+
state.samples.push({ t: time, p: placed });
|
|
226
|
+
if (state.samples.length > 6)
|
|
227
|
+
state.samples.shift();
|
|
228
|
+
commit(index, settle(index, valueAt(raw), current));
|
|
229
|
+
}
|
|
230
|
+
function onPointerDown(event) {
|
|
231
|
+
if (disabled || event.button !== 0 || !event.isPrimary)
|
|
232
|
+
return;
|
|
233
|
+
const rect = trackRef.current?.getBoundingClientRect();
|
|
234
|
+
if (!rect)
|
|
235
|
+
return;
|
|
236
|
+
const at = ((event.clientX - rect.left) / (rect.width || 1)) * 100;
|
|
237
|
+
const grabbed = event.target.closest("[data-thumb]");
|
|
238
|
+
const current = latest.current;
|
|
239
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
240
|
+
const state = { pointer: event.pointerId, index: null, grab: 0, x: event.clientX, raw: at, samples: [] };
|
|
241
|
+
drag.current = state;
|
|
242
|
+
// Stacked range thumbs wait for the first movement to decide which one the pointer means.
|
|
243
|
+
if (grabbed && isRange && current[0] === current[1]) {
|
|
244
|
+
focusFromPointer(Number(grabbed.dataset.index));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
begin(state, grabbed ? Number(grabbed.dataset.index) : nearest(at, current), at, !grabbed);
|
|
248
|
+
state.samples = [{ t: event.timeStamp, p: thumbs[state.index ?? 0].pos.get() }];
|
|
249
|
+
}
|
|
250
|
+
function onPointerMove(event) {
|
|
251
|
+
const state = drag.current;
|
|
252
|
+
if (!state || state.pointer !== event.pointerId)
|
|
253
|
+
return;
|
|
254
|
+
const rect = trackRef.current?.getBoundingClientRect();
|
|
255
|
+
if (!rect)
|
|
256
|
+
return;
|
|
257
|
+
const at = ((event.clientX - rect.left) / (rect.width || 1)) * 100;
|
|
258
|
+
if (state.index === null) {
|
|
259
|
+
const dx = event.clientX - state.x;
|
|
260
|
+
if (Math.abs(dx) < 2)
|
|
261
|
+
return;
|
|
262
|
+
begin(state, dx < 0 ? 0 : 1, at - (dx / (rect.width || 1)) * 100, false);
|
|
263
|
+
}
|
|
264
|
+
follow(state, at, event.timeStamp);
|
|
265
|
+
}
|
|
266
|
+
function onPointerEnd(event) {
|
|
267
|
+
const state = drag.current;
|
|
268
|
+
if (!state || state.pointer !== event.pointerId)
|
|
269
|
+
return;
|
|
270
|
+
drag.current = null;
|
|
271
|
+
const index = state.index;
|
|
272
|
+
if (index === null)
|
|
273
|
+
return;
|
|
274
|
+
const thumb = thumbs[index], current = latest.current, width = trackWidth();
|
|
275
|
+
// Velocity from the last few frames, in percent per second; a pointer that paused before letting go carries none.
|
|
276
|
+
const samples = state.samples, first = samples[0], last = samples[samples.length - 1];
|
|
277
|
+
const elapsed = last && first ? (last.t - first.t) / 1000 : 0;
|
|
278
|
+
// A release during the catch-up spring keeps that spring's speed too.
|
|
279
|
+
const velocity = (last && first && elapsed > .008 && event.timeStamp - last.t < 60 ? (last.p - first.p) / elapsed : 0) + thumb.lag.getVelocity();
|
|
280
|
+
// On a coarse grid the release momentum may carry the thumb one step further, never more.
|
|
281
|
+
const stepPct = (step / span) * 100;
|
|
282
|
+
const carry = event.type === "pointerup" && (stepPct / 100) * width >= SNAP_PX ? clamp(project(velocity), -stepPct, stepPct) : 0;
|
|
283
|
+
const next = settle(index, valueAt(state.raw + carry), current);
|
|
284
|
+
const from = thumb.pos.get() + thumb.lag.get(), target = pct(next);
|
|
285
|
+
thumb.lag.jump(0);
|
|
286
|
+
thumb.pos.jump(from);
|
|
287
|
+
goal.current[index] = target;
|
|
288
|
+
if (reduced)
|
|
289
|
+
thumb.pos.jump(target);
|
|
290
|
+
else
|
|
291
|
+
animate(thumb.pos, target, { ...thumbSpring, velocity });
|
|
292
|
+
commit(index, next);
|
|
293
|
+
setDragging(null);
|
|
294
|
+
holdBubble(index);
|
|
295
|
+
onValueCommit?.(emit(latest.current));
|
|
296
|
+
}
|
|
297
|
+
function onKeyDown(index, event) {
|
|
298
|
+
if (disabled)
|
|
299
|
+
return;
|
|
300
|
+
const current = latest.current, now = current[index];
|
|
301
|
+
const large = largeStep ?? Math.max(step, snap(min + span / 10) - min);
|
|
302
|
+
const moves = { ArrowRight: step, ArrowUp: step, ArrowLeft: -step, ArrowDown: -step, PageUp: large, PageDown: -large };
|
|
303
|
+
let wanted;
|
|
304
|
+
if (event.key === "Home")
|
|
305
|
+
wanted = lowOf(index, current);
|
|
306
|
+
else if (event.key === "End")
|
|
307
|
+
wanted = highOf(index, current);
|
|
308
|
+
else if (event.key in moves)
|
|
309
|
+
wanted = now + (event.shiftKey && /^Arrow/.test(event.key) ? Math.sign(moves[event.key]) * large : moves[event.key]);
|
|
310
|
+
else
|
|
311
|
+
return;
|
|
312
|
+
event.preventDefault();
|
|
313
|
+
setKeyFocus(index);
|
|
314
|
+
setQuiet(null);
|
|
315
|
+
setLastActive(index);
|
|
316
|
+
const next = settle(index, wanted, current);
|
|
317
|
+
// At a limit the thumb strains toward the press and springs home, so the key still answers.
|
|
318
|
+
if (next === now) {
|
|
319
|
+
const toward = Math.sign(wanted - now);
|
|
320
|
+
if (toward && !reduced)
|
|
321
|
+
animate(thumbs[index].pos, goal.current[index], { ...kickSpring, velocity: (toward * BUMP_PX / trackWidth()) * 100 });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
commit(index, next);
|
|
325
|
+
onValueCommit?.(emit(latest.current));
|
|
326
|
+
}
|
|
327
|
+
function jumpTo(target) {
|
|
328
|
+
if (disabled)
|
|
329
|
+
return;
|
|
330
|
+
const current = latest.current, index = nearest(pct(target), current);
|
|
331
|
+
const next = settle(index, target, current);
|
|
332
|
+
setLastActive(index);
|
|
333
|
+
focusFromPointer(index);
|
|
334
|
+
if (next === current[index])
|
|
335
|
+
return;
|
|
336
|
+
commit(index, next);
|
|
337
|
+
onValueCommit?.(emit(latest.current));
|
|
338
|
+
}
|
|
339
|
+
const markList = (marks ?? []).map(mark => typeof mark === "number" ? { value: mark } : mark).filter(mark => mark.value >= min && mark.value <= max);
|
|
340
|
+
const ticks = markList.filter(mark => mark.value > min && mark.value < max);
|
|
341
|
+
const labelled = markList.filter(mark => mark.label);
|
|
342
|
+
const inRange = (mark) => isRange ? mark >= values[0] && mark <= values[1] : mark <= values[0];
|
|
343
|
+
const names = thumbLabels ?? [`${label}, minimum`, `${label}, maximum`];
|
|
344
|
+
return _jsxs("div", { className: [styles.root, className].filter(Boolean).join(" "), "data-disabled": disabled || undefined, "data-dragging": dragging !== null || undefined, "data-marks": labelled.length > 0 || undefined, children: [_jsxs("div", { className: styles.header, children: [_jsx("span", { id: labelId, className: styles.label, children: label }), showValue && _jsxs("span", { className: styles.readout, "aria-hidden": "true", children: [_jsx(RollingNumber, { value: values[0], text: formatValue(values[0]) }), isRange && _jsxs(_Fragment, { children: [_jsx("span", { className: styles.dash, children: "\u2013" }), _jsx(RollingNumber, { value: values[1], text: formatValue(values[1]) })] })] })] }), _jsxs("div", { className: styles.body, children: [start && _jsx("div", { className: styles.start, children: start }), _jsxs("div", { className: styles.control, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerEnd, onPointerCancel: onPointerEnd, onLostPointerCapture: onPointerEnd, onMouseDown: event => event.preventDefault(), children: [_jsxs("div", { ref: trackRef, className: styles.track, children: [ticks.map(mark => _jsx("span", { className: styles.tick, style: { left: `${pct(mark.value)}%` } }, mark.value)), _jsx(motion.div, { className: styles.fill, style: { clipPath }, children: ticks.map(mark => _jsx("span", { className: styles.tick, style: { left: `${pct(mark.value)}%` } }, mark.value)) })] }), _jsx("div", { ref: thumbsRef, className: styles.thumbs, children: values.map((item, index) => _jsx(Thumb, { quiet: quiet === index, index: index, shown: thumbs[index].shown, value: item, text: formatValue(item), low: lowOf(index, values), high: highOf(index, values), ariaLabel: isRange ? names[index] : undefined, labelledBy: labelId, active: lastActive === index, lifted: dragging === index, bubble: dragging === index || keyFocus === index || linger === index, disabled: disabled, onKeyDown: event => onKeyDown(index, event), onFocus: event => { if (!pointerFocus.current && event.currentTarget.matches(":focus-visible"))
|
|
345
|
+
setKeyFocus(index); }, onBlur: () => { setKeyFocus(current => current === index ? null : current); setQuiet(current => current === index ? null : current); } }, index)) })] }), end && _jsx("div", { className: styles.end, children: end }), labelled.length > 0 && _jsx("div", { className: styles.marks, "aria-hidden": "true", children: labelled.map(mark => _jsx("span", { className: styles.markLabel, "data-on": inRange(mark.value) || undefined, "data-edge": mark.value === min ? "start" : mark.value === max ? "end" : undefined, style: { left: `${pct(mark.value)}%` }, onClick: () => jumpTo(mark.value), children: mark.label }, mark.value)) })] }), name && values.map((item, index) => _jsx("input", { type: "hidden", name: name, value: item, disabled: disabled }, index))] });
|
|
346
|
+
}
|
|
347
|
+
export default Slider;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* Set --slider-thumb and --slider-track to resize. The track is inset by half a thumb, so a thumb at either limit stays inside the box. */
|
|
2
|
+
.root { --slider-thumb: 22px; --slider-track: 6px; display: grid; width: 100%; min-width: 0; color: var(--foreground); font-size: var(--text-sm); line-height: var(--leading-body); letter-spacing: var(--tracking-body); user-select: none; -webkit-user-select: none; }
|
|
3
|
+
.header { display: flex; min-width: 0; align-items: baseline; justify-content: space-between; gap: var(--space-inset); }
|
|
4
|
+
.label { min-width: 0; font-weight: 500; }
|
|
5
|
+
.readout { display: inline-flex; flex: none; align-items: center; gap: .3em; color: var(--text-secondary); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
6
|
+
.dash { color: var(--text-muted); }
|
|
7
|
+
/* Adornments carry their own space, so an empty column costs nothing. */
|
|
8
|
+
.body { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; }
|
|
9
|
+
.start { display: flex; grid-area: 1 / 1; margin-right: var(--space-group); }
|
|
10
|
+
.end { display: flex; grid-area: 1 / 3; margin-left: var(--space-group); }
|
|
11
|
+
/* The whole row takes the press; vertical pans still scroll the page on touch. */
|
|
12
|
+
.control { position: relative; grid-area: 1 / 2; height: var(--control-height-md); cursor: pointer; touch-action: pan-y; -webkit-tap-highlight-color: transparent; }
|
|
13
|
+
.track { position: absolute; top: 50%; right: calc(var(--slider-thumb) / 2); left: calc(var(--slider-thumb) / 2); height: var(--slider-track); margin-top: calc(var(--slider-track) / -2); border-radius: var(--radius-full); corner-shape: round; background: var(--control-track); }
|
|
14
|
+
/* The fill spans the track and is revealed by a clip that follows the thumbs on the same frame. Ticks inside it are the "on" set, cut exactly at the thumb. */
|
|
15
|
+
.fill { position: absolute; inset: 0; border-radius: inherit; corner-shape: inherit; background: var(--control-fill); }
|
|
16
|
+
.tick { position: absolute; top: 50%; width: 4px; height: 4px; margin: -2px 0 0 -2px; border-radius: var(--radius-full); corner-shape: round; background: color-mix(in oklab, var(--foreground) 30%, transparent); }
|
|
17
|
+
.fill .tick { background: color-mix(in oklab, var(--control-glyph) 60%, transparent); }
|
|
18
|
+
.thumbs { position: absolute; inset: 0 calc(var(--slider-thumb) / 2); container-type: inline-size; pointer-events: none; }
|
|
19
|
+
/* Each layer is as wide as the track and slides by a percentage of itself; it only ever overflows to the left, which never scrolls. */
|
|
20
|
+
.thumbLayer { position: absolute; inset: 0; }
|
|
21
|
+
/* Centering uses the translate property, so Motion's scale on transform composes with it instead of replacing it. */
|
|
22
|
+
.thumb { position: absolute; top: 50%; right: 0; box-sizing: border-box; width: var(--slider-thumb); height: var(--slider-thumb); translate: 50% -50%; border: 0; border-radius: var(--radius-full); corner-shape: round; background: var(--control-thumb); box-shadow: var(--control-thumb-shadow); pointer-events: auto; cursor: grab; transition: box-shadow var(--duration-fast) var(--ease-standard); }
|
|
23
|
+
.thumb::after { content: ""; position: absolute; inset: -11px; border-radius: inherit; corner-shape: inherit; }
|
|
24
|
+
.root[data-dragging], .root[data-dragging] .control, .root[data-dragging] .thumb { cursor: grabbing; }
|
|
25
|
+
/* The bubble centers on the thumb until it would pass the component's edge, then holds there. --at is the thumb position in percent of
|
|
26
|
+
the track, written each frame, and cqw measures the track, so the clamp needs no JavaScript measuring. It floats higher on touch so a fingertip never hides it. */
|
|
27
|
+
.bubbleAnchor { --edge: calc(var(--slider-thumb) / 2); --x: calc(var(--at, 0) * 1cqw); position: absolute; right: 0; bottom: calc(50% + var(--slider-thumb) / 2 + 8px); display: flex; translate: clamp(calc(100% - var(--edge) - var(--x)), 50%, calc(100cqw + var(--edge) - var(--x))) 0; pointer-events: none; }
|
|
28
|
+
@media (pointer: coarse) { .bubbleAnchor { bottom: calc(50% + var(--slider-thumb) / 2 + 18px); } }
|
|
29
|
+
.bubble { display: inline-flex; min-height: 1.75rem; align-items: center; padding: 0 var(--space-group); border: 1px solid color-mix(in oklab, var(--background) 14%, var(--foreground)); border-radius: var(--radius-full); corner-shape: round; background: var(--foreground); color: var(--background); box-shadow: var(--shadow-raised); font-size: var(--text-sm); font-weight: 500; white-space: nowrap; transform-origin: 50% 100%; }
|
|
30
|
+
/* Mark labels sit on their ticks; one at a limit keeps its outer edge inside the component. */
|
|
31
|
+
.marks { position: relative; grid-area: 2 / 2; height: 1.25rem; margin: calc(var(--space-tight) * -1) calc(var(--slider-thumb) / 2) 0; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
|
|
32
|
+
.markLabel { position: absolute; top: 0; translate: -50% 0; white-space: nowrap; cursor: pointer; transition: color var(--duration-fast) var(--ease-standard); }
|
|
33
|
+
.markLabel[data-edge="start"] { translate: max(-50%, calc(var(--slider-thumb) / -2)) 0; }
|
|
34
|
+
.markLabel[data-edge="end"] { translate: min(-50%, calc(-100% + var(--slider-thumb) / 2)) 0; }
|
|
35
|
+
.markLabel[data-on] { color: var(--text-secondary); }
|
|
36
|
+
@media (hover: hover) and (pointer: fine) { .markLabel:hover { color: var(--foreground); } }
|
|
37
|
+
.root[data-disabled] { opacity: .5; }
|
|
38
|
+
.root[data-disabled] .control, .root[data-disabled] .thumb, .root[data-disabled] .markLabel { cursor: not-allowed; }
|
|
39
|
+
/* Rolling digits: each is a wheel whose window reaches a little past the line box and feathers out, so a turning digit fades at the edge. */
|
|
40
|
+
.number { display: inline-flex; align-items: center; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
41
|
+
.column { --feather: .2em; position: relative; display: inline-block; overflow-x: visible; overflow-y: clip; margin-block: calc(var(--feather) * -1); padding-block: var(--feather); mask-image: linear-gradient(to bottom, transparent, #000 calc(var(--feather) * 1.5), #000 calc(100% - var(--feather) * 1.5), transparent); }
|
|
42
|
+
.sizer { visibility: hidden; }
|
|
43
|
+
.glyph { position: absolute; inset: var(--feather) 0; text-align: center; }
|
|
44
|
+
.symbol { display: inline-block; overflow-x: clip; white-space: pre; }
|
|
45
|
+
@media (prefers-reduced-motion: reduce) { .thumb, .markLabel { transition: none; } }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Ref } from "react";
|
|
2
|
+
export interface SlopeItem {
|
|
3
|
+
/** Stable identity. An item keeps its line across datasets, so switching morphs it. */
|
|
4
|
+
key: string;
|
|
5
|
+
/** Name beside the start value, in the tooltip, and in the table. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Value in the first period. */
|
|
8
|
+
start: number;
|
|
9
|
+
/** Value in the second period. */
|
|
10
|
+
end: number;
|
|
11
|
+
}
|
|
12
|
+
/** Use a slope chart to show how several items changed between exactly two moments, and how their order changed. */
|
|
13
|
+
export interface SlopeChartProps {
|
|
14
|
+
data: SlopeItem[];
|
|
15
|
+
/** What is measured, such as "Conversion rate by channel". Names the chart for assistive technology. */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Heading of the first column, such as "Q1". */
|
|
18
|
+
startLabel: string;
|
|
19
|
+
/** Heading of the second column, such as "Q2". */
|
|
20
|
+
endLabel: string;
|
|
21
|
+
formatValue?: (value: number) => string;
|
|
22
|
+
/** Formats the change in the tooltip. Defaults to the difference in formatValue's terms. */
|
|
23
|
+
formatChange?: (change: number, item: SlopeItem) => string;
|
|
24
|
+
/** Plot height in pixels. Defaults to 44 pixels per item. */
|
|
25
|
+
height?: number;
|
|
26
|
+
/** The item drawn in the accent, such as the one the story is about. */
|
|
27
|
+
highlightKey?: string | null;
|
|
28
|
+
/** Controlled item in focus. The others fade while one is in focus. */
|
|
29
|
+
activeKey?: string | null;
|
|
30
|
+
onActiveChange?: (key: string | null) => void;
|
|
31
|
+
/** Rank movement beside each end value. */
|
|
32
|
+
ranks?: boolean;
|
|
33
|
+
emptyLabel?: string;
|
|
34
|
+
ref?: Ref<HTMLElement>;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function SlopeChart({ data, label, startLabel, endLabel, formatValue, formatChange, height, highlightKey, activeKey, onActiveChange, ranks, emptyLabel, ref, className }: SlopeChartProps): import("react").JSX.Element;
|
|
38
|
+
export default SlopeChart;
|