@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,201 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
position: relative;
|
|
3
|
+
width: 100%;
|
|
4
|
+
container-type: inline-size;
|
|
5
|
+
color: var(--foreground);
|
|
6
|
+
font-family: var(--font-body);
|
|
7
|
+
letter-spacing: var(--tracking-body);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* The writing surface. Everything inside is plain semantic HTML, styled here so pasted and typed content look the same. */
|
|
11
|
+
.editor {
|
|
12
|
+
min-height: 8rem;
|
|
13
|
+
padding: 2px 0;
|
|
14
|
+
outline: none;
|
|
15
|
+
color: var(--foreground);
|
|
16
|
+
font-size: var(--text-base);
|
|
17
|
+
line-height: 1.65;
|
|
18
|
+
overflow-wrap: break-word;
|
|
19
|
+
white-space: pre-wrap;
|
|
20
|
+
word-break: break-word;
|
|
21
|
+
caret-color: var(--accent);
|
|
22
|
+
}
|
|
23
|
+
.editor[contenteditable="false"] { caret-color: transparent; }
|
|
24
|
+
.editor::selection,
|
|
25
|
+
.editor *::selection { background: color-mix(in oklab, var(--accent) 22%, transparent); }
|
|
26
|
+
|
|
27
|
+
.editor > * { margin: 0; }
|
|
28
|
+
.editor > * + * { margin-top: .6em; }
|
|
29
|
+
.editor p { margin: 0; }
|
|
30
|
+
.editor h1,
|
|
31
|
+
.editor h2,
|
|
32
|
+
.editor h3 { font-weight: 500; letter-spacing: -.02em; line-height: 1.25; }
|
|
33
|
+
.editor h1 { font-size: var(--text-2xl); }
|
|
34
|
+
.editor h2 { font-size: var(--text-xl); }
|
|
35
|
+
.editor h3 { font-size: var(--text-lg); }
|
|
36
|
+
.editor > :is(h1, h2, h3) { margin-top: 1.1em; }
|
|
37
|
+
.editor > :is(h1, h2, h3):first-child { margin-top: 0; }
|
|
38
|
+
.editor > :is(h1, h2, h3) + * { margin-top: .45em; }
|
|
39
|
+
|
|
40
|
+
.editor strong { font-weight: 600; }
|
|
41
|
+
.editor s { color: var(--text-secondary); text-decoration-thickness: 1px; }
|
|
42
|
+
.editor a { color: var(--accent-strong); text-decoration: underline; text-decoration-color: color-mix(in oklab, var(--accent) 45%, transparent); text-decoration-thickness: 1px; text-underline-offset: 3px; cursor: text; }
|
|
43
|
+
.editor code {
|
|
44
|
+
padding: .12em .38em;
|
|
45
|
+
border-radius: var(--radius-detail);
|
|
46
|
+
background: var(--surface-muted);
|
|
47
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
48
|
+
font-size: .86em;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.editor blockquote { padding-left: 14px; border-left: 2px solid var(--border-strong); color: var(--text-secondary); }
|
|
52
|
+
|
|
53
|
+
.editor ul,
|
|
54
|
+
.editor ol { margin-block: 0; padding-left: 1.45em; }
|
|
55
|
+
/* Set the markers explicitly: app resets that strip list styles would otherwise leave lists unmarked. */
|
|
56
|
+
.editor ul { list-style: disc; }
|
|
57
|
+
.editor ul ul { list-style: circle; }
|
|
58
|
+
.editor ol { list-style: decimal; }
|
|
59
|
+
.editor li { padding-left: .2em; }
|
|
60
|
+
.editor li + li { margin-top: .2em; }
|
|
61
|
+
.editor li > :is(ul, ol) { margin-top: .2em; }
|
|
62
|
+
.editor li::marker { color: var(--text-muted); }
|
|
63
|
+
.editor ol > li::marker { font-variant-numeric: tabular-nums; }
|
|
64
|
+
|
|
65
|
+
.editor pre {
|
|
66
|
+
padding: 12px 14px;
|
|
67
|
+
overflow-x: auto;
|
|
68
|
+
border: 1px solid var(--border-subtle);
|
|
69
|
+
border-radius: var(--radius-control);
|
|
70
|
+
background: var(--surface-muted);
|
|
71
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
72
|
+
font-size: var(--text-sm);
|
|
73
|
+
line-height: 1.6;
|
|
74
|
+
tab-size: 2;
|
|
75
|
+
white-space: pre;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.editor hr { height: 1px; margin-block: 1.1em; border: 0; background: var(--border); }
|
|
79
|
+
|
|
80
|
+
/* Placeholders: the whole document when empty, and a quiet hint on the empty line holding the caret. */
|
|
81
|
+
.editor[data-empty] > :first-child::before,
|
|
82
|
+
.editor:focus [data-hint]::before {
|
|
83
|
+
float: left;
|
|
84
|
+
height: 0;
|
|
85
|
+
color: var(--text-muted);
|
|
86
|
+
pointer-events: none;
|
|
87
|
+
content: var(--placeholder);
|
|
88
|
+
}
|
|
89
|
+
.editor:focus:not([data-empty]) [data-hint]::before { content: var(--hint); }
|
|
90
|
+
|
|
91
|
+
/* Floating toolbar. One surface whose size springs between its two faces. */
|
|
92
|
+
.toolbar {
|
|
93
|
+
position: absolute;
|
|
94
|
+
z-index: 40;
|
|
95
|
+
translate: -50% -100%;
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
border: 1px solid var(--border);
|
|
98
|
+
border-radius: var(--radius-control);
|
|
99
|
+
background: var(--surface-raised);
|
|
100
|
+
box-shadow: var(--shadow-floating);
|
|
101
|
+
}
|
|
102
|
+
.toolbar[data-below] { translate: -50% 0; }
|
|
103
|
+
.face { position: absolute; top: 0; left: 0; width: max-content; }
|
|
104
|
+
.face[inert] { pointer-events: none; }
|
|
105
|
+
|
|
106
|
+
.row,
|
|
107
|
+
.linkRow { display: flex; align-items: center; gap: 2px; padding: 4px; }
|
|
108
|
+
.blockTools { display: flex; align-items: center; gap: 2px; }
|
|
109
|
+
/* Narrow editors keep the inline marks; block types stay one slash away. */
|
|
110
|
+
@container (max-width: 330px) { .blockTools { display: none; } }
|
|
111
|
+
.divider { width: 1px; height: 18px; margin: 0 4px; background: var(--border); }
|
|
112
|
+
|
|
113
|
+
.tool {
|
|
114
|
+
display: grid;
|
|
115
|
+
width: 32px;
|
|
116
|
+
height: 32px;
|
|
117
|
+
flex: none;
|
|
118
|
+
place-items: center;
|
|
119
|
+
padding: 0;
|
|
120
|
+
border: 0;
|
|
121
|
+
border-radius: var(--radius-detail);
|
|
122
|
+
background: transparent;
|
|
123
|
+
color: var(--text-secondary);
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
-webkit-tap-highlight-color: transparent;
|
|
126
|
+
transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
|
|
127
|
+
}
|
|
128
|
+
.tool:active { transform: scale(.92); }
|
|
129
|
+
.tool[aria-pressed="true"] { background: var(--accent-subtle); color: var(--accent-strong); }
|
|
130
|
+
.tool:focus-visible { background: var(--surface-muted); color: var(--foreground); }
|
|
131
|
+
@media (hover: hover) and (pointer: fine) { .tool:hover { background: var(--surface-muted); color: var(--foreground); } .tool[aria-pressed="true"]:hover { background: var(--accent-subtle); color: var(--accent-strong); } }
|
|
132
|
+
|
|
133
|
+
.linkInput {
|
|
134
|
+
width: min(15rem, 60vw);
|
|
135
|
+
height: 32px;
|
|
136
|
+
min-width: 0;
|
|
137
|
+
padding: 0 8px;
|
|
138
|
+
border: 0;
|
|
139
|
+
background: transparent;
|
|
140
|
+
color: var(--foreground);
|
|
141
|
+
font: inherit;
|
|
142
|
+
font-size: var(--text-sm);
|
|
143
|
+
outline: none;
|
|
144
|
+
}
|
|
145
|
+
.linkInput::placeholder { color: var(--text-muted); }
|
|
146
|
+
.apply {
|
|
147
|
+
display: grid;
|
|
148
|
+
width: 32px;
|
|
149
|
+
height: 32px;
|
|
150
|
+
flex: none;
|
|
151
|
+
place-items: center;
|
|
152
|
+
padding: 0;
|
|
153
|
+
border: 0;
|
|
154
|
+
border-radius: var(--radius-detail);
|
|
155
|
+
background: var(--surface-muted);
|
|
156
|
+
color: var(--text-muted);
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
transition: background-color var(--duration-standard) var(--ease-standard), color var(--duration-standard) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
|
|
159
|
+
}
|
|
160
|
+
.apply[data-ready] { background: var(--accent); color: var(--accent-foreground); }
|
|
161
|
+
.apply:active { transform: scale(.92); }
|
|
162
|
+
|
|
163
|
+
/* Slash menu. */
|
|
164
|
+
.slash {
|
|
165
|
+
position: absolute;
|
|
166
|
+
z-index: 40;
|
|
167
|
+
width: 240px;
|
|
168
|
+
max-width: calc(100% - 8px);
|
|
169
|
+
overflow-x: hidden;
|
|
170
|
+
overflow-y: auto;
|
|
171
|
+
overscroll-behavior: contain;
|
|
172
|
+
scrollbar-width: none;
|
|
173
|
+
border: 1px solid var(--border);
|
|
174
|
+
border-radius: var(--radius-control);
|
|
175
|
+
background: var(--surface-raised);
|
|
176
|
+
box-shadow: var(--shadow-floating);
|
|
177
|
+
}
|
|
178
|
+
.slash[data-above] { translate: 0 -100%; }
|
|
179
|
+
.slashBody { padding: 6px; }
|
|
180
|
+
.slashList { position: relative; display: grid; margin: 0; padding: 0; list-style: none; }
|
|
181
|
+
.slashHighlight { position: absolute; top: 0; right: 0; left: 0; border-radius: var(--radius-control); background: color-mix(in oklab, var(--foreground) 6.5%, transparent); pointer-events: none; }
|
|
182
|
+
.slashOption {
|
|
183
|
+
position: relative;
|
|
184
|
+
display: flex;
|
|
185
|
+
height: 34px;
|
|
186
|
+
align-items: center;
|
|
187
|
+
gap: 10px;
|
|
188
|
+
padding: 0 10px;
|
|
189
|
+
border-radius: var(--radius-control);
|
|
190
|
+
color: var(--text-secondary);
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
font-size: var(--text-sm);
|
|
193
|
+
-webkit-tap-highlight-color: transparent;
|
|
194
|
+
transition: color var(--duration-fast) var(--ease-standard);
|
|
195
|
+
}
|
|
196
|
+
.slashOption[aria-selected="true"] { color: var(--foreground); }
|
|
197
|
+
.slashIcon { display: grid; width: 20px; flex: none; place-items: center; }
|
|
198
|
+
.slashLabel { min-width: 0; flex: 1; overflow: hidden; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
|
199
|
+
.slashHint { flex: none; color: var(--text-muted); font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; font-size: var(--text-xs); }
|
|
200
|
+
|
|
201
|
+
.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Ref } from "react";
|
|
2
|
+
export interface RidgelineSeries {
|
|
3
|
+
/** Stable identity. A ridge that survives a data change morphs into its new shape. */
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** Raw observations. The ridge is a smoothed density of these values. */
|
|
7
|
+
values: number[];
|
|
8
|
+
}
|
|
9
|
+
/** Use a ridgeline to compare the shape of many distributions at once, such as daily temperatures by month or response times by region. */
|
|
10
|
+
export interface RidgelineProps {
|
|
11
|
+
/** One ridge per series, drawn top to bottom. */
|
|
12
|
+
series: RidgelineSeries[];
|
|
13
|
+
/** What is measured, such as "Daily highs in Zurich". Names the chart for assistive technology. */
|
|
14
|
+
label: string;
|
|
15
|
+
/** Unit after each value, such as "°C". */
|
|
16
|
+
unit?: string;
|
|
17
|
+
formatValue?: (value: number) => string;
|
|
18
|
+
/** Value range of the axis. Defaults to the data with a little room on each side. */
|
|
19
|
+
domain?: [number, number];
|
|
20
|
+
/** How far a ridge may rise into the rows above, in row heights. */
|
|
21
|
+
overlap?: number;
|
|
22
|
+
/** Height of one row in pixels. */
|
|
23
|
+
rowHeight?: number;
|
|
24
|
+
/** Smoothing in value units. Defaults to Silverman's rule per series. */
|
|
25
|
+
bandwidth?: number;
|
|
26
|
+
/** Shades each ridge by its median, from a light tint to the full first series color, and shows the scale under the axis. */
|
|
27
|
+
tint?: boolean;
|
|
28
|
+
/** Controlled id of the lifted ridge. */
|
|
29
|
+
active?: string | null;
|
|
30
|
+
defaultActive?: string | null;
|
|
31
|
+
onActiveChange?: (id: string | null) => void;
|
|
32
|
+
emptyLabel?: string;
|
|
33
|
+
ref?: Ref<HTMLElement>;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function Ridgeline({ series, label, unit, formatValue, domain, overlap, rowHeight, bandwidth, tint, active, defaultActive, onActiveChange, emptyLabel, ref, className }: RidgelineProps): import("react").JSX.Element;
|
|
37
|
+
export default Ridgeline;
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { animate, motion, useInView, useMotionValue, useReducedMotion, useSpring } from "motion/react";
|
|
5
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
6
|
+
import styles from "./ridgeline.module.css";
|
|
7
|
+
const { spring } = motionTokens;
|
|
8
|
+
const physical = ({ visualDuration, bounce }, restDelta = .0005) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta, restSpeed: restDelta * 2 }; };
|
|
9
|
+
const morph = physical({ visualDuration: .7, bounce: .08 });
|
|
10
|
+
const reveal = physical({ visualDuration: 1, bounce: 0 });
|
|
11
|
+
const glide = physical(spring.snappy, .01);
|
|
12
|
+
const follow = { stiffness: glide.stiffness, damping: glide.damping, restDelta: .01 };
|
|
13
|
+
const SAMPLES = 96, AXIS = 28, TIP = 14, LIFT = 6;
|
|
14
|
+
const grouped = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });
|
|
15
|
+
const percent = new Intl.NumberFormat("en-US", { style: "percent", maximumFractionDigits: 0 });
|
|
16
|
+
const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
17
|
+
const mix = (a, b, t) => a + (b - a) * t;
|
|
18
|
+
function quantile(sorted, q) {
|
|
19
|
+
if (!sorted.length)
|
|
20
|
+
return 0;
|
|
21
|
+
const at = (sorted.length - 1) * q, low = Math.floor(at), high = Math.ceil(at);
|
|
22
|
+
return sorted[low] + (sorted[high] - sorted[low]) * (at - low);
|
|
23
|
+
}
|
|
24
|
+
function statsOf(values) {
|
|
25
|
+
const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
|
|
26
|
+
return { sorted, n: sorted.length, min: sorted[0] ?? 0, max: sorted[sorted.length - 1] ?? 0, q1: quantile(sorted, .25), median: quantile(sorted, .5), q3: quantile(sorted, .75) };
|
|
27
|
+
}
|
|
28
|
+
function silverman(stats) {
|
|
29
|
+
if (stats.n < 2)
|
|
30
|
+
return 1;
|
|
31
|
+
const mean = stats.sorted.reduce((sum, value) => sum + value, 0) / stats.n;
|
|
32
|
+
const sd = Math.sqrt(stats.sorted.reduce((sum, value) => sum + (value - mean) ** 2, 0) / (stats.n - 1));
|
|
33
|
+
const spread = Math.min(sd, (stats.q3 - stats.q1) / 1.34 || sd);
|
|
34
|
+
return Math.max(1e-6, .9 * (spread || 1) * Math.pow(stats.n, -.2));
|
|
35
|
+
}
|
|
36
|
+
function niceStep(span, count) {
|
|
37
|
+
const raw = span / Math.max(1, count) || 1, power = Math.pow(10, Math.floor(Math.log10(raw))), unit = raw / power;
|
|
38
|
+
return (unit >= 5 ? 5 : unit >= 2 ? 2 : 1) * power;
|
|
39
|
+
}
|
|
40
|
+
/** Samples a Gaussian kernel density across the domain. */
|
|
41
|
+
function density(stats, lo, hi, bandwidth) {
|
|
42
|
+
const xs = [], ys = [];
|
|
43
|
+
const norm = 1 / (Math.max(1, stats.n) * bandwidth * Math.sqrt(2 * Math.PI));
|
|
44
|
+
for (let i = 0; i < SAMPLES; i++) {
|
|
45
|
+
const x = lo + ((hi - lo) * i) / (SAMPLES - 1);
|
|
46
|
+
let sum = 0;
|
|
47
|
+
for (const value of stats.sorted) {
|
|
48
|
+
const u = (x - value) / bandwidth;
|
|
49
|
+
if (u > -5 && u < 5)
|
|
50
|
+
sum += Math.exp(-.5 * u * u);
|
|
51
|
+
}
|
|
52
|
+
xs.push(x);
|
|
53
|
+
ys.push(sum * norm);
|
|
54
|
+
}
|
|
55
|
+
return { xs, ys };
|
|
56
|
+
}
|
|
57
|
+
const heightAt = (shape, x) => {
|
|
58
|
+
const { xs, ys } = shape;
|
|
59
|
+
if (x <= xs[0] || x >= xs[xs.length - 1])
|
|
60
|
+
return 0;
|
|
61
|
+
let i = 1;
|
|
62
|
+
while (i < xs.length - 1 && xs[i] < x)
|
|
63
|
+
i++;
|
|
64
|
+
const t = (x - xs[i - 1]) / (xs[i] - xs[i - 1] || 1);
|
|
65
|
+
return mix(ys[i - 1], ys[i], t);
|
|
66
|
+
};
|
|
67
|
+
const subscribeNothing = () => () => { };
|
|
68
|
+
function useReducedMotionSafe() {
|
|
69
|
+
const hydrated = useSyncExternalStore(subscribeNothing, () => true, () => false);
|
|
70
|
+
return !!useReducedMotion() && hydrated;
|
|
71
|
+
}
|
|
72
|
+
export function Ridgeline({ series, label, unit = "", formatValue, domain, overlap = 2.4, rowHeight = 30, bandwidth, tint = true, active, defaultActive = null, onActiveChange, emptyLabel = "No data yet", ref, className }) {
|
|
73
|
+
const reduced = useReducedMotionSafe();
|
|
74
|
+
const figure = useRef(null);
|
|
75
|
+
const plot = useRef(null);
|
|
76
|
+
const tip = useRef(null);
|
|
77
|
+
const fills = useRef(new Map());
|
|
78
|
+
const lines = useRef(new Map());
|
|
79
|
+
const medians = useRef(new Map());
|
|
80
|
+
const lifted = useRef({ fill: null, band: null, line: null, median: null, group: null });
|
|
81
|
+
const tickRefs = useRef(new Map());
|
|
82
|
+
const cross = useRef(null);
|
|
83
|
+
useImperativeHandle(ref, () => figure.current);
|
|
84
|
+
const inView = useInView(figure, { once: true, amount: .3 });
|
|
85
|
+
// A true minus sign keeps negative values tight and lined up with the tabular digits.
|
|
86
|
+
const format = (value) => formatValue ? formatValue(value) : `${grouped.format(value).replace("-", "\u2212")}${unit}`;
|
|
87
|
+
const [width, setWidth] = useState(0);
|
|
88
|
+
useLayoutEffect(() => {
|
|
89
|
+
const node = plot.current;
|
|
90
|
+
if (!node || typeof ResizeObserver === "undefined")
|
|
91
|
+
return;
|
|
92
|
+
const observer = new ResizeObserver(() => setWidth(node.clientWidth));
|
|
93
|
+
observer.observe(node);
|
|
94
|
+
setWidth(node.clientWidth);
|
|
95
|
+
return () => observer.disconnect();
|
|
96
|
+
}, []);
|
|
97
|
+
// Targets: one density per series on a shared domain, scaled so the tallest peak of all rises `overlap` rows.
|
|
98
|
+
const signature = JSON.stringify([series, domain, bandwidth]);
|
|
99
|
+
const model = useMemo(() => {
|
|
100
|
+
const stats = series.map(entry => statsOf(entry.values));
|
|
101
|
+
const widths = stats.map(entry => bandwidth ?? silverman(entry));
|
|
102
|
+
const filled = stats.map((entry, index) => ({ entry, room: 2.5 * widths[index] })).filter(({ entry }) => entry.n);
|
|
103
|
+
let lo = domain?.[0] ?? Math.min(...filled.map(({ entry, room }) => entry.min - room));
|
|
104
|
+
let hi = domain?.[1] ?? Math.max(...filled.map(({ entry, room }) => entry.max + room));
|
|
105
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi) || lo === hi) {
|
|
106
|
+
lo = 0;
|
|
107
|
+
hi = 1;
|
|
108
|
+
}
|
|
109
|
+
if (!domain) {
|
|
110
|
+
const step = niceStep(hi - lo, 5);
|
|
111
|
+
lo = Math.floor(lo / step) * step;
|
|
112
|
+
hi = Math.ceil(hi / step) * step;
|
|
113
|
+
}
|
|
114
|
+
const curves = stats.map((entry, index) => entry.n ? density(entry, lo, hi, widths[index]) : { xs: Array.from({ length: SAMPLES }, (_, i) => lo + ((hi - lo) * i) / (SAMPLES - 1)), ys: new Array(SAMPLES).fill(0) });
|
|
115
|
+
const peak = Math.max(1e-9, ...curves.flatMap(curve => curve.ys));
|
|
116
|
+
const shapes = new Map(series.map((entry, index) => { const curve = curves[index], stat = stats[index]; return [entry.id, { xs: curve.xs, ys: curve.ys.map(y => y / peak), row: index, q1: stat.q1, median: stat.median, q3: stat.q3 }]; }));
|
|
117
|
+
return { stats: new Map(series.map((entry, index) => [entry.id, stats[index]])), shapes, lo, hi };
|
|
118
|
+
}, [signature]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
119
|
+
const [ownActive, setOwnActive] = useState(defaultActive);
|
|
120
|
+
const current = active !== undefined ? active : ownActive;
|
|
121
|
+
const setCurrent = (id) => { if (active === undefined)
|
|
122
|
+
setOwnActive(id); if (id !== current)
|
|
123
|
+
onActiveChange?.(id); };
|
|
124
|
+
const [cursor, setCursor] = useState(null);
|
|
125
|
+
const narrow = width > 0 && width < 420;
|
|
126
|
+
const gutter = narrow ? 36 : 52;
|
|
127
|
+
const top = Math.ceil((overlap - 1) * rowHeight) + LIFT + 8;
|
|
128
|
+
const height = top + series.length * rowHeight + AXIS;
|
|
129
|
+
const plotWidth = Math.max(40, width - gutter - 4);
|
|
130
|
+
const baselineOf = (row) => top + (row + 1) * rowHeight;
|
|
131
|
+
// Drawn geometry lives outside React; a spring interpolates it from what is on screen to the new targets.
|
|
132
|
+
const drawn = useRef({ shapes: new Map(), lo: 0, hi: 1 });
|
|
133
|
+
const live = useRef({ gutter, plotWidth, rowHeight, overlap, top, current });
|
|
134
|
+
useLayoutEffect(() => { live.current = { gutter, plotWidth, rowHeight, overlap, top, current }; });
|
|
135
|
+
const xOf = (value) => { const d = drawn.current, L = live.current; return L.gutter + ((value - d.lo) / (d.hi - d.lo || 1)) * L.plotWidth; };
|
|
136
|
+
const paint = () => {
|
|
137
|
+
const d = drawn.current, L = live.current;
|
|
138
|
+
const sx = (value) => L.gutter + ((value - d.lo) / (d.hi - d.lo || 1)) * L.plotWidth;
|
|
139
|
+
const lift = L.rowHeight * L.overlap;
|
|
140
|
+
const curvePath = (shape, closed, from = -Infinity, to = Infinity) => {
|
|
141
|
+
const base = L.top + (shape.row + 1) * L.rowHeight;
|
|
142
|
+
const points = [];
|
|
143
|
+
const push = (x, y) => points.push(`${sx(x).toFixed(2)},${(base - y * lift).toFixed(2)}`);
|
|
144
|
+
if (from > -Infinity)
|
|
145
|
+
push(from, heightAt(shape, from));
|
|
146
|
+
shape.xs.forEach((x, i) => { if (x > from && x < to)
|
|
147
|
+
push(x, shape.ys[i]); });
|
|
148
|
+
if (to < Infinity)
|
|
149
|
+
push(to, heightAt(shape, to));
|
|
150
|
+
if (!points.length)
|
|
151
|
+
return "";
|
|
152
|
+
const first = from > -Infinity ? from : shape.xs[0], last = to < Infinity ? to : shape.xs[shape.xs.length - 1];
|
|
153
|
+
return closed ? `M${sx(first).toFixed(2)},${base}L${points.join("L")}L${sx(last).toFixed(2)},${base}Z` : `M${points.join("L")}`;
|
|
154
|
+
};
|
|
155
|
+
for (const [id, shape] of d.shapes) {
|
|
156
|
+
const fill = fills.current.get(id), line = lines.current.get(id), median = medians.current.get(id);
|
|
157
|
+
fill?.setAttribute("d", curvePath(shape, true));
|
|
158
|
+
line?.setAttribute("d", curvePath(shape, false));
|
|
159
|
+
if (median) {
|
|
160
|
+
const x = sx(shape.median), base = L.top + (shape.row + 1) * L.rowHeight;
|
|
161
|
+
median.setAttribute("x1", x.toFixed(2));
|
|
162
|
+
median.setAttribute("x2", x.toFixed(2));
|
|
163
|
+
median.setAttribute("y1", String(base));
|
|
164
|
+
median.setAttribute("y2", (base - heightAt(shape, shape.median) * lift).toFixed(2));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const shape = L.current ? d.shapes.get(L.current) : undefined, lens = lifted.current;
|
|
168
|
+
if (shape && lens.fill && lens.line && lens.band && lens.median) {
|
|
169
|
+
lens.fill.setAttribute("d", curvePath(shape, true));
|
|
170
|
+
lens.line.setAttribute("d", curvePath(shape, false));
|
|
171
|
+
lens.band.setAttribute("d", curvePath(shape, true, shape.q1, shape.q3));
|
|
172
|
+
const x = sx(shape.median), base = L.top + (shape.row + 1) * L.rowHeight;
|
|
173
|
+
lens.median.setAttribute("x1", x.toFixed(2));
|
|
174
|
+
lens.median.setAttribute("x2", x.toFixed(2));
|
|
175
|
+
lens.median.setAttribute("y1", String(base));
|
|
176
|
+
lens.median.setAttribute("y2", (base - heightAt(shape, shape.median) * lift).toFixed(2));
|
|
177
|
+
}
|
|
178
|
+
for (const [value, tick] of tickRefs.current)
|
|
179
|
+
tick.style.transform = `translate3d(${sx(value).toFixed(2)}px, 0, 0)`;
|
|
180
|
+
};
|
|
181
|
+
const paintRef = useRef(paint);
|
|
182
|
+
useLayoutEffect(() => { paintRef.current = paint; });
|
|
183
|
+
const shown = inView || reduced;
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!shown)
|
|
186
|
+
return;
|
|
187
|
+
const d = drawn.current;
|
|
188
|
+
const first = d.shapes.size === 0;
|
|
189
|
+
const from = new Map();
|
|
190
|
+
for (const [id, target] of model.shapes) {
|
|
191
|
+
const now = d.shapes.get(id);
|
|
192
|
+
from.set(id, now ? { ...now, xs: [...now.xs], ys: [...now.ys] } : { ...target, ys: target.ys.map(() => 0), q1: target.median, q3: target.median });
|
|
193
|
+
}
|
|
194
|
+
const fromLo = first ? model.lo : d.lo, fromHi = first ? model.hi : d.hi;
|
|
195
|
+
const finish = () => { d.shapes = new Map([...model.shapes].map(([id, shape]) => [id, { ...shape }])); d.lo = model.lo; d.hi = model.hi; paintRef.current(); };
|
|
196
|
+
if (reduced) {
|
|
197
|
+
finish();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
d.shapes = new Map([...from].map(([id, shape]) => [id, { ...shape, xs: [...shape.xs], ys: [...shape.ys] }]));
|
|
201
|
+
const controls = animate(0, 1, { ...(first ? reveal : morph), onUpdate: t => {
|
|
202
|
+
d.lo = mix(fromLo, model.lo, t);
|
|
203
|
+
d.hi = mix(fromHi, model.hi, t);
|
|
204
|
+
for (const [id, to] of model.shapes) {
|
|
205
|
+
const f = from.get(id), out = d.shapes.get(id);
|
|
206
|
+
// Stagger the reveal a little from the top row down, so the ridges roll in like a wave.
|
|
207
|
+
const local = first ? clamp((t - to.row * .025) / (1 - Math.min(.5, model.shapes.size * .025)), 0, 1) : t;
|
|
208
|
+
for (let i = 0; i < SAMPLES; i++) {
|
|
209
|
+
out.xs[i] = mix(f.xs[i], to.xs[i], local);
|
|
210
|
+
out.ys[i] = mix(f.ys[i], to.ys[i], local);
|
|
211
|
+
}
|
|
212
|
+
out.row = mix(f.row, to.row, local);
|
|
213
|
+
out.q1 = mix(f.q1, to.q1, local);
|
|
214
|
+
out.median = mix(f.median, to.median, local);
|
|
215
|
+
out.q3 = mix(f.q3, to.q3, local);
|
|
216
|
+
}
|
|
217
|
+
paintRef.current();
|
|
218
|
+
}, onComplete: finish });
|
|
219
|
+
return () => controls.stop();
|
|
220
|
+
}, [shown, model, reduced]);
|
|
221
|
+
useLayoutEffect(() => { paintRef.current(); });
|
|
222
|
+
// Axis ticks for the target domain; their positions follow the drawn domain every frame.
|
|
223
|
+
const tickCount = Math.max(2, Math.floor(plotWidth / 72));
|
|
224
|
+
const step = niceStep(model.hi - model.lo, tickCount);
|
|
225
|
+
const ticks = [];
|
|
226
|
+
for (let value = Math.ceil(model.lo / step) * step; value <= model.hi + 1e-9; value += step)
|
|
227
|
+
ticks.push(Math.round(value * 1e6) / 1e6 || 0);
|
|
228
|
+
const activeStats = current ? model.stats.get(current) : undefined;
|
|
229
|
+
const activeSeries = current ? series.find(entry => entry.id === current) : undefined;
|
|
230
|
+
const reading = cursor && activeStats ? { value: cursor.value, below: activeStats.n ? activeStats.sorted.filter(value => value <= cursor.value).length / activeStats.n : 0 } : null;
|
|
231
|
+
// The tooltip keeps its last content while it fades out, so it never collapses to an empty box on leave.
|
|
232
|
+
const [held, setHeld] = useState(null);
|
|
233
|
+
if (current && (held?.id !== current || held.value !== (reading?.value ?? null)))
|
|
234
|
+
setHeld({ id: current, value: reading?.value ?? null });
|
|
235
|
+
const tipSeries = held ? series.find(entry => entry.id === held.id) : undefined;
|
|
236
|
+
const tipStats = held ? model.stats.get(held.id) : undefined;
|
|
237
|
+
const tipBelow = held && held.value !== null && tipStats?.n ? tipStats.sorted.filter(value => value <= held.value).length / tipStats.n : 0;
|
|
238
|
+
// The tooltip trails the pointer or the keyboard cursor and stays inside the chart.
|
|
239
|
+
const tipX = useMotionValue(0), tipY = useMotionValue(0);
|
|
240
|
+
const tipSpringX = useSpring(tipX, follow), tipSpringY = useSpring(tipY, follow);
|
|
241
|
+
const wasOn = useRef(false);
|
|
242
|
+
useLayoutEffect(() => {
|
|
243
|
+
const bubble = tip.current, shape = current ? model.shapes.get(current) : undefined;
|
|
244
|
+
if (cross.current)
|
|
245
|
+
cross.current.style.transform = `translate3d(${reading ? xOf(reading.value).toFixed(2) : 0}px, 0, 0)`;
|
|
246
|
+
if (!bubble || !shape || !width) {
|
|
247
|
+
wasOn.current = false;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const tw = bubble.offsetWidth, th = bubble.offsetHeight;
|
|
251
|
+
const ax = reading ? xOf(reading.value) : xOf(shape.median);
|
|
252
|
+
const ay = cursor?.pointerY ?? baselineOf(shape.row) - heightAt(shape, shape.median) * rowHeight * overlap - LIFT;
|
|
253
|
+
let left = ax + TIP;
|
|
254
|
+
if (left + tw > width)
|
|
255
|
+
left = ax - TIP - tw;
|
|
256
|
+
let y = ay - th - TIP;
|
|
257
|
+
if (y < 0)
|
|
258
|
+
y = ay + TIP;
|
|
259
|
+
tipX.set(clamp(left, 0, Math.max(0, width - tw)));
|
|
260
|
+
tipY.set(clamp(y, 0, Math.max(0, height - th)));
|
|
261
|
+
if (!wasOn.current || reduced) {
|
|
262
|
+
tipSpringX.jump(tipX.get());
|
|
263
|
+
tipSpringY.jump(tipY.get());
|
|
264
|
+
}
|
|
265
|
+
wasOn.current = true;
|
|
266
|
+
});
|
|
267
|
+
const valueAt = (x) => { const d = drawn.current; return d.lo + clamp((x - gutter) / plotWidth, 0, 1) * (d.hi - d.lo); };
|
|
268
|
+
const pick = (x, y) => {
|
|
269
|
+
const value = valueAt(x), lift = rowHeight * overlap;
|
|
270
|
+
let best = null, bestId = null;
|
|
271
|
+
for (const [id, shape] of drawn.current.shapes) {
|
|
272
|
+
const base = baselineOf(shape.row), h = heightAt(shape, value) * lift;
|
|
273
|
+
if (y <= base + 2 && y >= base - h - 4 && (!best || shape.row > best.row)) {
|
|
274
|
+
best = shape;
|
|
275
|
+
bestId = id;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (!bestId) {
|
|
279
|
+
const row = clamp(Math.floor((y - top) / rowHeight), 0, series.length - 1);
|
|
280
|
+
bestId = series[row]?.id ?? null;
|
|
281
|
+
}
|
|
282
|
+
return { id: bestId, value };
|
|
283
|
+
};
|
|
284
|
+
const onPointerMove = (event) => {
|
|
285
|
+
const rect = event.currentTarget.getBoundingClientRect(), x = event.clientX - rect.left, y = event.clientY - rect.top;
|
|
286
|
+
if (x < gutter - 8 || y > height - AXIS + 4) {
|
|
287
|
+
if (event.pointerType === "mouse") {
|
|
288
|
+
setCurrent(null);
|
|
289
|
+
setCursor(null);
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const hit = pick(x, y);
|
|
294
|
+
setCurrent(hit.id);
|
|
295
|
+
setCursor({ value: hit.value, pointerY: y });
|
|
296
|
+
};
|
|
297
|
+
const onKeyDown = (event) => {
|
|
298
|
+
const at = current ? series.findIndex(entry => entry.id === current) : -1;
|
|
299
|
+
const stats = (id) => id ? model.stats.get(id) : undefined;
|
|
300
|
+
const choose = (index) => { event.preventDefault(); const next = series[clamp(index, 0, series.length - 1)]; if (!next)
|
|
301
|
+
return; setCurrent(next.id); setCursor({ value: cursor?.value ?? stats(next.id)?.median ?? model.lo, pointerY: null }); };
|
|
302
|
+
const nudge = (direction) => { event.preventDefault(); if (!current) {
|
|
303
|
+
choose(0);
|
|
304
|
+
return;
|
|
305
|
+
} const base = cursor?.value ?? stats(current)?.median ?? model.lo; const next = clamp((Math.round(base / step) + direction) * step, model.lo, model.hi); setCursor({ value: next, pointerY: null }); };
|
|
306
|
+
if (event.key === "ArrowDown")
|
|
307
|
+
choose(at + 1);
|
|
308
|
+
else if (event.key === "ArrowUp")
|
|
309
|
+
choose(at < 0 ? 0 : at - 1);
|
|
310
|
+
else if (event.key === "Home")
|
|
311
|
+
choose(0);
|
|
312
|
+
else if (event.key === "End")
|
|
313
|
+
choose(series.length - 1);
|
|
314
|
+
else if (event.key === "ArrowRight")
|
|
315
|
+
nudge(1);
|
|
316
|
+
else if (event.key === "ArrowLeft")
|
|
317
|
+
nudge(-1);
|
|
318
|
+
else if (event.key === "Escape" && current) {
|
|
319
|
+
event.preventDefault();
|
|
320
|
+
setCurrent(null);
|
|
321
|
+
setCursor(null);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
const empty = !series.length || series.every(entry => !entry.values.length);
|
|
325
|
+
/** Ridge tone: one hue, the first series color, from a light tint (low median) to full strength (high median), so the scale never passes through a muddy middle. */
|
|
326
|
+
const toneOf = (t) => `color-mix(in oklab, var(--series-1) ${Math.round(38 + clamp(t, 0, 1) * 62)}%, var(--surface))`;
|
|
327
|
+
const ridgeColor = (value) => tint ? toneOf((value - model.lo) / (model.hi - model.lo || 1)) : "var(--series-1)";
|
|
328
|
+
const describe = (entry) => { const s = model.stats.get(entry.id); return `${entry.label}: median ${format(s.median)}, middle half ${format(s.q1)} to ${format(s.q3)}, range ${format(s.min)} to ${format(s.max)}, ${s.n} values`; };
|
|
329
|
+
return _jsxs("figure", { ref: figure, className: [styles.figure, className].filter(Boolean).join(" "), "aria-label": label, children: [_jsxs("div", { ref: plot, className: styles.plot, style: { height }, "data-active": current ? true : undefined, "data-cursor": reading ? true : undefined, role: "group", tabIndex: empty ? -1 : 0, "aria-roledescription": "ridgeline chart", "aria-label": `${label}. Up and down arrows choose a ridge, left and right arrows move along the values.`, onPointerMove: onPointerMove, onPointerDown: onPointerMove, onPointerLeave: event => { if (event.pointerType === "mouse") {
|
|
330
|
+
setCurrent(null);
|
|
331
|
+
setCursor(null);
|
|
332
|
+
} }, onKeyDown: onKeyDown, onBlur: () => { setCurrent(null); setCursor(null); }, children: [_jsx("div", { className: styles.ticks, "aria-hidden": "true", style: { height: height - AXIS + 22 }, children: ticks.map(value => _jsx("div", { ref: element => { if (element)
|
|
333
|
+
tickRefs.current.set(value, element);
|
|
334
|
+
else
|
|
335
|
+
tickRefs.current.delete(value); }, className: styles.tick, style: { top: top - rowHeight * .5 }, children: _jsx("span", { className: styles.tickLabel, children: format(value) }) }, value)) }), _jsx("div", { ref: cross, className: styles.crosshair, style: { top: top - rowHeight * .5, height: height - AXIS - top + rowHeight * .5 }, "aria-hidden": "true" }), _jsxs("svg", { className: styles.svg, width: width || undefined, height: height, viewBox: `0 0 ${Math.max(1, width)} ${height}`, "aria-hidden": "true", focusable: "false", children: [series.map(entry => _jsxs("g", { className: styles.ridge, "data-dim": current !== null && current !== entry.id || undefined, "data-lifted": current === entry.id || undefined, style: { "--ridge": ridgeColor(model.stats.get(entry.id)?.median ?? model.lo) }, children: [_jsx("path", { ref: element => { if (element)
|
|
336
|
+
fills.current.set(entry.id, element);
|
|
337
|
+
else
|
|
338
|
+
fills.current.delete(entry.id); }, className: styles.fill }), _jsx("line", { ref: element => { if (element)
|
|
339
|
+
medians.current.set(entry.id, element);
|
|
340
|
+
else
|
|
341
|
+
medians.current.delete(entry.id); }, className: styles.median }), _jsx("path", { ref: element => { if (element)
|
|
342
|
+
lines.current.set(entry.id, element);
|
|
343
|
+
else
|
|
344
|
+
lines.current.delete(entry.id); }, className: styles.line })] }, entry.id)), _jsxs("g", { ref: element => { lifted.current.group = element; }, className: styles.lens, "data-on": current ? true : undefined, style: tipStats ? { "--ridge": ridgeColor(tipStats.median) } : undefined, children: [_jsx("path", { ref: element => { lifted.current.fill = element; }, className: styles.lensFill }), _jsx("path", { ref: element => { lifted.current.band = element; }, className: styles.lensBand }), _jsx("line", { ref: element => { lifted.current.median = element; }, className: styles.lensMedian }), _jsx("path", { ref: element => { lifted.current.line = element; }, className: styles.lensLine })] })] }), _jsx("div", { className: styles.labels, "aria-hidden": "true", children: series.map((entry, index) => _jsx("span", { className: styles.rowLabel, "data-active": current === entry.id || undefined, style: { top: baselineOf(index) - 18, width: gutter - 10 }, children: entry.label }, entry.id)) }), empty && _jsx("p", { className: styles.message, children: emptyLabel }), _jsx(motion.div, { ref: tip, className: styles.tooltip, style: { x: reduced ? tipX : tipSpringX, y: reduced ? tipY : tipSpringY }, "aria-hidden": "true", children: tipSeries && tipStats && _jsxs(_Fragment, { children: [_jsx("p", { className: styles.tipTitle, children: tipSeries.label }), _jsxs("p", { className: styles.tipValue, children: [_jsx("span", { className: styles.tipKey, children: "Median" }), _jsx("span", { children: format(tipStats.median) })] }), _jsxs("p", { className: styles.tipRow, children: [_jsx("span", { className: styles.tipKey, children: "Middle half" }), _jsxs("span", { children: [format(tipStats.q1), " to ", format(tipStats.q3)] })] }), _jsxs("p", { className: styles.tipRow, children: [_jsx("span", { className: styles.tipKey, children: "Range" }), _jsxs("span", { children: [format(tipStats.min), " to ", format(tipStats.max)] })] }), held?.value != null && _jsxs("p", { className: styles.tipNote, children: [percent.format(tipBelow), " at or below ", format(held.value)] })] }) })] }), tint && !empty && _jsxs("div", { className: styles.scale, "aria-hidden": "true", children: [_jsx("span", { className: styles.scaleLabel, children: "Median" }), _jsx("span", { children: format(model.lo) }), _jsx("span", { className: styles.steps, children: [0, .25, .5, .75, 1].map(step => _jsx("span", { className: styles.step, style: { "--ridge": toneOf(step) } }, step)) }), _jsx("span", { children: format(model.hi) })] }), _jsx("p", { className: styles.srOnly, "aria-live": "polite", "aria-atomic": "true", children: activeSeries ? `${describe(activeSeries)}${reading ? `. ${percent.format(reading.below)} at or below ${format(reading.value)}` : ""}` : "" }), !empty && _jsx("div", { className: styles.srOnly, children: _jsxs("table", { children: [_jsx("caption", { children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Series" }), _jsx("th", { scope: "col", children: "Median" }), _jsx("th", { scope: "col", children: "First quartile" }), _jsx("th", { scope: "col", children: "Third quartile" }), _jsx("th", { scope: "col", children: "Lowest" }), _jsx("th", { scope: "col", children: "Highest" }), _jsx("th", { scope: "col", children: "Count" })] }) }), _jsx("tbody", { children: series.map(entry => { const s = model.stats.get(entry.id); return _jsxs("tr", { children: [_jsx("th", { scope: "row", children: entry.label }), _jsx("td", { children: format(s.median) }), _jsx("td", { children: format(s.q1) }), _jsx("td", { children: format(s.q3) }), _jsx("td", { children: format(s.min) }), _jsx("td", { children: format(s.max) }), _jsx("td", { children: s.n })] }, entry.id); }) })] }) })] });
|
|
345
|
+
}
|
|
346
|
+
export default Ridgeline;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
.figure { display: grid; min-width: 0; margin: 0; color: var(--foreground); font-family: var(--font-body); letter-spacing: var(--tracking-body); }
|
|
2
|
+
|
|
3
|
+
/* Series colors (--series-1 to --series-4) come from registry/foundation.css. */
|
|
4
|
+
|
|
5
|
+
.plot { position: relative; min-width: 0; outline: none; touch-action: pan-y; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; cursor: crosshair; }
|
|
6
|
+
.svg { position: absolute; inset: 0; display: block; overflow: visible; }
|
|
7
|
+
|
|
8
|
+
/* Ridges are opaque, so each one hides the valleys of the row behind it. */
|
|
9
|
+
.ridge { transition: opacity var(--duration-standard) var(--ease-standard); }
|
|
10
|
+
.fill { fill: color-mix(in oklab, var(--ridge) 34%, var(--surface)); stroke: none; transition: fill var(--duration-considered) var(--ease-standard); }
|
|
11
|
+
.line { fill: none; stroke: var(--ridge); stroke-width: 1.5; stroke-linejoin: round; stroke-linecap: round; transition: stroke var(--duration-considered) var(--ease-standard); }
|
|
12
|
+
.median { stroke: color-mix(in oklab, var(--ridge) 55%, var(--surface)); stroke-width: 1; transition: stroke var(--duration-considered) var(--ease-standard); }
|
|
13
|
+
.plot[data-active] .ridge[data-dim] { opacity: .38; }
|
|
14
|
+
.ridge[data-lifted] { opacity: 0; transition-duration: var(--duration-instant); }
|
|
15
|
+
|
|
16
|
+
/* The lifted ridge is a copy drawn above the rest, so it can rise over its neighbours. */
|
|
17
|
+
.lens { opacity: 0; pointer-events: none; transform: translateY(0); transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
|
|
18
|
+
.lens[data-on] { opacity: 1; transform: translateY(-6px); }
|
|
19
|
+
.lensFill { fill: color-mix(in oklab, var(--ridge) 18%, var(--surface)); stroke: none; }
|
|
20
|
+
.lensBand { fill: color-mix(in oklab, var(--ridge) 48%, var(--surface)); stroke: none; }
|
|
21
|
+
.lensMedian { stroke: var(--foreground); stroke-width: 1.5; stroke-linecap: round; }
|
|
22
|
+
.lensLine { fill: none; stroke: var(--ridge); stroke-width: 2.25; stroke-linejoin: round; stroke-linecap: round; }
|
|
23
|
+
|
|
24
|
+
.ticks { position: absolute; top: 0; right: 0; left: 0; pointer-events: none; }
|
|
25
|
+
.tick { position: absolute; bottom: 0; left: 0; width: 1px; background: var(--border-subtle); will-change: transform; animation: tickIn var(--duration-standard) var(--ease-standard); }
|
|
26
|
+
.tickLabel { position: absolute; bottom: 0; left: 0; color: var(--text-muted); font-size: var(--text-xs); line-height: 1; white-space: nowrap; font-variant-numeric: tabular-nums; translate: -50% 0; }
|
|
27
|
+
@keyframes tickIn { from { opacity: 0; } }
|
|
28
|
+
|
|
29
|
+
.crosshair { position: absolute; left: 0; width: 1px; background: var(--foreground); opacity: 0; pointer-events: none; transition: opacity var(--duration-instant) var(--ease-standard); }
|
|
30
|
+
.plot[data-cursor] .crosshair { opacity: .35; }
|
|
31
|
+
|
|
32
|
+
.labels { position: absolute; inset: 0; pointer-events: none; }
|
|
33
|
+
.rowLabel { position: absolute; left: 0; overflow: hidden; color: var(--text-secondary); font-size: var(--text-xs); line-height: 18px; text-overflow: ellipsis; white-space: nowrap; transition: color var(--duration-fast) var(--ease-standard); }
|
|
34
|
+
.rowLabel[data-active] { color: var(--foreground); font-weight: 500; }
|
|
35
|
+
|
|
36
|
+
.message { position: absolute; inset: 0; display: grid; place-items: center; margin: 0; color: var(--text-muted); font-size: var(--text-sm); pointer-events: none; }
|
|
37
|
+
|
|
38
|
+
.tooltip { position: absolute; top: 0; left: 0; z-index: 2; display: grid; min-width: 170px; max-width: min(240px, 100%); gap: 2px; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--surface-raised); box-shadow: var(--shadow-floating); opacity: 0; pointer-events: none; scale: .96; transition: opacity var(--duration-fast) var(--ease-standard), scale var(--duration-fast) var(--ease-standard); }
|
|
39
|
+
.plot[data-active] .tooltip { opacity: 1; scale: 1; }
|
|
40
|
+
.tipTitle { margin: 0 0 2px; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); }
|
|
41
|
+
.tipValue, .tipRow { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
42
|
+
.tipValue { color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: 1.4; }
|
|
43
|
+
.tipRow { color: var(--foreground); font-size: var(--text-xs); line-height: var(--leading-body); }
|
|
44
|
+
.tipKey { color: var(--text-muted); font-weight: 400; }
|
|
45
|
+
.tipValue .tipKey { font-size: var(--text-xs); }
|
|
46
|
+
.tipNote { margin: 4px 0 0; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); font-variant-numeric: tabular-nums; }
|
|
47
|
+
|
|
48
|
+
.scale { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; margin-top: var(--space-group); color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); font-variant-numeric: tabular-nums; }
|
|
49
|
+
.scaleLabel { margin-right: 4px; color: var(--text-secondary); }
|
|
50
|
+
.steps { display: inline-flex; gap: 2px; }
|
|
51
|
+
.step { width: 18px; height: 10px; border-radius: var(--radius-detail); background: var(--ridge); }
|
|
52
|
+
|
|
53
|
+
.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
54
|
+
|
|
55
|
+
@media (prefers-reduced-motion: reduce) {
|
|
56
|
+
.ridge, .lens, .crosshair, .tooltip, .rowLabel { transition: none; }
|
|
57
|
+
.tick { animation: none; }
|
|
58
|
+
}
|