@teamofsilicons/silicon-ui 0.0.0-stage → 0.2.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 +81 -2
- package/check.mjs +236 -0
- 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/eslint.js +53 -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 +76 -4
- package/styles.css +218 -0
- package/styles.css.d.ts +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.figure { display: grid; min-width: 0; gap: var(--space-group); margin: 0; color: var(--foreground); }.caption { display: flex; align-items: baseline; gap: var(--space-related); min-width: 0; color: var(--text-secondary); font-size: var(--text-sm); }.label { min-width: 0; overflow: hidden; white-space: nowrap; }.caption strong { margin-left: auto; color: var(--foreground); font-size: var(--text-lg); font-weight: 500; letter-spacing: var(--tracking-body); font-variant-numeric: tabular-nums; }.caption small { flex: none; font-size: var(--text-xs); font-weight: 500; font-variant-numeric: tabular-nums; transition: color var(--duration-standard) var(--ease-standard); }
|
|
2
|
+
.accent { color: var(--accent-strong); stroke: var(--accent); }.success { color: var(--success); stroke: var(--success); }.warning { color: var(--warning); stroke: var(--warning); }.danger { color: var(--danger); stroke: var(--danger); }
|
|
3
|
+
/* The scrubbed date is context, not a change, so it stays neutral whatever the tone. */
|
|
4
|
+
.point { color: var(--text-secondary); }
|
|
5
|
+
|
|
6
|
+
/* Changing text rolls in place. Outgoing pieces are popped out of flow, so the wrapper never holds both widths; numbers stay anchored to their right edge. */
|
|
7
|
+
.roll { position: relative; display: inline-flex; max-width: 100%; overflow-x: clip; white-space: pre; vertical-align: top; }
|
|
8
|
+
.roll[data-end] { justify-content: flex-end; }
|
|
9
|
+
.track, .slot, .glyph { position: relative; display: inline-block; white-space: pre; }
|
|
10
|
+
.roll[data-whole] .track, .roll[data-whole] .slot, .roll[data-whole] .glyph { max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: top; }
|
|
11
|
+
.sizer { position: absolute; top: 0; left: 0; visibility: hidden; white-space: pre; pointer-events: none; }
|
|
12
|
+
|
|
13
|
+
/* The plot is the scrubber: hover or drag across it, or focus it and use the arrow keys. Vertical swipes still scroll the page. */
|
|
14
|
+
.plot { position: relative; min-width: 0; border-radius: calc(var(--radius-control) / 2); outline: none; -webkit-tap-highlight-color: transparent; }
|
|
15
|
+
.plot[data-interactive] { touch-action: pan-y; cursor: default; user-select: none; }
|
|
16
|
+
.plot:focus-visible { --focus-outline-offset: 8px; }
|
|
17
|
+
.chart { display: block; width: 100%; height: auto; overflow: visible; }
|
|
18
|
+
.baseline { stroke: var(--border); stroke-width: 1; }
|
|
19
|
+
/* The area under the line carries a faint wash of the line tone. */
|
|
20
|
+
.area.area { fill: color-mix(in oklab, currentColor 9%, transparent); stroke: none; }
|
|
21
|
+
.line, .trail { fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
|
|
22
|
+
.line { transition: stroke var(--duration-standard) var(--ease-standard); }
|
|
23
|
+
.trail { stroke: var(--border-strong); }
|
|
24
|
+
.cursor { stroke: var(--border-strong); stroke-width: 1; }
|
|
25
|
+
.end { transition: stroke var(--duration-standard) var(--ease-standard); }
|
|
26
|
+
@media (prefers-reduced-motion: reduce) { .line, .end, .caption small { transition: none; } }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface SplitButtonAction {
|
|
3
|
+
label: string;
|
|
4
|
+
onSelect?: () => void;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
destructive?: boolean;
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
export interface SplitButtonProps {
|
|
10
|
+
label: string;
|
|
11
|
+
actions: SplitButtonAction[];
|
|
12
|
+
onClick?: () => void;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
icon?: ReactNode;
|
|
15
|
+
variant?: "primary" | "secondary";
|
|
16
|
+
}
|
|
17
|
+
/** The main action morphs its icon and label in place while its width follows on a spring; the menu half never scales, so the menu opens from a still anchor. */
|
|
18
|
+
export declare function SplitButton({ label, actions, onClick, disabled, icon, variant }: SplitButtonProps): import("react").JSX.Element;
|
|
19
|
+
export default SplitButton;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { isValidElement } from "react";
|
|
4
|
+
import * as DropdownPrimitive from "@radix-ui/react-dropdown-menu";
|
|
5
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
6
|
+
import { ChevronDown } from "lucide-react";
|
|
7
|
+
import { MenuHighlight, useMenuHighlight } from "../../lib/menu-highlight.js";
|
|
8
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
9
|
+
import { TextMorph } from "../text-morph/text-morph.js";
|
|
10
|
+
import styles from "./split-button.module.css";
|
|
11
|
+
const rest = { opacity: 1, y: 0, scale: 1, filter: "blur(0px)" };
|
|
12
|
+
const fadeIn = { ...rest, opacity: 0 };
|
|
13
|
+
const fadeOut = { opacity: 0, transition: { duration: motionTokens.duration.instant } };
|
|
14
|
+
const iconIn = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };
|
|
15
|
+
const iconOut = { ...iconIn, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } };
|
|
16
|
+
/** Scale rides the spring; opacity and blur tween so blur never overshoots below zero. */
|
|
17
|
+
const iconEnter = { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] }, filter: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] } };
|
|
18
|
+
/** Names the icon element, so swapping Copy for Check morphs while a re-render of the same icon stays still. */
|
|
19
|
+
function iconKey(node) {
|
|
20
|
+
if (!isValidElement(node))
|
|
21
|
+
return node == null || typeof node === "boolean" ? "" : String(node);
|
|
22
|
+
const type = node.type;
|
|
23
|
+
return typeof type === "string" ? type : type?.displayName ?? type?.name ?? "icon";
|
|
24
|
+
}
|
|
25
|
+
/** The main action morphs its icon and label in place while its width follows on a spring; the menu half never scales, so the menu opens from a still anchor. */
|
|
26
|
+
export function SplitButton({ label, actions, onClick, disabled, icon, variant = "primary" }) {
|
|
27
|
+
const reduced = useReducedMotion() ?? false;
|
|
28
|
+
const { highlight, reset, contentProps } = useMenuHighlight();
|
|
29
|
+
const glyph = iconKey(icon);
|
|
30
|
+
return _jsxs(DropdownPrimitive.Root, { onOpenChange: open => { if (open)
|
|
31
|
+
reset(); }, children: [_jsxs("div", { className: [styles.group, variant === "secondary" ? styles.secondary : ""].filter(Boolean).join(" "), children: [_jsxs("button", { className: styles.primary, type: "button", onClick: onClick, disabled: disabled, children: [_jsx("span", { className: styles.primarySlot, "aria-hidden": "true", children: _jsxs("span", { className: styles.primaryContent, children: [icon ? _jsx("span", { className: styles.mainIcon, children: _jsx(AnimatePresence, { initial: false, children: _jsx(motion.span, { className: styles.iconPhase, initial: reduced ? fadeIn : iconIn, animate: rest, exit: reduced ? fadeOut : iconOut, transition: reduced ? { duration: motionTokens.duration.instant } : iconEnter, children: icon }, glyph) }) }) : null, _jsx(TextMorph, { children: label })] }) }), _jsx("span", { className: styles.srOnly, "aria-live": "polite", children: label })] }), _jsx(DropdownPrimitive.Trigger, { className: styles.trigger, type: "button", "aria-label": `${label} more actions`, disabled: disabled, children: _jsx(ChevronDown, { className: styles.chevron, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }), _jsx(DropdownPrimitive.Portal, { children: _jsxs(DropdownPrimitive.Content, { className: styles.menu, sideOffset: 4, align: "end", collisionPadding: 12, loop: true, ...contentProps, children: [_jsx(MenuHighlight, { state: highlight, className: styles.highlight }), actions.map((action, index) => _jsxs(DropdownPrimitive.Item, { className: [styles.item, action.destructive ? styles.destructive : ""].filter(Boolean).join(" "), "data-tone": action.destructive ? "danger" : undefined, style: { "--i": index }, disabled: action.disabled, onSelect: action.onSelect, children: [action.icon ? _jsx("span", { className: styles.icon, "aria-hidden": "true", children: action.icon }) : null, action.label] }, action.label))] }) })] });
|
|
32
|
+
}
|
|
33
|
+
export default SplitButton;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* One pill, two actions. The whole pill presses in for the main action; the menu half answers in color only, because Radix measures it on pointerdown to place the menu. */
|
|
2
|
+
.group { display: inline-flex; align-items: stretch; border: 1px solid var(--foreground); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--foreground); transition: transform var(--duration-spring) var(--ease-spring); }
|
|
3
|
+
.group:has(> .primary:active:not(:disabled)) { transform: scale(.97); transition-duration: var(--duration-instant); transition-timing-function: var(--ease-standard); }
|
|
4
|
+
.primary, .trigger { border: 0; background: var(--foreground); color: var(--background); font: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
|
|
5
|
+
.primary { position: relative; display: inline-grid; min-height: var(--control-height-md); place-items: center; padding: 0 var(--space-inset); border-radius: calc(var(--radius-control) - 1px) 0 0 calc(var(--radius-control) - 1px); corner-shape: var(--corner-shape); font-size: var(--text-sm); font-weight: 500; }
|
|
6
|
+
/* TextMorph springs the label's width, so the main half grows and shrinks with its text. */
|
|
7
|
+
.primarySlot { display: inline-flex; min-width: 0; align-items: center; }
|
|
8
|
+
.primaryContent { display: inline-flex; flex: none; align-items: center; gap: var(--space-related); white-space: nowrap; }
|
|
9
|
+
.mainIcon { display: inline-grid; flex: none; place-items: center; }
|
|
10
|
+
.iconPhase { grid-area: 1 / 1; display: inline-flex; align-items: center; justify-content: center; }
|
|
11
|
+
.trigger { display: grid; width: 38px; min-height: var(--control-height-md); place-items: center; border-left: 1px solid color-mix(in oklab, var(--background) 18%, var(--foreground)); border-radius: 0 calc(var(--radius-control) - 1px) calc(var(--radius-control) - 1px) 0; corner-shape: var(--corner-shape); }
|
|
12
|
+
.chevron { transition: transform var(--duration-spring) var(--ease-spring); }
|
|
13
|
+
.trigger[data-state="open"] .chevron { transform: rotate(180deg); }
|
|
14
|
+
@media (hover: hover) and (pointer: fine) {
|
|
15
|
+
.primary:hover:not(:disabled), .trigger:hover:not(:disabled) { background: color-mix(in oklab, var(--foreground) 88%, var(--background)); }
|
|
16
|
+
.secondary .primary:hover:not(:disabled), .secondary .trigger:hover:not(:disabled) { background: var(--surface-muted); }
|
|
17
|
+
}
|
|
18
|
+
.primary:active:not(:disabled), .trigger:active:not(:disabled), .trigger[data-state="open"] { background: color-mix(in oklab, var(--foreground) 80%, var(--background)); }
|
|
19
|
+
.primary:disabled, .trigger:disabled { cursor: not-allowed; opacity: .52; }
|
|
20
|
+
.secondary { border-color: var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); }
|
|
21
|
+
.secondary .primary, .secondary .trigger { min-height: var(--control-height-sm); background: var(--surface); color: var(--foreground); }
|
|
22
|
+
.secondary .primary { min-width: 142px; border-radius: var(--radius-full) 0 0 var(--radius-full); corner-shape: round; padding-inline: var(--space-group); font-size: var(--text-xs); }
|
|
23
|
+
.secondary .trigger { width: 32px; border-left-color: var(--border-subtle); border-radius: 0 var(--radius-full) var(--radius-full) 0; corner-shape: round; }
|
|
24
|
+
.secondary .primary:active:not(:disabled), .secondary .trigger:active:not(:disabled), .secondary .trigger[data-state="open"] { background: color-mix(in oklab, var(--surface-muted), var(--border) 55%); }
|
|
25
|
+
.icon { display: inline-flex; }
|
|
26
|
+
.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
27
|
+
/* The menu grows from the trigger edge: offset toward the trigger, scale from the Radix origin, fade quickly; it leaves faster than it arrives.
|
|
28
|
+
Transitions instead of keyframes, so a close that interrupts the open reverses from where the menu is instead of snapping to rest first. */
|
|
29
|
+
.menu { --menu-x: 0px; --menu-y: -5px; position: relative; z-index: 60; min-width: min(12rem, var(--radix-dropdown-menu-content-available-width)); max-width: var(--radix-dropdown-menu-content-available-width); padding: 5px; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); transform-origin: var(--radix-dropdown-menu-content-transform-origin); transition: opacity var(--duration-fast) var(--ease-enter), transform var(--duration-spring) var(--ease-spring); }
|
|
30
|
+
.menu[data-side="top"] { --menu-y: 5px; }
|
|
31
|
+
.menu[data-side="left"] { --menu-x: 5px; --menu-y: 0px; }
|
|
32
|
+
.menu[data-side="right"] { --menu-x: -5px; --menu-y: 0px; }
|
|
33
|
+
@starting-style {
|
|
34
|
+
.menu[data-state="open"] { opacity: 0; transform: translate(var(--menu-x), var(--menu-y)) scale(.97); }
|
|
35
|
+
.menu[data-state="open"] .item { opacity: 0; transform: translate(calc(var(--menu-x) * .4), calc(var(--menu-y) * .4)); }
|
|
36
|
+
}
|
|
37
|
+
/* Radix unmounts when the exit animation ends, so a no-op keyframe times the exit while the transition does the visual work. */
|
|
38
|
+
.menu[data-state="closed"] { opacity: 0; transform: translate(calc(var(--menu-x) * .5), calc(var(--menu-y) * .5)) scale(.985); transition: opacity 130ms var(--ease-standard), transform 130ms var(--ease-standard); animation: menu-exit 130ms linear both; pointer-events: none; }
|
|
39
|
+
.highlight { position: absolute; top: 0; right: 5px; left: 5px; border-radius: calc(var(--radius-container) - 6px); background: var(--surface-muted); opacity: 0; pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); }
|
|
40
|
+
.highlight[data-tone="danger"] { background: color-mix(in oklab, var(--danger) 8%, var(--surface)); }
|
|
41
|
+
/* One highlight glides between items for the pointer and jumps instantly for the keyboard (lib/menu-highlight). */
|
|
42
|
+
.item { position: relative; display: flex; min-height: var(--control-height-sm); align-items: center; gap: 10px; padding: 0 11px; border-radius: calc(var(--radius-container) - 6px); corner-shape: var(--corner-shape); font-size: var(--text-sm); cursor: pointer; outline: none; transition: opacity var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * 35ms), transform var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * 35ms); }
|
|
43
|
+
.item[data-disabled] { cursor: default; opacity: .45; }
|
|
44
|
+
.destructive { color: var(--danger); }
|
|
45
|
+
@keyframes menu-exit { to { --menu-exit: 1; } }
|
|
46
|
+
@media (prefers-reduced-motion: reduce) {
|
|
47
|
+
.group, .primary, .trigger, .chevron { transition: none; }
|
|
48
|
+
.group:has(> .primary:active:not(:disabled)) { transform: none; }
|
|
49
|
+
.item { transition: none; }
|
|
50
|
+
.menu, .menu[data-state="closed"] { transform: none; transition: opacity var(--duration-instant) linear; }
|
|
51
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type StepperOrientation = "horizontal" | "vertical";
|
|
2
|
+
export type StepperStatus = "complete" | "current" | "upcoming" | "error";
|
|
3
|
+
export interface StepperStep {
|
|
4
|
+
/** Stable key, so each step keeps its own motion when the list changes. */
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
/** A short hint under the label. */
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Marks the step as failed: the marker morphs into an alert and this message replaces the description. */
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A steps indicator for onboarding, checkout, and setup flows. Place it above (horizontal) or beside (vertical) the step
|
|
14
|
+
* content and drive it with `current`; set `current` to `steps.length` once every step is done. Pass `onStepSelect` to let
|
|
15
|
+
* people return to completed steps by click or keyboard. For a multi-step form that owns its own content, use MultiStepForm.
|
|
16
|
+
*/
|
|
17
|
+
export interface StepperProps {
|
|
18
|
+
steps: StepperStep[];
|
|
19
|
+
/** Index of the step in progress. `steps.length` marks the whole flow complete. */
|
|
20
|
+
current: number;
|
|
21
|
+
orientation?: StepperOrientation;
|
|
22
|
+
/** Called with the index of a completed step when it is chosen. Without it the stepper is a read-only indicator. */
|
|
23
|
+
onStepSelect?: (index: number) => void;
|
|
24
|
+
/** `current` shows only the active step's description, for tight spaces. Errors always show. */
|
|
25
|
+
details?: "all" | "current";
|
|
26
|
+
/** Markers only; labels stay available to assistive tech. Horizontal steppers switch to this below 30rem on their own
|
|
27
|
+
* and show the current step's label underneath. */
|
|
28
|
+
compact?: boolean;
|
|
29
|
+
/** Accessible name for the stepper. */
|
|
30
|
+
label?: string;
|
|
31
|
+
/** Announced, and shown in the compact caption, once every step is complete. */
|
|
32
|
+
completeLabel?: string;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function Stepper({ steps, current, orientation, onStepSelect, details, compact, label, completeLabel, className }: StepperProps): import("react").JSX.Element;
|
|
36
|
+
export default Stepper;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion } from "motion/react";
|
|
5
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
6
|
+
import styles from "./stepper.module.css";
|
|
7
|
+
const blur = (radius) => `blur(${radius}px)`;
|
|
8
|
+
const still = { duration: 0 };
|
|
9
|
+
const leave = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };
|
|
10
|
+
const settle = { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] };
|
|
11
|
+
const textFrom = { opacity: 0, y: "0.3em", filter: blur(motionTokens.blur.soft) };
|
|
12
|
+
const textRest = { opacity: 1, y: 0, filter: blur(0) };
|
|
13
|
+
const textGone = { opacity: 0, y: "-0.3em", filter: blur(motionTokens.blur.subtle) };
|
|
14
|
+
const glyphFrom = { opacity: 0, scale: .5, filter: blur(motionTokens.blur.subtle) };
|
|
15
|
+
const glyphRest = { opacity: 1, scale: 1, filter: blur(0) };
|
|
16
|
+
/** Text that rises in with a small blur when it changes. Its slot springs to the new height, so a message that wraps
|
|
17
|
+
* eases the steps below it down instead of pushing them; unrelated resizes (fonts, container width) follow exactly. */
|
|
18
|
+
function SwapText({ text, className, reduced }) {
|
|
19
|
+
const inner = useRef(null);
|
|
20
|
+
const armedUntil = useRef(0);
|
|
21
|
+
const height = useMotionValue("auto");
|
|
22
|
+
useLayoutEffect(() => { armedUntil.current = performance.now() + 700; }, [text]);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const node = inner.current, slot = node?.parentElement;
|
|
25
|
+
if (!node || !slot || typeof ResizeObserver === "undefined")
|
|
26
|
+
return;
|
|
27
|
+
let measured = false;
|
|
28
|
+
const observer = new ResizeObserver(() => {
|
|
29
|
+
const next = node.offsetHeight;
|
|
30
|
+
if (!measured || reduced || performance.now() > armedUntil.current) {
|
|
31
|
+
measured = true;
|
|
32
|
+
height.jump(next);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
animate(height, next, motionTokens.spring.smooth);
|
|
36
|
+
});
|
|
37
|
+
observer.observe(node);
|
|
38
|
+
return () => observer.disconnect();
|
|
39
|
+
}, [height, reduced]);
|
|
40
|
+
return _jsx(motion.span, { className: styles.slot, style: { height }, children: _jsx("span", { ref: inner, className: styles.slotInner, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: text ? _jsx(motion.span, { className: className, initial: textFrom, animate: textRest, exit: { ...textGone, transition: reduced ? still : leave }, transition: reduced ? still : settle, children: text }, `${className}:${text}`) : null }) }) });
|
|
41
|
+
}
|
|
42
|
+
/** The number, check, and alert share one spot: the outgoing glyph shrinks away while the next one pops in and draws its stroke. */
|
|
43
|
+
function Glyph({ kind, number, delay, reduced }) {
|
|
44
|
+
const pop = reduced ? still : { scale: { ...motionTokens.spring.snappy, delay }, opacity: { duration: motionTokens.duration.fast, delay }, filter: { duration: motionTokens.duration.fast, delay } };
|
|
45
|
+
const exit = { ...glyphFrom, transition: reduced ? still : leave };
|
|
46
|
+
const draw = reduced ? still : { duration: motionTokens.duration.standard, ease: motionTokens.ease.enter, delay: delay + .04 };
|
|
47
|
+
if (kind === "number")
|
|
48
|
+
return _jsx(motion.span, { className: styles.glyph, initial: glyphFrom, animate: glyphRest, exit: exit, transition: pop, children: number });
|
|
49
|
+
return _jsx(motion.svg, { className: `${styles.glyph} ${styles.icon}`, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", initial: glyphFrom, animate: glyphRest, exit: exit, transition: pop, children: kind === "check"
|
|
50
|
+
? _jsx(motion.path, { d: "M5.5 12.5l4.25 4.25L18.5 8", initial: { pathLength: 0 }, animate: { pathLength: 1 }, transition: draw })
|
|
51
|
+
: _jsxs(_Fragment, { children: [_jsx(motion.path, { d: "M12 6.75v6.5", initial: { pathLength: 0 }, animate: { pathLength: 1 }, transition: draw }), _jsx(motion.circle, { cx: 12, cy: 17.4, r: 1.4, fill: "currentColor", stroke: "none", initial: { scale: 0 }, animate: { scale: 1 }, transition: reduced ? still : { ...motionTokens.spring.snappy, delay: delay + .16 } })] }) });
|
|
52
|
+
}
|
|
53
|
+
/** The ring grows out from behind the disc once progress arrives, so the eye lands on the new step after the connector fills. */
|
|
54
|
+
function Marker({ number, kind, current, glyphDelay, ringDelay, reduced }) {
|
|
55
|
+
return _jsxs("span", { className: styles.marker, "aria-hidden": "true", children: [_jsx(AnimatePresence, { initial: false, children: current ? _jsx(motion.span, { className: styles.ring, initial: { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: .6, transition: reduced ? still : leave }, transition: reduced ? still : { scale: { ...motionTokens.spring.snappy, delay: ringDelay }, opacity: { duration: motionTokens.duration.fast, delay: ringDelay } } }, "ring") : null }), _jsx("span", { className: styles.disc, children: _jsx(AnimatePresence, { initial: false, children: _jsx(Glyph, { kind: kind, number: number, delay: glyphDelay, reduced: reduced }, kind) }) })] });
|
|
56
|
+
}
|
|
57
|
+
const statusText = { complete: "Completed", current: "", upcoming: "Not started", error: "Error" };
|
|
58
|
+
export function Stepper({ steps, current, orientation = "horizontal", onStepSelect, details = "all", compact = false, label = "Progress", completeLabel = "All steps complete", className }) {
|
|
59
|
+
const reduced = useReducedMotion() ?? false;
|
|
60
|
+
const count = steps.length;
|
|
61
|
+
const active = Math.min(Math.max(Math.round(current), 0), count);
|
|
62
|
+
// Remember where progress came from, so a jump across several steps fills or drains its connectors one after another.
|
|
63
|
+
const [travel, setTravel] = useState({ to: active, from: active });
|
|
64
|
+
if (travel.to !== active)
|
|
65
|
+
setTravel({ to: active, from: travel.to });
|
|
66
|
+
const from = travel.from;
|
|
67
|
+
const gap = motionTokens.stagger.line;
|
|
68
|
+
const delayAt = (index) => {
|
|
69
|
+
if (reduced)
|
|
70
|
+
return 0;
|
|
71
|
+
if (active > from)
|
|
72
|
+
return index >= from && index < active ? (index - from) * gap : 0;
|
|
73
|
+
return index >= active && index < from ? (from - 1 - index) * gap : 0;
|
|
74
|
+
};
|
|
75
|
+
const ringDelay = reduced ? 0 : Math.max(0, Math.abs(active - from) - 1) * gap + .12;
|
|
76
|
+
const interactive = Boolean(onStepSelect);
|
|
77
|
+
const vertical = orientation === "vertical";
|
|
78
|
+
const done = active >= count;
|
|
79
|
+
const now = done ? undefined : steps[active];
|
|
80
|
+
/** Arrow keys move between the steps you can reach; Home and End jump to the first and the current one. */
|
|
81
|
+
function onKeyDown(event) {
|
|
82
|
+
const rtl = !vertical && getComputedStyle(event.currentTarget).direction === "rtl";
|
|
83
|
+
const back = ["ArrowUp", rtl ? "ArrowRight" : "ArrowLeft"], ahead = ["ArrowDown", rtl ? "ArrowLeft" : "ArrowRight"];
|
|
84
|
+
if (![...back, ...ahead, "Home", "End"].includes(event.key))
|
|
85
|
+
return;
|
|
86
|
+
const targets = Array.from(event.currentTarget.querySelectorAll("button[data-reachable]"));
|
|
87
|
+
const at = targets.indexOf(event.target);
|
|
88
|
+
if (at < 0)
|
|
89
|
+
return;
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? targets.length - 1 : back.includes(event.key) ? Math.max(0, at - 1) : Math.min(targets.length - 1, at + 1);
|
|
92
|
+
targets[next]?.focus();
|
|
93
|
+
}
|
|
94
|
+
const Root = interactive ? "nav" : "div";
|
|
95
|
+
const classes = [styles.root, vertical ? styles.vertical : styles.horizontal, compact ? styles.compact : "", className].filter(Boolean).join(" ");
|
|
96
|
+
return _jsxs(Root, { className: classes, "aria-label": label, role: interactive ? undefined : "group", children: [_jsx("ol", { className: styles.list, style: vertical ? undefined : { gridTemplateColumns: count > 1 ? `repeat(${count - 1}, minmax(0, 1fr)) auto` : "auto" }, onKeyDown: interactive ? onKeyDown : undefined, children: steps.map((step, index) => {
|
|
97
|
+
const isCurrent = index === active;
|
|
98
|
+
const status = step.error ? "error" : index < active ? "complete" : isCurrent ? "current" : "upcoming";
|
|
99
|
+
const clickable = interactive && index < active;
|
|
100
|
+
const detail = step.error ?? (details === "all" || isCurrent ? step.description : undefined);
|
|
101
|
+
const kind = step.error ? "error" : index < active ? "check" : "number";
|
|
102
|
+
const content = _jsxs(_Fragment, { children: [_jsx(Marker, { number: index + 1, kind: kind, current: isCurrent, glyphDelay: delayAt(index), ringDelay: ringDelay, reduced: reduced }), _jsxs("span", { className: styles.text, children: [_jsx("span", { className: styles.label, children: step.label }), statusText[status] ? _jsxs("span", { className: styles.srOnly, children: [", ", statusText[status]] }) : null, _jsx(SwapText, { text: detail, className: step.error ? styles.error : styles.description, reduced: reduced })] })] });
|
|
103
|
+
return _jsxs("li", { className: styles.item, "data-status": status, children: [index < count - 1 ? _jsx("span", { className: styles.connector, "aria-hidden": "true", children: _jsx(motion.span, { className: styles.fill, initial: false, animate: vertical ? { scaleY: index < active ? 1 : 0 } : { scaleX: index < active ? 1 : 0 }, transition: reduced ? still : { ...motionTokens.spring.smooth, delay: delayAt(index) } }, orientation) }) : null, interactive
|
|
104
|
+
? _jsx("button", { type: "button", className: styles.head, "data-clickable": clickable || undefined, "data-reachable": index <= active || undefined, "aria-current": isCurrent ? "step" : undefined, "aria-disabled": clickable ? undefined : true, tabIndex: clickable ? undefined : -1, onClick: clickable ? () => onStepSelect?.(index) : undefined, children: content })
|
|
105
|
+
: _jsx("span", { className: styles.head, "aria-current": isCurrent ? "step" : undefined, children: content })] }, step.id);
|
|
106
|
+
}) }), vertical ? null : _jsxs("span", { className: styles.caption, "aria-hidden": "true", children: [_jsx(SwapText, { text: now?.label ?? completeLabel, className: styles.captionLabel, reduced: reduced }), _jsx(SwapText, { text: now?.error ?? now?.description, className: now?.error ? styles.error : styles.description, reduced: reduced })] }), _jsx("span", { className: styles.srOnly, "aria-live": "polite", children: now ? `Step ${active + 1} of ${count}: ${now.label}` : completeLabel })] });
|
|
107
|
+
}
|
|
108
|
+
export default Stepper;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
.root { --marker: 1.75rem; --halo: 4px; min-width: 0; color: var(--foreground); font-family: var(--font-body); letter-spacing: var(--tracking-body); }
|
|
2
|
+
/* A horizontal stepper spans its container, so it can safely measure itself and fold into markers when space runs out. */
|
|
3
|
+
.horizontal { width: 100%; container: stepper / inline-size; }
|
|
4
|
+
.list { display: grid; margin: 0; padding: 0; list-style: none; }
|
|
5
|
+
.item { position: relative; min-width: 0; }
|
|
6
|
+
.horizontal .item:not(:last-child) { padding-inline-end: var(--space-inset); }
|
|
7
|
+
.vertical .item:not(:last-child) { padding-bottom: var(--space-comfort); }
|
|
8
|
+
.compact.vertical .item:not(:last-child) { padding-bottom: var(--space-section); }
|
|
9
|
+
|
|
10
|
+
/* Buttons and spans share one head, so a chosen step keeps focus as it becomes the current one. */
|
|
11
|
+
.head { display: grid; margin: 0; padding: 0; border: 0; border-radius: var(--space-related); background: none; color: inherit; font: inherit; letter-spacing: inherit; text-align: start; cursor: default; -webkit-tap-highlight-color: transparent; }
|
|
12
|
+
.head[data-clickable] { cursor: pointer; }
|
|
13
|
+
.horizontal .head { width: fit-content; max-width: 100%; justify-items: start; row-gap: var(--space-group); }
|
|
14
|
+
.vertical .head { width: 100%; grid-template-columns: var(--marker) minmax(0, 1fr); column-gap: var(--space-group); align-items: start; }
|
|
15
|
+
.compact.vertical .head { grid-template-columns: var(--marker); column-gap: 0; }
|
|
16
|
+
.head:focus-visible { --focus-outline-offset: 4px; }
|
|
17
|
+
.head:focus:not(:focus-visible) { outline: none; }
|
|
18
|
+
|
|
19
|
+
/* The ring sits behind the disc and grows out of it; the disc carries the state color and the glyph. */
|
|
20
|
+
.marker { position: relative; display: grid; width: var(--marker); height: var(--marker); flex: none; place-items: center; }
|
|
21
|
+
.ring { position: absolute; inset: calc(var(--halo) * -1); border-radius: var(--radius-full); corner-shape: round; background: var(--accent-subtle); transition: background-color var(--duration-standard) var(--ease-standard); }
|
|
22
|
+
.disc { position: relative; display: grid; width: 100%; height: 100%; place-items: center; border-radius: var(--radius-full); corner-shape: round; background: var(--surface); box-shadow: inset 0 0 0 1px var(--border-strong); color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; font-variant-numeric: tabular-nums; line-height: 1; transition: background-color var(--duration-standard) var(--ease-standard), box-shadow var(--duration-standard) var(--ease-standard), color var(--duration-standard) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
|
|
23
|
+
.glyph { display: grid; grid-area: 1 / 1; place-items: center; }
|
|
24
|
+
.icon { width: .875rem; height: .875rem; }
|
|
25
|
+
.item[data-status="current"] .disc { box-shadow: inset 0 0 0 1.5px var(--accent); color: var(--foreground); }
|
|
26
|
+
.item[data-status="complete"] .disc { background: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent-foreground); }
|
|
27
|
+
.item[data-status="error"] .disc { background: color-mix(in oklab, var(--danger) 10%, var(--surface)); box-shadow: inset 0 0 0 1.5px var(--danger); color: var(--danger); }
|
|
28
|
+
.item[data-status="error"] .ring { background: color-mix(in oklab, var(--danger) 16%, transparent); }
|
|
29
|
+
|
|
30
|
+
/* The track stays put; only the fill scales along it on a spring, from the step that was just finished. */
|
|
31
|
+
.connector { position: absolute; overflow: hidden; border-radius: var(--radius-full); corner-shape: round; background: var(--border); pointer-events: none; }
|
|
32
|
+
.horizontal .connector { top: calc(var(--marker) / 2 - 1px); inset-inline: calc(var(--marker) + var(--halo) + var(--space-tight)) calc(var(--halo) + var(--space-tight)); height: 2px; }
|
|
33
|
+
/* Both ends keep a clear gap from the markers, and from the current step's ring. */
|
|
34
|
+
.vertical .connector { top: calc(var(--marker) + var(--halo) + 2px); bottom: calc(var(--halo) + 2px); inset-inline-start: calc(var(--marker) / 2 - 1px); width: 2px; }
|
|
35
|
+
.fill { position: absolute; inset: 0; border-radius: inherit; corner-shape: inherit; background: var(--accent); }
|
|
36
|
+
.horizontal .fill { transform-origin: left center; }
|
|
37
|
+
.horizontal .fill:dir(rtl) { transform-origin: right center; }
|
|
38
|
+
.vertical .fill { transform-origin: center top; }
|
|
39
|
+
|
|
40
|
+
.text { display: grid; min-width: 0; }
|
|
41
|
+
/* Centers the first line of the label on the marker. */
|
|
42
|
+
.vertical .text { padding-top: .25rem; }
|
|
43
|
+
.label { display: block; color: var(--text-muted); font-size: var(--text-sm); font-weight: 500; line-height: 1.25rem; overflow-wrap: break-word; transition: color var(--duration-standard) var(--ease-standard); }
|
|
44
|
+
.item[data-status="complete"] .label { color: var(--text-secondary); }
|
|
45
|
+
.item[data-status="current"] .label, .item[data-status="error"] .label { color: var(--foreground); }
|
|
46
|
+
/* Changing text pops out of flow against this box while it leaves, so it must be the positioned parent. */
|
|
47
|
+
.slot { display: block; }
|
|
48
|
+
.slotInner { position: relative; display: block; }
|
|
49
|
+
.description, .error { display: block; padding-top: 2px; font-size: var(--text-xs); font-weight: 400; line-height: var(--leading-body); overflow-wrap: break-word; }
|
|
50
|
+
.description { color: var(--text-muted); }
|
|
51
|
+
.error { color: var(--danger); }
|
|
52
|
+
|
|
53
|
+
.caption { display: none; margin-top: var(--space-group); }
|
|
54
|
+
.captionLabel { display: block; color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: 1.25rem; }
|
|
55
|
+
|
|
56
|
+
@media (hover: hover) and (pointer: fine) {
|
|
57
|
+
.head[data-clickable]:hover .label { color: var(--foreground); }
|
|
58
|
+
.item[data-status="complete"] .head[data-clickable]:hover .disc { background: var(--accent-strong); box-shadow: inset 0 0 0 1px var(--accent-strong); }
|
|
59
|
+
.item[data-status="error"] .head[data-clickable]:hover .disc { background: color-mix(in oklab, var(--danger) 18%, var(--surface)); }
|
|
60
|
+
}
|
|
61
|
+
.head[data-clickable]:active .disc { transform: scale(.92); }
|
|
62
|
+
|
|
63
|
+
.srOnly, .compact .text { position: absolute; width: 1px; height: 1px; margin: 0; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
64
|
+
.compact.horizontal .caption { display: grid; }
|
|
65
|
+
/* Four labels no longer fit side by side: keep the markers and name the current step underneath. */
|
|
66
|
+
@container stepper (max-width: 30rem) {
|
|
67
|
+
.horizontal .text { position: absolute; width: 1px; height: 1px; margin: 0; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
68
|
+
.horizontal .caption { display: grid; }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@media (prefers-reduced-motion: reduce) {
|
|
72
|
+
.disc, .ring, .label { transition: none; }
|
|
73
|
+
.head[data-clickable]:active .disc { transform: none; }
|
|
74
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type Ref } from "react";
|
|
2
|
+
export interface StreamgraphSeries {
|
|
3
|
+
/** Stable identity. A layer keeps its place and color across data changes, so switching datasets morphs it. */
|
|
4
|
+
key: string;
|
|
5
|
+
/** Name in the legend, the direct label, the tooltip, and the data table. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Any CSS color. Defaults to the chart series for the first four layers (the first in the accent), then neutral steps. */
|
|
8
|
+
color?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface StreamgraphDatum {
|
|
11
|
+
/** Stable identity, such as an ISO week. */
|
|
12
|
+
key: string;
|
|
13
|
+
/** Full label for the tooltip and screen readers, such as "Week of Sep 14". */
|
|
14
|
+
label: string;
|
|
15
|
+
/** Short label on the time axis. Leave it out to keep the axis quiet at that point. */
|
|
16
|
+
axisLabel?: string;
|
|
17
|
+
/** One value per series key. Missing values count as zero; negatives are treated as zero. */
|
|
18
|
+
values: Record<string, number | undefined>;
|
|
19
|
+
}
|
|
20
|
+
/** Use a streamgraph for how the mix of several parts changes over time, when the overall shape matters more than exact values. */
|
|
21
|
+
export interface StreamgraphProps {
|
|
22
|
+
data: StreamgraphDatum[];
|
|
23
|
+
/** Layers from the centre out: the first runs through the middle of the stream, the rest alternate above and below it. */
|
|
24
|
+
series: StreamgraphSeries[];
|
|
25
|
+
/** What is measured, such as "Support tickets by topic". Names the chart for assistive technology. */
|
|
26
|
+
label: string;
|
|
27
|
+
/** Unit after each value, such as "tickets". */
|
|
28
|
+
unit?: string;
|
|
29
|
+
/** Plot height in pixels. The width follows the container. */
|
|
30
|
+
height?: number;
|
|
31
|
+
/** "wiggle" minimises layer slopes for a flowing stream, "silhouette" centres the stack, "zero" stacks from a flat baseline. */
|
|
32
|
+
offset?: "wiggle" | "silhouette" | "zero";
|
|
33
|
+
formatValue?: (value: number, series: StreamgraphSeries) => string;
|
|
34
|
+
/** Controlled hidden layer keys. A hidden layer thins to nothing and the rest reflow. */
|
|
35
|
+
hiddenSeries?: string[];
|
|
36
|
+
defaultHiddenSeries?: string[];
|
|
37
|
+
onHiddenSeriesChange?: (hidden: string[]) => void;
|
|
38
|
+
/** Called as the reading moves, and with nulls when it leaves. */
|
|
39
|
+
onActiveChange?: (index: number | null, seriesKey: string | null) => void;
|
|
40
|
+
/** Layer toggles above the plot. */
|
|
41
|
+
legend?: boolean;
|
|
42
|
+
/** Names each layer inside its widest stretch when it is thick enough to hold the text. */
|
|
43
|
+
directLabels?: boolean;
|
|
44
|
+
/** Header of the first column in the screen reader table. */
|
|
45
|
+
categoryLabel?: string;
|
|
46
|
+
emptyLabel?: string;
|
|
47
|
+
ref?: Ref<HTMLElement>;
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
export declare function Streamgraph({ data, series, label, unit, height, offset, formatValue, hiddenSeries, defaultHiddenSeries, onHiddenSeriesChange, onActiveChange, legend, directLabels, categoryLabel, emptyLabel, ref, className }: StreamgraphProps): import("react").JSX.Element;
|
|
51
|
+
export default Streamgraph;
|