@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,97 @@
|
|
|
1
|
+
.overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
inset: 0;
|
|
4
|
+
z-index: 50;
|
|
5
|
+
background: oklch(10% 0 0 / .38);
|
|
6
|
+
backdrop-filter: blur(4px);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.content {
|
|
10
|
+
--drawer-size: min(30rem, calc(100vw - var(--space-inset)));
|
|
11
|
+
/* Critically damped, like a sheet spring: it never overshoots past the edge it is anchored to. */
|
|
12
|
+
--ease-drawer: cubic-bezier(.32,.72,0,1);
|
|
13
|
+
--drawer-from: translateX(100%);
|
|
14
|
+
position: fixed;
|
|
15
|
+
z-index: 51;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
width: var(--drawer-size);
|
|
19
|
+
max-width: 100vw;
|
|
20
|
+
max-height: 100dvh;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
border: 1px solid var(--border);
|
|
23
|
+
background: var(--surface-raised);
|
|
24
|
+
color: var(--foreground);
|
|
25
|
+
box-shadow: var(--shadow-floating);
|
|
26
|
+
}
|
|
27
|
+
/* While a layer leaves it lets clicks through, so pressing the trigger again during the close reopens the drawer from wherever it is. */
|
|
28
|
+
/* Radix writes pointer-events inline on the overlay, so the closing state has to win over it. */
|
|
29
|
+
.overlay[data-state="closed"], .content[data-state="closed"] { pointer-events: none !important; }
|
|
30
|
+
|
|
31
|
+
.content[data-side="right"] { inset: 0 0 0 auto; border-width: 1px 0 1px 1px; border-radius: var(--radius-container) 0 0 var(--radius-container); corner-shape: var(--corner-shape); }
|
|
32
|
+
.content[data-side="left"] { inset: 0 auto 0 0; border-width: 1px 1px 1px 0; border-radius: 0 var(--radius-container) var(--radius-container) 0; corner-shape: var(--corner-shape); }
|
|
33
|
+
.content[data-side="top"] { inset: 0 0 auto; width: 100%; max-width: none; max-height: min(32rem, 100dvh); border-width: 0 0 1px; border-radius: 0 0 var(--radius-container) var(--radius-container); corner-shape: var(--corner-shape); }
|
|
34
|
+
.content[data-side="bottom"] { inset: auto 0 0; width: 100%; max-width: none; max-height: min(32rem, 100dvh); border-width: 1px 0 0; border-radius: var(--radius-container) var(--radius-container) 0 0; corner-shape: var(--corner-shape); }
|
|
35
|
+
.content[data-side="bottom"]::before { position: absolute; top: 9px; left: 50%; width: 2.25rem; height: 4px; border-radius: var(--radius-full); corner-shape: round; background: var(--border-strong); content: ""; opacity: .75; transform: translateX(-50%); }
|
|
36
|
+
|
|
37
|
+
/* Motion springs the panel from its edge. Keyframes only run when DrawerContent sits under a bare Radix root. */
|
|
38
|
+
.content[data-side="left"] { --drawer-from: translateX(-100%); }
|
|
39
|
+
.content[data-side="top"] { --drawer-from: translateY(-100%); }
|
|
40
|
+
.content[data-side="bottom"] { --drawer-from: translateY(100%); }
|
|
41
|
+
.keyframes.overlay[data-state="open"] { animation: overlay-in var(--duration-standard) var(--ease-enter) both; }
|
|
42
|
+
.keyframes.overlay[data-state="closed"] { animation: overlay-out var(--duration-fast) var(--ease-standard) both; }
|
|
43
|
+
.keyframes.content[data-state="open"] { animation: drawer-in var(--duration-considered) var(--ease-drawer) both; }
|
|
44
|
+
.keyframes.content[data-state="closed"] { animation: drawer-out var(--duration-standard) var(--ease-standard) both; }
|
|
45
|
+
.content:focus { outline: none; }
|
|
46
|
+
/* Inside a container the layers anchor to that element instead of the viewport. */
|
|
47
|
+
.overlay[data-contained], .content[data-contained] { position: absolute; }
|
|
48
|
+
.overlay[data-contained] { z-index: 1; background: oklch(10% 0 0 / .16); backdrop-filter: none; }
|
|
49
|
+
.content[data-contained] { --drawer-size: min(22rem, calc(100% - var(--space-block))); z-index: 2; max-height: 100%; box-shadow: none; }
|
|
50
|
+
.content[data-contained][data-side="top"], .content[data-contained][data-side="bottom"] { max-height: min(32rem, calc(100% - var(--space-block))); }
|
|
51
|
+
/* The container already draws the outer edges, so only the inner edge keeps a border. */
|
|
52
|
+
.content[data-contained][data-side] { border-width: 0; }
|
|
53
|
+
.content[data-contained][data-side="right"] { border-left-width: 1px; }
|
|
54
|
+
.content[data-contained][data-side="left"] { border-right-width: 1px; }
|
|
55
|
+
.content[data-contained][data-side="top"] { border-bottom-width: 1px; }
|
|
56
|
+
.content[data-contained][data-side="bottom"] { border-top-width: 1px; }
|
|
57
|
+
|
|
58
|
+
.header { display: flex; flex: 0 0 auto; align-items: flex-start; justify-content: space-between; gap: var(--space-comfort); padding: var(--space-section); border-bottom: 1px solid var(--border); }
|
|
59
|
+
/* The header doubles as the drag handle, so touch drags move the panel instead of the page. */
|
|
60
|
+
.handle { touch-action: none; user-select: none; }
|
|
61
|
+
.content[data-side="bottom"] .handle, .content[data-side="top"] .handle { cursor: grab; }
|
|
62
|
+
.title, .description { position: relative; }
|
|
63
|
+
/* The copy sets its own line height so a page-level heading rule cannot squeeze the title. */
|
|
64
|
+
.swap { display: block; line-height: var(--leading-body); }
|
|
65
|
+
.title { margin: 0; font-family: var(--font-body); font-size: var(--text-lg); font-weight: 500; letter-spacing: var(--tracking-body); line-height: var(--leading-body); }
|
|
66
|
+
.description { max-width: 32rem; margin: var(--space-related) 0 0; color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); }
|
|
67
|
+
/* Centered on the first line of the title rather than its top edge. */
|
|
68
|
+
.close { display: grid; flex: 0 0 auto; width: var(--space-block); height: var(--space-block); margin-top: calc((var(--text-lg) * var(--leading-body) - var(--space-block)) / 2); place-items: center; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--surface-muted) 65%, transparent); color: var(--text-secondary); cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
|
|
69
|
+
@media (hover: hover) and (pointer: fine) { .close:hover { background: var(--surface-muted); color: var(--foreground); } }
|
|
70
|
+
.close:active { transform: scale(.96); transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
|
|
71
|
+
.body { min-height: 0; overflow: auto; padding: var(--space-section); font-size: var(--text-sm); line-height: var(--leading-body); overscroll-behavior: contain; scrollbar-width: thin; }
|
|
72
|
+
|
|
73
|
+
@media (max-width: 40rem) {
|
|
74
|
+
.content { --drawer-size: calc(100vw - var(--space-group)); }
|
|
75
|
+
.content[data-side="right"] { border-radius: var(--radius-control) 0 0 var(--radius-control); corner-shape: var(--corner-shape); }
|
|
76
|
+
.content[data-side="left"] { border-radius: 0 var(--radius-control) var(--radius-control) 0; corner-shape: var(--corner-shape); }
|
|
77
|
+
.content[data-side="top"] { border-radius: 0 0 var(--radius-control) var(--radius-control); corner-shape: var(--corner-shape); }
|
|
78
|
+
.content[data-side="bottom"] { border-radius: var(--radius-control) var(--radius-control) 0 0; corner-shape: var(--corner-shape); }
|
|
79
|
+
.header, .body { padding: var(--space-comfort); }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@keyframes overlay-in { from { opacity: 0; } }
|
|
83
|
+
@keyframes overlay-out { to { opacity: 0; } }
|
|
84
|
+
@keyframes drawer-in { from { transform: var(--drawer-from); } }
|
|
85
|
+
/* The late fade lets the floating shadow leave with the panel instead of popping away at unmount. */
|
|
86
|
+
@keyframes drawer-out { 65% { opacity: 1; } to { opacity: 0; transform: var(--drawer-from); } }
|
|
87
|
+
@keyframes drawer-fade-in { from { opacity: 0; } }
|
|
88
|
+
@keyframes drawer-fade-out { to { opacity: 0; } }
|
|
89
|
+
|
|
90
|
+
@media (prefers-reduced-motion: reduce) {
|
|
91
|
+
.keyframes.overlay[data-state="open"] { animation: overlay-in var(--duration-instant) linear both; }
|
|
92
|
+
.keyframes.overlay[data-state="closed"] { animation: overlay-out 100ms linear both; }
|
|
93
|
+
.keyframes.content[data-state="open"] { animation: drawer-fade-in var(--duration-instant) linear both; }
|
|
94
|
+
.keyframes.content[data-state="closed"] { animation: drawer-fade-out 100ms linear both; }
|
|
95
|
+
.close, .close:active { transition: none; }
|
|
96
|
+
.close:active { transform: none; }
|
|
97
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface DropdownItem {
|
|
3
|
+
label: string;
|
|
4
|
+
onSelect?: () => void;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
icon?: ReactNode;
|
|
7
|
+
destructive?: boolean;
|
|
8
|
+
separatorBefore?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface DropdownMenuProps {
|
|
11
|
+
label: string;
|
|
12
|
+
items: DropdownItem[];
|
|
13
|
+
icon?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
export declare function DropdownMenu({ label, items, icon }: DropdownMenuProps): import("react").JSX.Element;
|
|
16
|
+
export default DropdownMenu;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Fragment, useLayoutEffect, useRef, useState } 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 styles from "./dropdown-menu.module.css";
|
|
10
|
+
/** A new trigger label rises in while the old one leaves, and the trigger width springs to the measured text instead of snapping. */
|
|
11
|
+
function TriggerLabel({ text }) {
|
|
12
|
+
const reduced = useReducedMotion();
|
|
13
|
+
const measure = useRef(null);
|
|
14
|
+
const measured = useRef(null);
|
|
15
|
+
const [size, setSize] = useState({ width: "auto", animate: false });
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
const node = measure.current;
|
|
18
|
+
if (!node)
|
|
19
|
+
return;
|
|
20
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
21
|
+
if (!entry)
|
|
22
|
+
return;
|
|
23
|
+
const current = node.textContent;
|
|
24
|
+
// Only a text change morphs; the first measure and font swaps settle instantly.
|
|
25
|
+
const animate = measured.current !== null && measured.current !== current;
|
|
26
|
+
measured.current = current;
|
|
27
|
+
setSize({ width: Math.ceil(entry.borderBoxSize?.[0]?.inlineSize ?? node.offsetWidth), animate });
|
|
28
|
+
});
|
|
29
|
+
observer.observe(node);
|
|
30
|
+
return () => observer.disconnect();
|
|
31
|
+
}, []);
|
|
32
|
+
return _jsxs(motion.span, { className: styles.label, initial: false, animate: { width: size.width }, transition: size.animate && !reduced ? motionTokens.spring.morph : { duration: 0 }, children: [_jsx("span", { ref: measure, className: styles.labelMeasure, "aria-hidden": "true", children: text }), _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(motion.span, { className: styles.labelText, initial: reduced ? false : { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } }, transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }, children: text }, text) })] });
|
|
33
|
+
}
|
|
34
|
+
export function DropdownMenu({ label, items, icon }) {
|
|
35
|
+
const { highlight, reset, contentProps } = useMenuHighlight();
|
|
36
|
+
return _jsxs(DropdownPrimitive.Root, { onOpenChange: open => { if (open)
|
|
37
|
+
reset(); }, children: [_jsxs(DropdownPrimitive.Trigger, { className: styles.trigger, type: "button", children: [icon && _jsx("span", { className: styles.triggerIcon, "aria-hidden": "true", children: icon }), _jsx(TriggerLabel, { text: label }), _jsx(ChevronDown, { className: styles.chevron, size: 15, strokeWidth: 1.8, "aria-hidden": "true" })] }), _jsx(DropdownPrimitive.Portal, { children: _jsxs(DropdownPrimitive.Content, { className: styles.menu, sideOffset: 6, align: "end", collisionPadding: 12, loop: true, ...contentProps, children: [_jsx(MenuHighlight, { state: highlight, className: styles.highlight }), items.map((item, index) => _jsxs(Fragment, { children: [item.separatorBefore && _jsx(DropdownPrimitive.Separator, { className: styles.separator }), _jsxs(DropdownPrimitive.Item, { className: [styles.item, item.destructive ? styles.destructive : ""].filter(Boolean).join(" "), "data-tone": item.destructive ? "danger" : undefined, style: { "--i": index }, disabled: item.disabled, onSelect: item.onSelect, children: [item.icon && _jsx("span", { className: styles.icon, "aria-hidden": "true", children: item.icon }), item.label] })] }, item.label))] }) })] });
|
|
38
|
+
}
|
|
39
|
+
export default DropdownMenu;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
.trigger { display: inline-flex; max-width: 100%; min-height: var(--control-height-sm); align-items: center; gap: var(--space-related); padding: 0 var(--space-group); border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); font-weight: 500; cursor: pointer; transition: background-color var(--duration-instant) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
|
|
2
|
+
@media (hover: hover) and (pointer: fine) { .trigger:hover { border-color: var(--border-strong); background: var(--surface-muted); box-shadow: var(--shadow-resting); } }
|
|
3
|
+
/* The trigger anchors the menu, so press feedback stays in color. A scaled rect would be measured on pointerdown and shift the menu. */
|
|
4
|
+
.trigger[data-state="open"] { border-color: var(--border-strong); background: var(--surface-muted); box-shadow: none; }
|
|
5
|
+
.trigger:active { border-color: var(--border-strong); background: color-mix(in oklab, var(--surface-muted), var(--border) 55%); box-shadow: none; }
|
|
6
|
+
.item:focus-visible { outline: none; }
|
|
7
|
+
.triggerIcon { display: inline-flex; flex: none; color: var(--text-secondary); }
|
|
8
|
+
.label { position: relative; display: inline-flex; min-width: 0; flex: 0 1 auto; overflow-x: clip; }
|
|
9
|
+
.labelMeasure { position: absolute; top: 0; left: 0; visibility: hidden; white-space: nowrap; pointer-events: none; }
|
|
10
|
+
.labelText { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
11
|
+
.chevron { flex: none; color: var(--text-muted); transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard); }
|
|
12
|
+
.trigger[data-state="open"] .chevron { transform: rotate(180deg); }
|
|
13
|
+
/* The menu grows from the trigger edge: offset toward the trigger, scale from the Radix origin, fade quickly.
|
|
14
|
+
Transitions instead of keyframes, so a close that interrupts the open (or a reopen during the close) reverses from where it is. */
|
|
15
|
+
.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); background: var(--surface-raised); 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); }
|
|
16
|
+
.menu[data-side="top"] { --menu-y: 5px; }
|
|
17
|
+
.menu[data-side="left"] { --menu-x: 5px; --menu-y: 0px; }
|
|
18
|
+
.menu[data-side="right"] { --menu-x: -5px; --menu-y: 0px; }
|
|
19
|
+
@starting-style {
|
|
20
|
+
.menu[data-state="open"] { opacity: 0; transform: translate(var(--menu-x), var(--menu-y)) scale(.97); }
|
|
21
|
+
.menu[data-state="open"] .item { opacity: 0; transform: translate(calc(var(--menu-x) * .4), calc(var(--menu-y) * .4)); }
|
|
22
|
+
}
|
|
23
|
+
/* Radix unmounts when the exit animation ends, so a no-op keyframe times the exit while the transition does the visual work. */
|
|
24
|
+
.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; }
|
|
25
|
+
.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); }
|
|
26
|
+
.highlight[data-tone="danger"] { background: color-mix(in oklab, var(--danger) 8%, var(--surface)); }
|
|
27
|
+
.item { position: relative; display: flex; min-height: 36px; align-items: center; gap: 10px; padding: 0 11px; border-radius: calc(var(--radius-container) - 6px); color: var(--foreground); font-size: var(--text-sm); cursor: pointer; outline: none; transition: color var(--duration-fast) var(--ease-standard), 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); }
|
|
28
|
+
.icon { display: inline-flex; width: 17px; color: var(--text-secondary); }
|
|
29
|
+
.separator { height: 1px; margin: var(--space-tight) calc(var(--space-tight) * -1); background: var(--border-subtle); }
|
|
30
|
+
.destructive { color: var(--danger); }
|
|
31
|
+
.destructive .icon { color: var(--danger); }
|
|
32
|
+
.item[data-disabled] { opacity: .45; cursor: default; }
|
|
33
|
+
@keyframes menu-exit { to { --menu-exit: 1; } }
|
|
34
|
+
@media (prefers-reduced-transparency: reduce) { .menu { background: var(--surface-raised); backdrop-filter: none; -webkit-backdrop-filter: none; } }
|
|
35
|
+
@media (prefers-reduced-motion: reduce) {
|
|
36
|
+
.trigger, .chevron, .item, .highlight { transition: none; }
|
|
37
|
+
.menu, .menu[data-state="closed"] { transform: none; transition: opacity var(--duration-instant) linear; }
|
|
38
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface ElasticSliderMark {
|
|
3
|
+
value: number;
|
|
4
|
+
label?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ElasticSliderProps {
|
|
7
|
+
/** Visible label; it also names the thumb. */
|
|
8
|
+
label: string;
|
|
9
|
+
value?: number;
|
|
10
|
+
defaultValue?: number;
|
|
11
|
+
onValueChange?: (value: number) => void;
|
|
12
|
+
/** Runs once a drag is released or a key changes the value. */
|
|
13
|
+
onValueCommit?: (value: number) => void;
|
|
14
|
+
min?: number;
|
|
15
|
+
max?: number;
|
|
16
|
+
step?: number;
|
|
17
|
+
/** PageUp, PageDown, and Shift with an arrow move this far. Defaults to a tenth of the range. */
|
|
18
|
+
largeStep?: number;
|
|
19
|
+
/** Detents. A dragged thumb clicks into each one and holds for a few pixels before letting go. Labels print under the track. */
|
|
20
|
+
marks?: (number | ElasticSliderMark)[];
|
|
21
|
+
/** Formats the readout, the bubble, and the spoken value. */
|
|
22
|
+
format?: (value: number) => string;
|
|
23
|
+
/** Show the value beside the label. The bubble over the thumb still appears while dragging or using keys. */
|
|
24
|
+
showValue?: boolean;
|
|
25
|
+
/** Icons at either end. The one on the side being pulled past its limit is pushed outward with the track. */
|
|
26
|
+
startIcon?: ReactNode;
|
|
27
|
+
endIcon?: ReactNode;
|
|
28
|
+
/** Submits the value with a form. */
|
|
29
|
+
name?: string;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A single value slider made of something that gives. The thumb swells under the finger, the track stretches like rubber past either
|
|
35
|
+
* end and springs back with the release velocity, a value tag hangs off the thumb and sways with its speed, and marks act as detents
|
|
36
|
+
* the thumb clicks into. Arrows, Shift+arrows, PageUp, PageDown, Home, and End work as in a native range input.
|
|
37
|
+
*/
|
|
38
|
+
export declare function ElasticSlider({ label, value, defaultValue, onValueChange, onValueCommit, min, max, step: stepProp, largeStep, marks, format, showValue, startIcon, endIcon, name, disabled, className }: ElasticSliderProps): import("react").JSX.Element;
|
|
39
|
+
export default ElasticSlider;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, useSpring, useTransform, useVelocity } from "motion/react";
|
|
5
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
6
|
+
import styles from "./elastic-slider.module.css";
|
|
7
|
+
const { spring, duration, blur } = motionTokens;
|
|
8
|
+
const enterEase = [...motionTokens.ease.enter];
|
|
9
|
+
const exitEase = [...motionTokens.ease.standard];
|
|
10
|
+
/** Motion drops velocity on time-defined springs, so anything a gesture hands off to runs the same spring written as stiffness and damping. */
|
|
11
|
+
const physical = ({ visualDuration, bounce }, restDelta = .002) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta, restSpeed: restDelta * 10 }; };
|
|
12
|
+
const thumbSpring = physical(spring.snappy);
|
|
13
|
+
/** The stretch lets go with more life than anything else here: it is the one thing the user threw. */
|
|
14
|
+
const snapBack = physical({ visualDuration: .5, bounce: .42 }, .05);
|
|
15
|
+
/** Pixels the track may stretch past a limit, the pixel reach of a detent, and the speed a key press at a limit hits the wall with. */
|
|
16
|
+
const STRETCH = 26, DETENT_PX = 7, BUMP_PX = 260;
|
|
17
|
+
/** iOS style resistance: each pixel past the limit gives less, and never more than `limit`. */
|
|
18
|
+
const rubber = (distance, limit = STRETCH) => Math.sign(distance) * (1 - 1 / (Math.abs(distance) * .55 / limit + 1)) * limit;
|
|
19
|
+
const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
20
|
+
const decimalsOf = (value) => (String(value).split(".")[1] ?? "").length;
|
|
21
|
+
const bubbleIn = { opacity: 0, scale: .7, y: 6, filter: `blur(${blur.subtle}px)` };
|
|
22
|
+
const bubbleRest = { opacity: 1, scale: 1, y: 0, filter: "blur(0px)", transitionEnd: { filter: "none" } };
|
|
23
|
+
const bubbleOut = { opacity: 0, scale: .85, y: 4, filter: `blur(${blur.subtle}px)`, transition: { duration: duration.instant, ease: exitEase } };
|
|
24
|
+
const bubbleEnter = { ...spring.snappy, opacity: { duration: duration.fast, ease: enterEase }, filter: { duration: duration.fast, ease: enterEase } };
|
|
25
|
+
/**
|
|
26
|
+
* A single value slider made of something that gives. The thumb swells under the finger, the track stretches like rubber past either
|
|
27
|
+
* end and springs back with the release velocity, a value tag hangs off the thumb and sways with its speed, and marks act as detents
|
|
28
|
+
* the thumb clicks into. Arrows, Shift+arrows, PageUp, PageDown, Home, and End work as in a native range input.
|
|
29
|
+
*/
|
|
30
|
+
export function ElasticSlider({ label, value, defaultValue, onValueChange, onValueCommit, min = 0, max = 100, step: stepProp = 1, largeStep, marks, format, showValue = true, startIcon, endIcon, name, disabled, className }) {
|
|
31
|
+
const reduced = useReducedMotion() ?? false;
|
|
32
|
+
const labelId = useId();
|
|
33
|
+
const step = stepProp > 0 ? stepProp : 1;
|
|
34
|
+
const span = max - min || 1;
|
|
35
|
+
const decimals = Math.max(decimalsOf(step), decimalsOf(min));
|
|
36
|
+
const [internal, setInternal] = useState(() => clamp(defaultValue ?? min, min, max));
|
|
37
|
+
const current = value === undefined ? internal : clamp(value, min, max);
|
|
38
|
+
const formatValue = format ?? ((input) => input.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }));
|
|
39
|
+
const pct = (input) => ((input - min) / span) * 100;
|
|
40
|
+
const valueAt = (percent) => min + (percent / 100) * span;
|
|
41
|
+
const snap = (input) => clamp(Number((min + Math.round((input - min) / step) * step).toFixed(decimals)), min, max);
|
|
42
|
+
const markList = (marks ?? []).map(mark => typeof mark === "number" ? { value: mark } : mark).filter(mark => mark.value >= min && mark.value <= max);
|
|
43
|
+
const trackRef = useRef(null);
|
|
44
|
+
const thumbRef = useRef(null);
|
|
45
|
+
const tickRefs = useRef(new Map());
|
|
46
|
+
const latest = useRef(current);
|
|
47
|
+
const goal = useRef(pct(current));
|
|
48
|
+
const drag = useRef(null);
|
|
49
|
+
const width = useRef(1);
|
|
50
|
+
const lingerTimer = useRef(undefined);
|
|
51
|
+
const [dragging, setDragging] = useState(false);
|
|
52
|
+
const [keyed, setKeyed] = useState(false);
|
|
53
|
+
const [linger, setLinger] = useState(false);
|
|
54
|
+
// The thumb is a committed position plus a catch-up offset, so it can glide into a detent or to a pressed spot while the pointer
|
|
55
|
+
// keeps moving the position 1:1. `over` is the rubber-band stretch in pixels, signed by side.
|
|
56
|
+
const pos = useMotionValue(pct(current));
|
|
57
|
+
const lag = useMotionValue(0);
|
|
58
|
+
const over = useMotionValue(0);
|
|
59
|
+
const press = useMotionValue(0);
|
|
60
|
+
const shown = useTransform(() => pos.get() + lag.get());
|
|
61
|
+
const thumbX = useTransform(shown, at => `${at - 100}%`);
|
|
62
|
+
const clipPath = useTransform(shown, at => `inset(0 ${(100 - clamp(at, 0, 100)).toFixed(3)}% 0 0 round 999px)`);
|
|
63
|
+
// Stretch: the track grows away from its anchored end and thins as it does, like a band pulled tight. A press thickens it a little.
|
|
64
|
+
const scaleX = useTransform(over, px => 1 + Math.abs(px) / width.current);
|
|
65
|
+
const originX = useTransform(over, px => px < 0 ? 1 : 0);
|
|
66
|
+
const scaleY = useTransform(() => (1 + press.get() * .45) * (1 - (Math.abs(over.get()) / STRETCH) * .32));
|
|
67
|
+
const startShift = useTransform(over, px => Math.min(0, px));
|
|
68
|
+
const endShift = useTransform(over, px => Math.max(0, px));
|
|
69
|
+
// The tag trails the thumb on a soft spring, so it lags more the faster the thumb moves, and swings on its tail with that speed.
|
|
70
|
+
const trailed = useSpring(shown, { stiffness: 420, damping: 30, mass: .7 });
|
|
71
|
+
const bubbleAt = reduced ? shown : trailed;
|
|
72
|
+
const bubbleX = useTransform(bubbleAt, at => `${at - 100}%`);
|
|
73
|
+
const speed = useVelocity(trailed);
|
|
74
|
+
const rotate = useTransform(speed, velocity => reduced ? 0 : clamp((-velocity / 100) * width.current * .009, -11, 11));
|
|
75
|
+
const emit = (next) => {
|
|
76
|
+
if (latest.current === next)
|
|
77
|
+
return;
|
|
78
|
+
latest.current = next;
|
|
79
|
+
if (value === undefined)
|
|
80
|
+
setInternal(next);
|
|
81
|
+
onValueChange?.(next);
|
|
82
|
+
};
|
|
83
|
+
// Values from outside a drag (keys, props) spring the thumb to their place from wherever it is on screen.
|
|
84
|
+
useLayoutEffect(() => {
|
|
85
|
+
latest.current = current;
|
|
86
|
+
const target = pct(current);
|
|
87
|
+
if (drag.current || goal.current === target)
|
|
88
|
+
return;
|
|
89
|
+
goal.current = target;
|
|
90
|
+
const from = pos.get() + lag.get();
|
|
91
|
+
lag.jump(0);
|
|
92
|
+
pos.jump(from);
|
|
93
|
+
if (reduced)
|
|
94
|
+
pos.jump(target);
|
|
95
|
+
else
|
|
96
|
+
animate(pos, target, thumbSpring);
|
|
97
|
+
});
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const node = trackRef.current;
|
|
100
|
+
if (!node || typeof ResizeObserver === "undefined")
|
|
101
|
+
return;
|
|
102
|
+
const observer = new ResizeObserver(([entry]) => { if (entry)
|
|
103
|
+
width.current = entry.contentRect.width || 1; });
|
|
104
|
+
observer.observe(node);
|
|
105
|
+
return () => observer.disconnect();
|
|
106
|
+
}, []);
|
|
107
|
+
useEffect(() => () => window.clearTimeout(lingerTimer.current), []);
|
|
108
|
+
/** A detent answers with a small pop of its tick and, where the device supports it, a short tap of vibration. */
|
|
109
|
+
function pulse(mark, haptic) {
|
|
110
|
+
const node = tickRefs.current.get(mark);
|
|
111
|
+
if (node && !reduced)
|
|
112
|
+
animate(node, { scale: [1, 2.1, 1] }, { duration: .34, ease: enterEase });
|
|
113
|
+
if (haptic && typeof navigator !== "undefined" && "vibrate" in navigator)
|
|
114
|
+
navigator.vibrate?.(6);
|
|
115
|
+
}
|
|
116
|
+
/** Moves the thumb to a new resting spot through a spring on the catch-up offset, so the jump reads as travel. */
|
|
117
|
+
function glide(placed) {
|
|
118
|
+
const from = pos.get() + lag.get();
|
|
119
|
+
pos.set(placed);
|
|
120
|
+
if (reduced) {
|
|
121
|
+
lag.jump(0);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
lag.jump(from - placed);
|
|
125
|
+
animate(lag, 0, thumbSpring);
|
|
126
|
+
}
|
|
127
|
+
const detentAt = (percent) => {
|
|
128
|
+
let best = null, gap = DETENT_PX + 1;
|
|
129
|
+
for (const mark of markList) {
|
|
130
|
+
const distance = Math.abs(((percent - pct(mark.value)) / 100) * width.current);
|
|
131
|
+
if (distance <= DETENT_PX && distance < gap) {
|
|
132
|
+
best = mark.value;
|
|
133
|
+
gap = distance;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return best;
|
|
137
|
+
};
|
|
138
|
+
function follow(state, at, time, first = false) {
|
|
139
|
+
const raw = at - state.grab;
|
|
140
|
+
const edge = clamp(raw, 0, 100);
|
|
141
|
+
over.set(reduced ? 0 : rubber(((raw - edge) / 100) * width.current));
|
|
142
|
+
const detent = detentAt(edge);
|
|
143
|
+
const placed = detent === null ? edge : pct(detent);
|
|
144
|
+
if (first || detent !== state.detent) {
|
|
145
|
+
if (detent !== null && !first)
|
|
146
|
+
pulse(detent, state.type === "touch");
|
|
147
|
+
glide(placed);
|
|
148
|
+
}
|
|
149
|
+
else
|
|
150
|
+
pos.set(placed);
|
|
151
|
+
state.detent = detent;
|
|
152
|
+
state.edge = edge;
|
|
153
|
+
state.samples.push({ t: time, p: placed });
|
|
154
|
+
if (state.samples.length > 6)
|
|
155
|
+
state.samples.shift();
|
|
156
|
+
emit(detent ?? snap(valueAt(edge)));
|
|
157
|
+
}
|
|
158
|
+
function onPointerDown(event) {
|
|
159
|
+
if (disabled || event.button !== 0 || !event.isPrimary)
|
|
160
|
+
return;
|
|
161
|
+
const rect = trackRef.current?.getBoundingClientRect();
|
|
162
|
+
if (!rect)
|
|
163
|
+
return;
|
|
164
|
+
width.current = rect.width || 1;
|
|
165
|
+
const at = ((event.clientX - rect.left) / width.current) * 100;
|
|
166
|
+
const grabbed = !!event.target.closest("[data-thumb]");
|
|
167
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
168
|
+
const from = pos.get() + lag.get();
|
|
169
|
+
lag.jump(0);
|
|
170
|
+
pos.jump(from);
|
|
171
|
+
const state = { pointer: event.pointerId, grab: grabbed ? at - from : 0, samples: [], detent: null, edge: from, type: event.pointerType };
|
|
172
|
+
drag.current = state;
|
|
173
|
+
setDragging(true);
|
|
174
|
+
window.clearTimeout(lingerTimer.current);
|
|
175
|
+
setLinger(false);
|
|
176
|
+
thumbRef.current?.focus({ preventScroll: true });
|
|
177
|
+
if (reduced)
|
|
178
|
+
press.jump(1);
|
|
179
|
+
else
|
|
180
|
+
animate(press, 1, spring.snappy);
|
|
181
|
+
// A grabbed thumb stays put under the finger; a pressed track pulls the thumb over on a spring.
|
|
182
|
+
follow(state, grabbed ? from + state.grab : at, event.timeStamp, true);
|
|
183
|
+
}
|
|
184
|
+
function onPointerMove(event) {
|
|
185
|
+
const state = drag.current;
|
|
186
|
+
if (!state || state.pointer !== event.pointerId)
|
|
187
|
+
return;
|
|
188
|
+
const rect = trackRef.current?.getBoundingClientRect();
|
|
189
|
+
if (!rect)
|
|
190
|
+
return;
|
|
191
|
+
follow(state, ((event.clientX - rect.left) / (rect.width || 1)) * 100, event.timeStamp);
|
|
192
|
+
}
|
|
193
|
+
function onPointerEnd(event) {
|
|
194
|
+
const state = drag.current;
|
|
195
|
+
if (!state || state.pointer !== event.pointerId)
|
|
196
|
+
return;
|
|
197
|
+
drag.current = null;
|
|
198
|
+
const samples = state.samples, first = samples[0], last = samples[samples.length - 1];
|
|
199
|
+
const elapsed = last && first ? (last.t - first.t) / 1000 : 0;
|
|
200
|
+
const velocity = (elapsed > .008 && event.timeStamp - last.t < 60 ? (last.p - first.p) / elapsed : 0) + lag.getVelocity();
|
|
201
|
+
const next = state.detent ?? snap(valueAt(state.edge));
|
|
202
|
+
const from = pos.get() + lag.get(), target = pct(next);
|
|
203
|
+
lag.jump(0);
|
|
204
|
+
pos.jump(from);
|
|
205
|
+
goal.current = target;
|
|
206
|
+
if (reduced) {
|
|
207
|
+
pos.jump(target);
|
|
208
|
+
over.jump(0);
|
|
209
|
+
press.jump(0);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
animate(pos, target, { ...thumbSpring, velocity });
|
|
213
|
+
animate(over, 0, { ...snapBack, velocity: over.getVelocity() });
|
|
214
|
+
animate(press, 0, spring.snappy);
|
|
215
|
+
}
|
|
216
|
+
emit(next);
|
|
217
|
+
setDragging(false);
|
|
218
|
+
setLinger(true);
|
|
219
|
+
lingerTimer.current = window.setTimeout(() => setLinger(false), 700);
|
|
220
|
+
onValueCommit?.(next);
|
|
221
|
+
}
|
|
222
|
+
function onKeyDown(event) {
|
|
223
|
+
if (disabled)
|
|
224
|
+
return;
|
|
225
|
+
const now = latest.current;
|
|
226
|
+
const large = largeStep ?? Math.max(step, snap(min + span / 10) - min);
|
|
227
|
+
const moves = { ArrowRight: step, ArrowUp: step, ArrowLeft: -step, ArrowDown: -step, PageUp: large, PageDown: -large };
|
|
228
|
+
let wanted;
|
|
229
|
+
if (event.key === "Home")
|
|
230
|
+
wanted = min;
|
|
231
|
+
else if (event.key === "End")
|
|
232
|
+
wanted = max;
|
|
233
|
+
else if (event.key in moves)
|
|
234
|
+
wanted = now + (event.shiftKey && event.key.startsWith("Arrow") ? Math.sign(moves[event.key]) * large : moves[event.key]);
|
|
235
|
+
else
|
|
236
|
+
return;
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
setKeyed(true);
|
|
239
|
+
const next = snap(wanted);
|
|
240
|
+
// At a limit the track still stretches toward the press and snaps back, so the key visibly lands on something.
|
|
241
|
+
if (next === now) {
|
|
242
|
+
const toward = Math.sign(wanted - now);
|
|
243
|
+
if (toward && !reduced)
|
|
244
|
+
animate(over, 0, { ...snapBack, velocity: toward * BUMP_PX });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
emit(next);
|
|
248
|
+
if (markList.some(mark => mark.value === next))
|
|
249
|
+
pulse(next, false);
|
|
250
|
+
onValueCommit?.(next);
|
|
251
|
+
}
|
|
252
|
+
const text = formatValue(current);
|
|
253
|
+
const labelled = markList.filter(mark => mark.label);
|
|
254
|
+
const bubble = dragging || keyed || linger;
|
|
255
|
+
return _jsxs("div", { className: [styles.root, className].filter(Boolean).join(" "), "data-disabled": disabled || undefined, "data-dragging": dragging || undefined, "data-tag": bubble || undefined, "data-marks": labelled.length > 0 || undefined, children: [_jsxs("div", { className: styles.header, children: [_jsx("span", { id: labelId, className: styles.label, children: label }), showValue && _jsx("span", { className: styles.readout, "aria-hidden": "true", children: text })] }), _jsxs("div", { className: styles.body, children: [startIcon && _jsx(motion.span, { className: styles.start, style: { x: startShift }, "aria-hidden": "true", children: startIcon }), _jsxs("div", { className: styles.control, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerEnd, onPointerCancel: onPointerEnd, onLostPointerCapture: onPointerEnd, onMouseDown: event => event.preventDefault(), children: [_jsxs("div", { ref: trackRef, className: styles.rail, children: [_jsx(motion.div, { className: styles.track, style: { scaleX, scaleY, originX }, children: _jsx(motion.div, { className: styles.fill, style: { clipPath } }) }), markList.filter(mark => mark.value > min && mark.value < max).map(mark => _jsx("span", { ref: node => { if (node)
|
|
256
|
+
tickRefs.current.set(mark.value, node);
|
|
257
|
+
else
|
|
258
|
+
tickRefs.current.delete(mark.value); }, className: styles.tick, "data-on": mark.value <= current || undefined, style: { left: `${pct(mark.value)}%` } }, mark.value))] }), _jsxs("div", { className: styles.thumbs, children: [_jsx(motion.div, { className: styles.layer, style: { x: bubbleX }, children: _jsx(motion.span, { className: styles.bubbleAnchor, style: { x: over, "--at": bubbleAt }, children: _jsx(AnimatePresence, { children: bubble && _jsx(motion.span, { className: styles.bubble, "aria-hidden": "true", style: { rotate }, initial: reduced ? { opacity: 0 } : bubbleIn, animate: bubbleRest, exit: reduced ? { opacity: 0, transition: { duration: duration.instant } } : bubbleOut, transition: reduced ? { duration: .15 } : bubbleEnter, children: text }, "bubble") }) }) }), _jsx(motion.div, { className: styles.layer, style: { x: thumbX }, children: _jsx(motion.div, { className: styles.shift, style: { x: over }, children: _jsx(motion.div, { ref: thumbRef, role: "slider", "data-thumb": "", className: styles.thumb, tabIndex: disabled ? -1 : 0, "aria-labelledby": labelId, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": current, "aria-valuetext": text, "aria-orientation": "horizontal", "aria-disabled": disabled || undefined, initial: false, animate: { scale: dragging ? 1.28 : 1 }, transition: reduced ? { duration: 0 } : spring.snappy, onKeyDown: onKeyDown, onBlur: () => setKeyed(false) }) }) })] })] }), endIcon && _jsx(motion.span, { className: styles.end, style: { x: endShift }, "aria-hidden": "true", children: endIcon }), labelled.length > 0 && _jsx("div", { className: styles.marks, "aria-hidden": "true", children: labelled.map(mark => _jsx("span", { className: styles.markLabel, "data-on": mark.value === current || undefined, "data-edge": mark.value === min ? "start" : mark.value === max ? "end" : undefined, style: { left: `${pct(mark.value)}%` }, onClick: () => { if (disabled || mark.value === latest.current)
|
|
259
|
+
return; emit(mark.value); pulse(mark.value, false); onValueCommit?.(mark.value); }, children: mark.label }, mark.value)) })] }), name && _jsx("input", { type: "hidden", name: name, value: current, disabled: disabled })] });
|
|
260
|
+
}
|
|
261
|
+
export default ElasticSlider;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* Set --elastic-thumb and --elastic-track to resize. The rail is inset by half a thumb, so a thumb at either limit stays inside the box. */
|
|
2
|
+
.root { --elastic-thumb: 24px; --elastic-track: 8px; display: grid; width: 100%; min-width: 0; gap: var(--space-related); color: var(--foreground); font-size: var(--text-sm); line-height: var(--leading-body); letter-spacing: var(--tracking-body); user-select: none; -webkit-user-select: none; }
|
|
3
|
+
.header { display: flex; min-width: 0; align-items: baseline; justify-content: space-between; gap: var(--space-inset); }
|
|
4
|
+
.label { min-width: 0; font-weight: 500; }
|
|
5
|
+
.readout { flex: none; color: var(--text-secondary); font-variant-numeric: tabular-nums; white-space: nowrap; transition: opacity var(--duration-fast) var(--ease-standard); }
|
|
6
|
+
/* While the tag rides the thumb it carries the value, so the readout steps back instead of competing with it. */
|
|
7
|
+
.root[data-tag] .readout { opacity: 0; }
|
|
8
|
+
.body { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; }
|
|
9
|
+
.start, .end { display: flex; color: var(--text-muted); will-change: transform; }
|
|
10
|
+
.start { grid-area: 1 / 1; margin-right: var(--space-group); }
|
|
11
|
+
.end { grid-area: 1 / 3; margin-left: var(--space-group); }
|
|
12
|
+
/* The whole row takes the press; vertical pans still scroll the page on touch. */
|
|
13
|
+
.control { position: relative; grid-area: 1 / 2; height: var(--control-height-md); cursor: pointer; touch-action: pan-y; -webkit-tap-highlight-color: transparent; }
|
|
14
|
+
.rail { position: absolute; top: 50%; right: calc(var(--elastic-thumb) / 2); left: calc(var(--elastic-thumb) / 2); height: var(--elastic-track); margin-top: calc(var(--elastic-track) / -2); }
|
|
15
|
+
/* Only the band stretches. Ticks, thumb, and tag ride along without distorting. */
|
|
16
|
+
.track { position: absolute; inset: 0; border-radius: var(--radius-full); corner-shape: round; background: var(--control-track); will-change: transform; }
|
|
17
|
+
.fill { position: absolute; inset: 0; border-radius: inherit; corner-shape: inherit; background: var(--control-fill); }
|
|
18
|
+
.tick { position: absolute; top: 50%; width: 4px; height: 4px; margin: -2px 0 0 -2px; border-radius: var(--radius-full); corner-shape: round; background: color-mix(in oklab, var(--foreground) 32%, transparent); pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); }
|
|
19
|
+
.tick[data-on] { background: color-mix(in oklab, var(--control-glyph) 60%, transparent); }
|
|
20
|
+
.thumbs { position: absolute; inset: 0 calc(var(--elastic-thumb) / 2); container-type: inline-size; pointer-events: none; }
|
|
21
|
+
/* Each layer is as wide as the rail and slides by a percentage of itself, so placing the thumb needs no measuring. */
|
|
22
|
+
.layer { position: absolute; inset: 0; }
|
|
23
|
+
.shift { position: absolute; inset: 0; }
|
|
24
|
+
.thumb { position: absolute; top: 50%; right: 0; box-sizing: border-box; width: var(--elastic-thumb); height: var(--elastic-thumb); translate: 50% -50%; border: 0; border-radius: var(--radius-full); corner-shape: round; background: var(--control-thumb); box-shadow: var(--control-thumb-shadow); pointer-events: auto; cursor: grab; outline: none; transition: box-shadow var(--duration-fast) var(--ease-standard); }
|
|
25
|
+
/* A generous invisible hit area for fingertips. */
|
|
26
|
+
.thumb::after { content: ""; position: absolute; inset: -12px; border-radius: inherit; corner-shape: inherit; }
|
|
27
|
+
.root[data-dragging], .root[data-dragging] .control, .root[data-dragging] .thumb { cursor: grabbing; }
|
|
28
|
+
/* The tag centers on the thumb until it would pass the component's edge, then holds there. It hangs from its bottom edge, so a swing pivots on the thumb. */
|
|
29
|
+
.bubbleAnchor { --edge: calc(var(--elastic-thumb) / 2); --x: calc(var(--at, 0) * 1cqw); position: absolute; right: 0; bottom: calc(50% + var(--elastic-thumb) / 2 + 10px); display: flex; translate: clamp(calc(100% - var(--edge) - var(--x)), 50%, calc(100cqw + var(--edge) - var(--x))) 0; pointer-events: none; }
|
|
30
|
+
@media (pointer: coarse) { .bubbleAnchor { bottom: calc(50% + var(--elastic-thumb) / 2 + 20px); } }
|
|
31
|
+
.bubble { display: inline-flex; min-height: 1.75rem; align-items: center; padding: 0 var(--space-group); border-radius: var(--radius-full); corner-shape: round; background: var(--foreground); color: var(--background); box-shadow: var(--shadow-raised); font-size: var(--text-sm); font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; transform-origin: 50% 130%; }
|
|
32
|
+
.marks { position: relative; grid-area: 2 / 2; height: 1.25rem; margin: 0 calc(var(--elastic-thumb) / 2); color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
|
|
33
|
+
.markLabel { position: absolute; top: 0; translate: -50% 0; white-space: nowrap; cursor: pointer; transition: color var(--duration-fast) var(--ease-standard); }
|
|
34
|
+
.markLabel[data-edge="start"] { translate: max(-50%, calc(var(--elastic-thumb) / -2)) 0; }
|
|
35
|
+
.markLabel[data-edge="end"] { translate: min(-50%, calc(-100% + var(--elastic-thumb) / 2)) 0; }
|
|
36
|
+
.markLabel[data-on] { color: var(--foreground); }
|
|
37
|
+
@media (hover: hover) and (pointer: fine) { .markLabel:hover { color: var(--foreground); } }
|
|
38
|
+
.root[data-disabled] { opacity: .5; }
|
|
39
|
+
.root[data-disabled] .control, .root[data-disabled] .thumb, .root[data-disabled] .markLabel { cursor: not-allowed; }
|
|
40
|
+
@media (prefers-reduced-motion: reduce) { .thumb, .tick, .markLabel { transition: none; } }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface EmptyStateProps {
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
action?: ReactNode;
|
|
6
|
+
icon?: ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
/** Optional accessible label for the state region. */
|
|
9
|
+
label?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function EmptyState({ title, description, action, icon, className, label }: EmptyStateProps): import("react").JSX.Element;
|
|
12
|
+
export default EmptyState;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { isValidElement, useEffect, useLayoutEffect, useRef } from "react";
|
|
4
|
+
import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion } from "motion/react";
|
|
5
|
+
import { Folder } from "lucide-react";
|
|
6
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
7
|
+
import styles from "./empty-state.module.css";
|
|
8
|
+
const exitFast = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };
|
|
9
|
+
const textIn = { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` };
|
|
10
|
+
const textOut = { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: exitFast };
|
|
11
|
+
const iconIn = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };
|
|
12
|
+
const shown = { opacity: 1, y: "0em", scale: 1, filter: "blur(0px)" };
|
|
13
|
+
const fadeOut = { opacity: 0, transition: { duration: motionTokens.duration.instant } };
|
|
14
|
+
/** Outgoing copies are hidden from assistive tech while they fade. */
|
|
15
|
+
function Swap(props) {
|
|
16
|
+
const present = useIsPresent();
|
|
17
|
+
return _jsx(motion.span, { ...props, "aria-hidden": present ? props["aria-hidden"] : true });
|
|
18
|
+
}
|
|
19
|
+
/** A new icon component crossfades in; re-rendering the same icon stays still. */
|
|
20
|
+
function iconKey(icon) {
|
|
21
|
+
if (!isValidElement(icon))
|
|
22
|
+
return "icon";
|
|
23
|
+
const type = icon.type;
|
|
24
|
+
return typeof type === "string" ? type : type.displayName ?? type.name ?? "icon";
|
|
25
|
+
}
|
|
26
|
+
/** Follows its content height. After `morphKey` changes, the height springs from the old size to the new one and then returns to auto, so passive reflows (a resize, a font swap) follow instantly. It clips only while moving, so focus rings stay visible at rest. */
|
|
27
|
+
function HeightFrame({ reduce, morphKey, children }) {
|
|
28
|
+
const frame = useRef(null);
|
|
29
|
+
const content = useRef(null);
|
|
30
|
+
const height = useMotionValue("auto");
|
|
31
|
+
const changedAt = useRef(0);
|
|
32
|
+
useLayoutEffect(() => { changedAt.current = performance.now(); }, [morphKey]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const node = content.current;
|
|
35
|
+
if (!node || typeof ResizeObserver === "undefined")
|
|
36
|
+
return;
|
|
37
|
+
let last;
|
|
38
|
+
let controls;
|
|
39
|
+
const settle = () => { height.jump("auto"); if (frame.current)
|
|
40
|
+
Object.assign(frame.current.style, { overflow: "", height: "auto" }); };
|
|
41
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
42
|
+
if (!entry)
|
|
43
|
+
return;
|
|
44
|
+
const next = entry.borderBoxSize?.[0]?.blockSize ?? node.offsetHeight;
|
|
45
|
+
const current = height.get();
|
|
46
|
+
const from = typeof current === "number" ? current : last;
|
|
47
|
+
last = next;
|
|
48
|
+
controls?.stop();
|
|
49
|
+
if (reduce || from === undefined || from === next || performance.now() - changedAt.current > 120)
|
|
50
|
+
return settle();
|
|
51
|
+
// Pin the old height before this frame paints, then spring to the new one.
|
|
52
|
+
if (frame.current)
|
|
53
|
+
Object.assign(frame.current.style, { overflow: "hidden", height: `${from}px` });
|
|
54
|
+
controls = animate(height, [from, next], { ...motionTokens.spring.smooth, onComplete: settle });
|
|
55
|
+
});
|
|
56
|
+
observer.observe(node);
|
|
57
|
+
return () => { observer.disconnect(); controls?.stop(); };
|
|
58
|
+
}, [height, reduce]);
|
|
59
|
+
return _jsx(motion.div, { ref: frame, className: styles.frame, style: { height }, children: _jsx("div", { ref: content, className: styles.copy, children: children }) });
|
|
60
|
+
}
|
|
61
|
+
export function EmptyState({ title, description, action, icon, className, label }) {
|
|
62
|
+
const reduce = useReducedMotion();
|
|
63
|
+
const glyph = icon ?? _jsx(Folder, { width: 24, height: 24, strokeWidth: 1.5 });
|
|
64
|
+
const enter = reduce ? { duration: motionTokens.duration.instant } : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] };
|
|
65
|
+
const swap = { initial: reduce ? { opacity: 0 } : textIn, animate: shown, exit: reduce ? fadeOut : textOut, transition: enter };
|
|
66
|
+
// The result of an action morphs in place: the icon crossfades and the copy rises in while the old copy leaves.
|
|
67
|
+
return _jsxs("section", { className: [styles.root, className].filter(Boolean).join(" "), "aria-label": label, children: [_jsx("div", { className: styles.icon, "aria-hidden": "true", children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(Swap, { className: styles.glyph, initial: reduce ? { opacity: 0 } : iconIn, animate: shown, exit: reduce ? fadeOut : { ...iconIn, transition: exitFast }, transition: reduce ? enter : motionTokens.spring.snappy, children: glyph }, iconKey(glyph)) }) }), _jsxs(HeightFrame, { reduce: reduce, morphKey: `${title}\n${description}`, children: [_jsx("h3", { children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(Swap, { className: styles.line, ...swap, children: title }, title) }) }), _jsx("p", { children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(Swap, { className: styles.line, ...swap, children: description }, description) }) })] }), action && _jsx("div", { className: styles.action, children: action })] });
|
|
68
|
+
}
|
|
69
|
+
export default EmptyState;
|