@teamofsilicons/silicon-ui 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +63 -2
- package/components/accordion/accordion.d.ts +13 -0
- package/components/accordion/accordion.js +35 -0
- package/components/accordion/accordion.module.css +15 -0
- package/components/action-button/action-button.d.ts +11 -0
- package/components/action-button/action-button.js +57 -0
- package/components/action-button/action-button.module.css +15 -0
- package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
- package/components/activity-heatmap/activity-heatmap.js +303 -0
- package/components/activity-heatmap/activity-heatmap.module.css +83 -0
- package/components/alert/alert.d.ts +13 -0
- package/components/alert/alert.js +79 -0
- package/components/alert/alert.module.css +15 -0
- package/components/animated-counter/animated-counter.d.ts +13 -0
- package/components/animated-counter/animated-counter.js +73 -0
- package/components/animated-counter/animated-counter.module.css +12 -0
- package/components/announcement-bar/announcement-bar.d.ts +53 -0
- package/components/announcement-bar/announcement-bar.js +187 -0
- package/components/announcement-bar/announcement-bar.module.css +60 -0
- package/components/avatar/avatar.d.ts +8 -0
- package/components/avatar/avatar.js +18 -0
- package/components/avatar/avatar.module.css +60 -0
- package/components/avatar-group/avatar-group.d.ts +13 -0
- package/components/avatar-group/avatar-group.js +24 -0
- package/components/avatar-group/avatar-group.module.css +27 -0
- package/components/badge/badge.d.ts +10 -0
- package/components/badge/badge.js +66 -0
- package/components/badge/badge.module.css +52 -0
- package/components/bar-chart/bar-chart.d.ts +32 -0
- package/components/bar-chart/bar-chart.js +327 -0
- package/components/bar-chart/bar-chart.module.css +49 -0
- package/components/billing-toggle/billing-toggle.d.ts +35 -0
- package/components/billing-toggle/billing-toggle.js +83 -0
- package/components/billing-toggle/billing-toggle.module.css +139 -0
- package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
- package/components/bottom-sheet/bottom-sheet.js +386 -0
- package/components/bottom-sheet/bottom-sheet.module.css +69 -0
- package/components/breadcrumb/breadcrumb.d.ts +13 -0
- package/components/breadcrumb/breadcrumb.js +19 -0
- package/components/breadcrumb/breadcrumb.module.css +12 -0
- package/components/brush-chart/brush-chart.d.ts +45 -0
- package/components/brush-chart/brush-chart.js +426 -0
- package/components/brush-chart/brush-chart.module.css +69 -0
- package/components/button/button.d.ts +10 -0
- package/components/button/button.js +97 -0
- package/components/button/button.module.css +39 -0
- package/components/button-group/button-group.d.ts +59 -0
- package/components/button-group/button-group.js +280 -0
- package/components/button-group/button-group.module.css +87 -0
- package/components/button-link/button-link.d.ts +24 -0
- package/components/button-link/button-link.js +11 -0
- package/components/button-link/button-link.module.css +7 -0
- package/components/calendar/calendar.d.ts +22 -0
- package/components/calendar/calendar.js +253 -0
- package/components/calendar/calendar.module.css +51 -0
- package/components/card/card.d.ts +19 -0
- package/components/card/card.js +108 -0
- package/components/card/card.module.css +51 -0
- package/components/card-stack/card-stack.d.ts +34 -0
- package/components/card-stack/card-stack.js +291 -0
- package/components/card-stack/card-stack.module.css +63 -0
- package/components/carousel/carousel.d.ts +28 -0
- package/components/carousel/carousel.js +332 -0
- package/components/carousel/carousel.module.css +42 -0
- package/components/chat-thread/chat-thread.d.ts +135 -0
- package/components/chat-thread/chat-thread.js +406 -0
- package/components/chat-thread/chat-thread.module.css +242 -0
- package/components/checkbox/checkbox.d.ts +7 -0
- package/components/checkbox/checkbox.js +23 -0
- package/components/checkbox/checkbox.module.css +15 -0
- package/components/chip-group/chip-group.d.ts +24 -0
- package/components/chip-group/chip-group.js +196 -0
- package/components/chip-group/chip-group.module.css +38 -0
- package/components/code-block/code-block.d.ts +11 -0
- package/components/code-block/code-block.js +133 -0
- package/components/code-block/code-block.module.css +43 -0
- package/components/color-picker/color-picker.d.ts +40 -0
- package/components/color-picker/color-picker.js +449 -0
- package/components/color-picker/color-picker.module.css +111 -0
- package/components/combobox/combobox.d.ts +19 -0
- package/components/combobox/combobox.js +167 -0
- package/components/combobox/combobox.module.css +28 -0
- package/components/command-palette/command-palette.d.ts +20 -0
- package/components/command-palette/command-palette.js +151 -0
- package/components/command-palette/command-palette.module.css +94 -0
- package/components/comment-thread/comment-thread.d.ts +120 -0
- package/components/comment-thread/comment-thread.js +343 -0
- package/components/comment-thread/comment-thread.module.css +195 -0
- package/components/confirm-morph/confirm-morph.d.ts +65 -0
- package/components/confirm-morph/confirm-morph.js +355 -0
- package/components/confirm-morph/confirm-morph.module.css +146 -0
- package/components/context-menu/context-menu.d.ts +18 -0
- package/components/context-menu/context-menu.js +148 -0
- package/components/context-menu/context-menu.module.css +17 -0
- package/components/copy-button/copy-button.d.ts +10 -0
- package/components/copy-button/copy-button.js +69 -0
- package/components/copy-button/copy-button.module.css +21 -0
- package/components/countdown/countdown.d.ts +31 -0
- package/components/countdown/countdown.js +177 -0
- package/components/countdown/countdown.module.css +49 -0
- package/components/date-picker/date-picker.d.ts +17 -0
- package/components/date-picker/date-picker.js +100 -0
- package/components/date-picker/date-picker.module.css +34 -0
- package/components/date-range-picker/date-range-picker.d.ts +44 -0
- package/components/date-range-picker/date-range-picker.js +534 -0
- package/components/date-range-picker/date-range-picker.module.css +214 -0
- package/components/dialog/dialog.d.ts +11 -0
- package/components/dialog/dialog.js +52 -0
- package/components/dialog/dialog.module.css +36 -0
- package/components/donut-chart/donut-chart.d.ts +46 -0
- package/components/donut-chart/donut-chart.js +382 -0
- package/components/donut-chart/donut-chart.module.css +48 -0
- package/components/drawer/drawer.d.ts +20 -0
- package/components/drawer/drawer.js +111 -0
- package/components/drawer/drawer.module.css +97 -0
- package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
- package/components/dropdown-menu/dropdown-menu.js +39 -0
- package/components/dropdown-menu/dropdown-menu.module.css +38 -0
- package/components/elastic-slider/elastic-slider.d.ts +39 -0
- package/components/elastic-slider/elastic-slider.js +261 -0
- package/components/elastic-slider/elastic-slider.module.css +40 -0
- package/components/empty-state/empty-state.d.ts +12 -0
- package/components/empty-state/empty-state.js +69 -0
- package/components/empty-state/empty-state.module.css +13 -0
- package/components/expandable-card/expandable-card.d.ts +13 -0
- package/components/expandable-card/expandable-card.js +89 -0
- package/components/expandable-card/expandable-card.module.css +22 -0
- package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
- package/components/expanding-button-group/expanding-button-group.js +258 -0
- package/components/expanding-button-group/expanding-button-group.module.css +44 -0
- package/components/expanding-search/expanding-search.d.ts +38 -0
- package/components/expanding-search/expanding-search.js +242 -0
- package/components/expanding-search/expanding-search.module.css +52 -0
- package/components/file-dropzone/file-dropzone.d.ts +44 -0
- package/components/file-dropzone/file-dropzone.js +379 -0
- package/components/file-dropzone/file-dropzone.module.css +115 -0
- package/components/file-upload/file-upload.d.ts +22 -0
- package/components/file-upload/file-upload.js +192 -0
- package/components/file-upload/file-upload.module.css +49 -0
- package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
- package/components/filter-toolbar/filter-toolbar.js +446 -0
- package/components/filter-toolbar/filter-toolbar.module.css +70 -0
- package/components/floating-button-group/floating-button-group.d.ts +51 -0
- package/components/floating-button-group/floating-button-group.js +215 -0
- package/components/floating-button-group/floating-button-group.module.css +74 -0
- package/components/gauge/gauge.d.ts +18 -0
- package/components/gauge/gauge.js +101 -0
- package/components/gauge/gauge.module.css +22 -0
- package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
- package/components/hold-to-confirm/hold-to-confirm.js +165 -0
- package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
- package/components/hover-card/hover-card.d.ts +38 -0
- package/components/hover-card/hover-card.js +145 -0
- package/components/hover-card/hover-card.module.css +20 -0
- package/components/image-compare/image-compare.d.ts +27 -0
- package/components/image-compare/image-compare.js +267 -0
- package/components/image-compare/image-compare.module.css +25 -0
- package/components/in-view-title/in-view-title.d.ts +17 -0
- package/components/in-view-title/in-view-title.js +70 -0
- package/components/in-view-title/in-view-title.module.css +13 -0
- package/components/inline-edit/inline-edit.d.ts +27 -0
- package/components/inline-edit/inline-edit.js +315 -0
- package/components/inline-edit/inline-edit.module.css +73 -0
- package/components/input/input.d.ts +7 -0
- package/components/input/input.js +60 -0
- package/components/input/input.module.css +22 -0
- package/components/json-viewer/json-viewer.d.ts +48 -0
- package/components/json-viewer/json-viewer.js +463 -0
- package/components/json-viewer/json-viewer.module.css +92 -0
- package/components/line-chart/line-chart.d.ts +58 -0
- package/components/line-chart/line-chart.js +383 -0
- package/components/line-chart/line-chart.module.css +60 -0
- package/components/mention-input/mention-input.d.ts +72 -0
- package/components/mention-input/mention-input.js +417 -0
- package/components/mention-input/mention-input.module.css +134 -0
- package/components/metric-card/metric-card.d.ts +8 -0
- package/components/metric-card/metric-card.js +44 -0
- package/components/metric-card/metric-card.module.css +18 -0
- package/components/money-input/money-input.d.ts +48 -0
- package/components/money-input/money-input.js +503 -0
- package/components/money-input/money-input.module.css +77 -0
- package/components/morph-select/morph-select.d.ts +50 -0
- package/components/morph-select/morph-select.js +395 -0
- package/components/morph-select/morph-select.module.css +88 -0
- package/components/multi-select/multi-select.d.ts +18 -0
- package/components/multi-select/multi-select.js +105 -0
- package/components/multi-select/multi-select.module.css +37 -0
- package/components/notification-center/notification-center.d.ts +24 -0
- package/components/notification-center/notification-center.js +146 -0
- package/components/notification-center/notification-center.module.css +79 -0
- package/components/number-field/number-field.d.ts +41 -0
- package/components/number-field/number-field.js +455 -0
- package/components/number-field/number-field.module.css +71 -0
- package/components/otp-input/otp-input.d.ts +13 -0
- package/components/otp-input/otp-input.js +129 -0
- package/components/otp-input/otp-input.module.css +28 -0
- package/components/pagination/pagination.d.ts +7 -0
- package/components/pagination/pagination.js +60 -0
- package/components/pagination/pagination.module.css +16 -0
- package/components/password-field/password-field.d.ts +14 -0
- package/components/password-field/password-field.js +51 -0
- package/components/password-field/password-field.module.css +27 -0
- package/components/password-strength/password-strength.d.ts +107 -0
- package/components/password-strength/password-strength.js +161 -0
- package/components/password-strength/password-strength.module.css +57 -0
- package/components/phone-input/phone-input.d.ts +73 -0
- package/components/phone-input/phone-input.js +612 -0
- package/components/phone-input/phone-input.module.css +98 -0
- package/components/popover/popover.d.ts +6 -0
- package/components/popover/popover.js +19 -0
- package/components/popover/popover.module.css +32 -0
- package/components/progress/progress.d.ts +8 -0
- package/components/progress/progress.js +42 -0
- package/components/progress/progress.module.css +1 -0
- package/components/radio-cards/radio-cards.d.ts +35 -0
- package/components/radio-cards/radio-cards.js +110 -0
- package/components/radio-cards/radio-cards.module.css +84 -0
- package/components/radio-group/radio-group.d.ts +13 -0
- package/components/radio-group/radio-group.js +55 -0
- package/components/radio-group/radio-group.module.css +30 -0
- package/components/resizable-panels/resizable-panels.d.ts +37 -0
- package/components/resizable-panels/resizable-panels.js +338 -0
- package/components/resizable-panels/resizable-panels.module.css +40 -0
- package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
- package/components/rich-text-editor/rich-text-editor.js +1733 -0
- package/components/rich-text-editor/rich-text-editor.module.css +201 -0
- package/components/ridgeline/ridgeline.d.ts +37 -0
- package/components/ridgeline/ridgeline.js +346 -0
- package/components/ridgeline/ridgeline.module.css +58 -0
- package/components/scroll-area/scroll-area.d.ts +40 -0
- package/components/scroll-area/scroll-area.js +153 -0
- package/components/scroll-area/scroll-area.module.css +79 -0
- package/components/search-field/search-field.d.ts +7 -0
- package/components/search-field/search-field.js +20 -0
- package/components/search-field/search-field.module.css +17 -0
- package/components/segmented-control/segmented-control.d.ts +16 -0
- package/components/segmented-control/segmented-control.js +56 -0
- package/components/segmented-control/segmented-control.module.css +15 -0
- package/components/select/select.d.ts +15 -0
- package/components/select/select.js +36 -0
- package/components/select/select.module.css +35 -0
- package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
- package/components/shortcut-recorder/shortcut-recorder.js +380 -0
- package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
- package/components/signature-pad/signature-pad.d.ts +72 -0
- package/components/signature-pad/signature-pad.js +450 -0
- package/components/signature-pad/signature-pad.module.css +84 -0
- package/components/skeleton/skeleton.d.ts +13 -0
- package/components/skeleton/skeleton.js +53 -0
- package/components/skeleton/skeleton.module.css +11 -0
- package/components/slider/slider.d.ts +45 -0
- package/components/slider/slider.js +347 -0
- package/components/slider/slider.module.css +45 -0
- package/components/slope-chart/slope-chart.d.ts +38 -0
- package/components/slope-chart/slope-chart.js +183 -0
- package/components/slope-chart/slope-chart.module.css +51 -0
- package/components/slot-text/slot-text.d.ts +26 -0
- package/components/slot-text/slot-text.js +119 -0
- package/components/slot-text/slot-text.module.css +19 -0
- package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
- package/components/sortable-data-table/sortable-data-table.js +240 -0
- package/components/sortable-data-table/sortable-data-table.module.css +116 -0
- package/components/sparkline/sparkline.d.ts +19 -0
- package/components/sparkline/sparkline.js +239 -0
- package/components/sparkline/sparkline.module.css +26 -0
- package/components/split-button/split-button.d.ts +19 -0
- package/components/split-button/split-button.js +33 -0
- package/components/split-button/split-button.module.css +51 -0
- package/components/stepper/stepper.d.ts +36 -0
- package/components/stepper/stepper.js +108 -0
- package/components/stepper/stepper.module.css +74 -0
- package/components/streamgraph/streamgraph.d.ts +51 -0
- package/components/streamgraph/streamgraph.js +376 -0
- package/components/streamgraph/streamgraph.module.css +55 -0
- package/components/swipe-actions/swipe-actions.d.ts +35 -0
- package/components/swipe-actions/swipe-actions.js +330 -0
- package/components/swipe-actions/swipe-actions.module.css +64 -0
- package/components/switch/switch.d.ts +7 -0
- package/components/switch/switch.js +45 -0
- package/components/switch/switch.module.css +12 -0
- package/components/tabs/tabs.d.ts +8 -0
- package/components/tabs/tabs.js +138 -0
- package/components/tabs/tabs.module.css +27 -0
- package/components/tag-input/tag-input.d.ts +15 -0
- package/components/tag-input/tag-input.js +150 -0
- package/components/tag-input/tag-input.module.css +34 -0
- package/components/text-morph/text-morph.d.ts +13 -0
- package/components/text-morph/text-morph.js +69 -0
- package/components/text-morph/text-morph.module.css +8 -0
- package/components/text-reveal/text-reveal.d.ts +14 -0
- package/components/text-reveal/text-reveal.js +20 -0
- package/components/text-reveal/text-reveal.module.css +13 -0
- package/components/text-shimmer/text-shimmer.d.ts +18 -0
- package/components/text-shimmer/text-shimmer.js +90 -0
- package/components/text-shimmer/text-shimmer.module.css +9 -0
- package/components/textarea/textarea.d.ts +7 -0
- package/components/textarea/textarea.js +56 -0
- package/components/textarea/textarea.module.css +22 -0
- package/components/theme-switch/theme-switch.d.ts +11 -0
- package/components/theme-switch/theme-switch.js +37 -0
- package/components/theme-switch/theme-switch.module.css +31 -0
- package/components/time-picker/time-picker.d.ts +13 -0
- package/components/time-picker/time-picker.js +95 -0
- package/components/time-picker/time-picker.module.css +23 -0
- package/components/timeline/timeline.d.ts +47 -0
- package/components/timeline/timeline.js +171 -0
- package/components/timeline/timeline.module.css +51 -0
- package/components/toast/toast.d.ts +8 -0
- package/components/toast/toast.js +118 -0
- package/components/toast/toast.module.css +100 -0
- package/components/toast-stack/toast-stack.d.ts +57 -0
- package/components/toast-stack/toast-stack.js +399 -0
- package/components/toast-stack/toast-stack.module.css +61 -0
- package/components/tooltip/tooltip.d.ts +8 -0
- package/components/tooltip/tooltip.js +62 -0
- package/components/tooltip/tooltip.module.css +18 -0
- package/components/tree-view/tree-view.d.ts +17 -0
- package/components/tree-view/tree-view.js +147 -0
- package/components/tree-view/tree-view.module.css +22 -0
- package/components/treemap/treemap.d.ts +35 -0
- package/components/treemap/treemap.js +375 -0
- package/components/treemap/treemap.module.css +59 -0
- package/components/usage-meter/usage-meter.d.ts +24 -0
- package/components/usage-meter/usage-meter.js +296 -0
- package/components/usage-meter/usage-meter.module.css +61 -0
- package/components/user-menu/user-menu.d.ts +62 -0
- package/components/user-menu/user-menu.js +316 -0
- package/components/user-menu/user-menu.module.css +97 -0
- package/components/waffle-chart/waffle-chart.d.ts +39 -0
- package/components/waffle-chart/waffle-chart.js +244 -0
- package/components/waffle-chart/waffle-chart.module.css +52 -0
- package/components.json +1330 -0
- package/lib/escape.d.ts +19 -0
- package/lib/escape.js +50 -0
- package/lib/media.d.ts +353 -0
- package/lib/media.js +65 -0
- package/lib/menu-highlight.d.ts +28 -0
- package/lib/menu-highlight.js +43 -0
- package/lib/motion-tokens.d.ts +60 -0
- package/lib/motion-tokens.js +25 -0
- package/lib/return-focus.d.ts +9 -0
- package/lib/return-focus.js +53 -0
- package/lib/silicon-provider.d.ts +60 -0
- package/lib/silicon-provider.js +47 -0
- package/lib/use-copy-feedback.d.ts +8 -0
- package/lib/use-copy-feedback.js +36 -0
- package/lib/use-today.d.ts +2 -0
- package/lib/use-today.js +29 -0
- package/package.json +66 -4
- package/styles.css +218 -0
- package/styles.css.d.ts +1 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "motion/react";
|
|
5
|
+
import { Check, RotateCcw, X } from "lucide-react";
|
|
6
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
7
|
+
import styles from "./card-stack.module.css";
|
|
8
|
+
/** Cards rendered in the stack. The last one waits invisibly one step behind and fades in as it rises. */
|
|
9
|
+
const VISIBLE = 4;
|
|
10
|
+
/** How far each card behind peeks out below the one in front, and how much smaller it is. */
|
|
11
|
+
const PEEK = 12;
|
|
12
|
+
const SHRINK = .05;
|
|
13
|
+
/** How far the cards behind rise while the top card is dragged away, in stack steps. */
|
|
14
|
+
const LIFT = .4;
|
|
15
|
+
/** Drag distance at which the cards behind have fully risen. */
|
|
16
|
+
const LIFT_DISTANCE = 160;
|
|
17
|
+
/** Degrees of rotation per pixel of horizontal travel. */
|
|
18
|
+
const ROTATE = .065;
|
|
19
|
+
/** A release faster than this (px/s) throws the card even when it has only moved a little. */
|
|
20
|
+
const FLICK = 500;
|
|
21
|
+
/** Vertical travel is not a decision, so past a small free zone it resists like a rubber band. */
|
|
22
|
+
const FREE_Y = 48;
|
|
23
|
+
const STRETCH_Y = 140;
|
|
24
|
+
const noop = () => () => { };
|
|
25
|
+
const resistY = (raw) => {
|
|
26
|
+
const distance = Math.abs(raw);
|
|
27
|
+
return distance <= FREE_Y ? raw : Math.sign(raw) * (FREE_Y + (1 - 1 / ((distance - FREE_Y) * .55 / STRETCH_Y + 1)) * STRETCH_Y);
|
|
28
|
+
};
|
|
29
|
+
const unresistY = (shown) => {
|
|
30
|
+
const distance = Math.abs(shown);
|
|
31
|
+
if (distance <= FREE_Y)
|
|
32
|
+
return shown;
|
|
33
|
+
const stretch = Math.min(distance - FREE_Y, STRETCH_Y - 1);
|
|
34
|
+
return Math.sign(shown) * (FREE_Y + (1 / (1 - stretch / STRETCH_Y) - 1) * STRETCH_Y / .55);
|
|
35
|
+
};
|
|
36
|
+
const clampUnit = (value) => Math.min(1, Math.max(0, value));
|
|
37
|
+
const throwSpring = { type: "spring", visualDuration: .5, bounce: 0 };
|
|
38
|
+
const fadeOut = { duration: .22, delay: .12, ease: [...motionTokens.ease.standard] };
|
|
39
|
+
const reducedFade = { duration: .15, ease: "linear" };
|
|
40
|
+
function velocityOf(samples, now) {
|
|
41
|
+
const recent = samples.filter(sample => now - sample.t <= 80);
|
|
42
|
+
const first = recent[0], last = recent[recent.length - 1];
|
|
43
|
+
if (!first || !last || first === last || now - last.t > 60)
|
|
44
|
+
return { x: 0, y: 0 };
|
|
45
|
+
const seconds = (last.t - first.t) / 1000;
|
|
46
|
+
return { x: (last.x - first.x) / seconds, y: (last.y - first.y) / seconds };
|
|
47
|
+
}
|
|
48
|
+
function StackCard({ id, depth, decision, returnFrom, interactive, zIndex, label, labels, lift, reduced, fling, onRelease, onThrown, onGone, children }) {
|
|
49
|
+
const cardRef = useRef(null);
|
|
50
|
+
// A card brought back by undo starts where it left the screen; with reduced motion it simply fades in at home.
|
|
51
|
+
const travel = returnFrom && !reduced ? returnFrom : undefined;
|
|
52
|
+
const x = useMotionValue(travel?.x ?? 0);
|
|
53
|
+
const y = useMotionValue(travel?.y ?? 0);
|
|
54
|
+
const tilt = useMotionValue(travel?.tilt ?? 1);
|
|
55
|
+
const opacity = useMotionValue(returnFrom ? 0 : 1);
|
|
56
|
+
const rotate = useTransform([x, tilt], ([offset = 0, sign = 0]) => offset * ROTATE * sign);
|
|
57
|
+
// Depth springs toward its slot; while the top card is dragged away, the cards behind already start to rise.
|
|
58
|
+
const depthTarget = useTransform(lift, value => depth - (depth > 0 ? value * LIFT : 0));
|
|
59
|
+
const depthSpring = useSpring(depthTarget, motionTokens.spring.smooth);
|
|
60
|
+
const shown = reduced ? depthTarget : depthSpring;
|
|
61
|
+
const scale = useTransform(shown, value => 1 - value * SHRINK);
|
|
62
|
+
const lower = useTransform(shown, value => value * PEEK);
|
|
63
|
+
const presence = useTransform(shown, [VISIBLE - 2, VISIBLE - 1], [1, 0]);
|
|
64
|
+
const shade = useTransform(shown, [0, 2], [0, 1]);
|
|
65
|
+
const keep = useTransform(x, [16, 96], [0, 1]);
|
|
66
|
+
const pass = useTransform(x, [-96, -16], [1, 0]);
|
|
67
|
+
const drag = useRef(null);
|
|
68
|
+
const settled = useRef(!returnFrom);
|
|
69
|
+
const wasDecided = useRef(false);
|
|
70
|
+
// A card flying home from undo or start over leaves the stack alone: the cards behind make room in one motion instead of
|
|
71
|
+
// pausing half-risen until it lands. Grabbing it mid-flight hands the stack back to the drag.
|
|
72
|
+
const homing = useRef(false);
|
|
73
|
+
// The top card reports how far it has travelled, so the stack behind responds to the drag and the throw.
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!interactive)
|
|
76
|
+
return;
|
|
77
|
+
const update = () => { if (!homing.current)
|
|
78
|
+
lift.set(clampUnit(Math.hypot(x.get(), y.get() * .5) / LIFT_DISTANCE)); };
|
|
79
|
+
const offX = x.on("change", update), offY = y.on("change", update);
|
|
80
|
+
return () => { offX(); offY(); };
|
|
81
|
+
}, [interactive, lift, x, y]);
|
|
82
|
+
useLayoutEffect(() => {
|
|
83
|
+
if (decision) {
|
|
84
|
+
// A key or button can decide the card mid-drag; the throw takes over from the finger.
|
|
85
|
+
drag.current = null;
|
|
86
|
+
homing.current = false;
|
|
87
|
+
cardRef.current?.removeAttribute("data-dragging");
|
|
88
|
+
wasDecided.current = true;
|
|
89
|
+
const direction = decision === "right" ? 1 : -1;
|
|
90
|
+
const width = cardRef.current?.offsetWidth ?? 320;
|
|
91
|
+
const cx = x.get(), cy = y.get();
|
|
92
|
+
// Buttons and keys throw with a default flick; a drag keeps the finger's velocity.
|
|
93
|
+
const velocity = fling(id) ?? { x: direction * 900, y: -120 };
|
|
94
|
+
const speed = Math.hypot(velocity.x, velocity.y);
|
|
95
|
+
// Leave along the flick when it heads the chosen way, otherwise along the drag, otherwise straight out.
|
|
96
|
+
let ux = velocity.x, uy = velocity.y;
|
|
97
|
+
if (speed < 200 || Math.sign(ux) !== direction) {
|
|
98
|
+
ux = Math.abs(cx) > 8 ? cx : direction;
|
|
99
|
+
uy = Math.abs(cx) > 8 ? cy * .5 : -.12;
|
|
100
|
+
}
|
|
101
|
+
const length = Math.hypot(ux, uy) || 1;
|
|
102
|
+
ux /= length;
|
|
103
|
+
uy /= length;
|
|
104
|
+
if (Math.abs(ux) < .45) {
|
|
105
|
+
ux = direction * .45;
|
|
106
|
+
uy = Math.sign(uy || -1) * Math.sqrt(1 - .45 * .45);
|
|
107
|
+
}
|
|
108
|
+
// Far enough to clear a typical preview or card column; the fade finishes before the spring does.
|
|
109
|
+
const reach = width * 1.6;
|
|
110
|
+
const exit = { x: cx + ux * reach, y: cy + uy * reach, tilt: tilt.get() };
|
|
111
|
+
onThrown(id, exit);
|
|
112
|
+
if (reduced) {
|
|
113
|
+
animate(opacity, 0, { ...reducedFade, onComplete: () => onGone(id) });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
animate(x, exit.x, { ...throwSpring, velocity: velocity.x });
|
|
117
|
+
animate(y, exit.y, { ...throwSpring, velocity: velocity.y });
|
|
118
|
+
animate(opacity, 0, { ...fadeOut, onComplete: () => onGone(id) });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Undo, or a first mount from where the card left: come back from there, catching any flight in progress.
|
|
122
|
+
if (!wasDecided.current && settled.current)
|
|
123
|
+
return;
|
|
124
|
+
wasDecided.current = false;
|
|
125
|
+
settled.current = true;
|
|
126
|
+
const delay = returnFrom?.delay ?? 0;
|
|
127
|
+
if (reduced) {
|
|
128
|
+
x.jump(0);
|
|
129
|
+
y.jump(0);
|
|
130
|
+
animate(opacity, 1, { ...reducedFade, delay });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
homing.current = true;
|
|
134
|
+
animate(x, 0, { ...motionTokens.spring.smooth, delay, onComplete: () => { homing.current = false; } });
|
|
135
|
+
animate(y, 0, { ...motionTokens.spring.smooth, delay });
|
|
136
|
+
animate(opacity, 1, { duration: motionTokens.duration.fast, delay, ease: [...motionTokens.ease.enter] });
|
|
137
|
+
// Only the decision drives this effect; the return position is read once at that moment.
|
|
138
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
139
|
+
}, [decision]);
|
|
140
|
+
function down(event) {
|
|
141
|
+
if (!interactive || event.button !== 0 || !event.isPrimary)
|
|
142
|
+
return;
|
|
143
|
+
if (event.target instanceof Element && event.target.closest("button, a, input, select, textarea, [role='button']"))
|
|
144
|
+
return;
|
|
145
|
+
x.stop();
|
|
146
|
+
y.stop();
|
|
147
|
+
homing.current = false;
|
|
148
|
+
// Grabbing the top half tips the card the way the hand moves; grabbing the bottom half tips it the other way.
|
|
149
|
+
if (Math.abs(x.get()) < 2) {
|
|
150
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
151
|
+
tilt.set(event.clientY < rect.top + rect.height / 2 ? 1 : -1);
|
|
152
|
+
}
|
|
153
|
+
drag.current = { ox: event.clientX - x.get(), oy: event.clientY - unresistY(y.get()), samples: [{ t: event.timeStamp, x: event.clientX, y: event.clientY }] };
|
|
154
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
155
|
+
event.currentTarget.setAttribute("data-dragging", "");
|
|
156
|
+
}
|
|
157
|
+
function move(event) {
|
|
158
|
+
const state = drag.current;
|
|
159
|
+
if (!state || !interactive)
|
|
160
|
+
return;
|
|
161
|
+
x.set(event.clientX - state.ox);
|
|
162
|
+
y.set(resistY(event.clientY - state.oy));
|
|
163
|
+
state.samples.push({ t: event.timeStamp, x: event.clientX, y: event.clientY });
|
|
164
|
+
if (state.samples.length > 12)
|
|
165
|
+
state.samples.shift();
|
|
166
|
+
}
|
|
167
|
+
function up(event) {
|
|
168
|
+
const state = drag.current;
|
|
169
|
+
drag.current = null;
|
|
170
|
+
event.currentTarget.removeAttribute("data-dragging");
|
|
171
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId))
|
|
172
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
173
|
+
if (!state || !interactive)
|
|
174
|
+
return;
|
|
175
|
+
const velocity = event.type === "pointercancel" ? { x: 0, y: 0 } : velocityOf(state.samples, event.timeStamp);
|
|
176
|
+
if (onRelease(id, { x: x.get(), y: y.get() }, velocity, event.currentTarget.offsetWidth))
|
|
177
|
+
return;
|
|
178
|
+
// Below the threshold the card springs home, keeping the release velocity so it settles instead of stopping dead.
|
|
179
|
+
if (reduced) {
|
|
180
|
+
x.jump(0);
|
|
181
|
+
y.jump(0);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
animate(x, 0, { ...motionTokens.spring.morph, velocity: velocity.x });
|
|
185
|
+
animate(y, 0, { ...motionTokens.spring.morph, velocity: velocity.y });
|
|
186
|
+
}
|
|
187
|
+
const flying = decision !== null;
|
|
188
|
+
return _jsx(motion.div, { className: styles.slot, style: { zIndex, scale, y: lower, opacity: presence }, "aria-hidden": interactive ? undefined : true, inert: !interactive, children: _jsxs(motion.div, { ref: cardRef, className: styles.card, style: { x, y, rotate, opacity }, "data-top": interactive ? "" : undefined, "data-flying": flying ? "" : undefined, role: interactive ? "group" : undefined, "aria-label": interactive ? label : undefined, onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up, children: [_jsxs("div", { className: styles.face, children: [children, _jsx(motion.span, { className: styles.shade, style: { opacity: shade }, "aria-hidden": "true" })] }), _jsxs(motion.span, { className: styles.stamp, "data-side": "right", style: { opacity: keep }, "aria-hidden": "true", children: [_jsx(Check, { size: 16, strokeWidth: 2 }), labels.right] }), _jsxs(motion.span, { className: styles.stamp, "data-side": "left", style: { opacity: pass }, "aria-hidden": "true", children: [_jsx(X, { size: 16, strokeWidth: 2 }), labels.left] })] }) });
|
|
189
|
+
}
|
|
190
|
+
function Control({ children, onClick, disabled, reduced, tone }) {
|
|
191
|
+
// aria-disabled keeps focus on the button when the stack runs out, instead of dropping it to the page.
|
|
192
|
+
return _jsx(motion.button, { type: "button", className: styles.control, "data-tone": tone, "aria-disabled": disabled || undefined, onClick: () => { if (!disabled)
|
|
193
|
+
onClick(); }, whileTap: { scale: reduced || disabled ? 1 : .97 }, transition: motionTokens.spring.snappy, children: children });
|
|
194
|
+
}
|
|
195
|
+
/** Renders a custom empty state; the reset handler travels as a prop so render never touches the stack's refs. */
|
|
196
|
+
function EmptySlot({ render, onReset }) { return _jsx(_Fragment, { children: render(onReset) }); }
|
|
197
|
+
export function CardStack({ items, getKey, getLabel, renderCard, onDecide, onUndo, onReset, labels = { left: "Pass", right: "Keep" }, outcomes, label = "Cards", renderEmpty, className }) {
|
|
198
|
+
const hydrated = useSyncExternalStore(noop, () => true, () => false);
|
|
199
|
+
const reduced = !!useReducedMotion() && hydrated;
|
|
200
|
+
const hintId = useId();
|
|
201
|
+
const [position, setPosition] = useState(0);
|
|
202
|
+
const [history, setHistory] = useState([]);
|
|
203
|
+
const [flying, setFlying] = useState([]);
|
|
204
|
+
const [returning, setReturning] = useState({});
|
|
205
|
+
const [message, setMessage] = useState("");
|
|
206
|
+
const lift = useMotionValue(0);
|
|
207
|
+
// Where each thrown card left the screen, so undo and start over can bring it back from there. Only those handlers read it,
|
|
208
|
+
// so it lives in a ref: recording an exit must not re-render the stack in the very frame the throw starts.
|
|
209
|
+
const exits = useRef({});
|
|
210
|
+
const flings = useRef(new Map());
|
|
211
|
+
const top = items[position];
|
|
212
|
+
const remaining = Math.max(0, items.length - position);
|
|
213
|
+
function decide(decision) {
|
|
214
|
+
if (!top)
|
|
215
|
+
return;
|
|
216
|
+
const key = getKey(top);
|
|
217
|
+
lift.set(0);
|
|
218
|
+
setPosition(position + 1);
|
|
219
|
+
setHistory([...history, { key, decision }]);
|
|
220
|
+
setFlying(current => [...current, key]);
|
|
221
|
+
setReturning({});
|
|
222
|
+
const left = remaining - 1;
|
|
223
|
+
setMessage(`${getLabel(top)}${outcomes ? ` ${outcomes[decision]}` : `: ${labels[decision]}`}. ${left ? `${left} left.` : "All cards reviewed."}`);
|
|
224
|
+
onDecide?.(top, decision);
|
|
225
|
+
}
|
|
226
|
+
function undo() {
|
|
227
|
+
const last = history[history.length - 1];
|
|
228
|
+
const item = last && items.find(entry => getKey(entry) === last.key);
|
|
229
|
+
if (!last || !item)
|
|
230
|
+
return;
|
|
231
|
+
const exit = exits.current[last.key];
|
|
232
|
+
setPosition(position - 1);
|
|
233
|
+
setHistory(history.slice(0, -1));
|
|
234
|
+
setFlying(current => current.filter(key => key !== last.key));
|
|
235
|
+
setReturning(exit ? { [last.key]: { ...exit, delay: 0 } } : {});
|
|
236
|
+
setMessage(`${getLabel(item)} restored.`);
|
|
237
|
+
onUndo?.(item, last.decision);
|
|
238
|
+
}
|
|
239
|
+
function reset() {
|
|
240
|
+
if (!history.length)
|
|
241
|
+
return;
|
|
242
|
+
// The deck deals itself back: the card that will sit deepest returns first, the top card last.
|
|
243
|
+
const count = Math.min(VISIBLE, items.length);
|
|
244
|
+
const back = {};
|
|
245
|
+
items.slice(0, count).forEach((item, index) => {
|
|
246
|
+
const key = getKey(item), exit = exits.current[key];
|
|
247
|
+
if (exit && index < position)
|
|
248
|
+
back[key] = { ...exit, delay: (count - 1 - index) * .06 };
|
|
249
|
+
});
|
|
250
|
+
setPosition(0);
|
|
251
|
+
setHistory([]);
|
|
252
|
+
setFlying([]);
|
|
253
|
+
setReturning(back);
|
|
254
|
+
setMessage("All cards are back in the stack.");
|
|
255
|
+
onReset?.();
|
|
256
|
+
}
|
|
257
|
+
function release(key, offset, velocity, width) {
|
|
258
|
+
const direction = Math.sign(offset.x);
|
|
259
|
+
if (!direction || !top || getKey(top) !== key)
|
|
260
|
+
return false;
|
|
261
|
+
const flick = Math.abs(velocity.x) > FLICK && Math.sign(velocity.x) === direction && Math.abs(offset.x) > 12;
|
|
262
|
+
const far = Math.abs(offset.x) > Math.min(width * .35, 140) && !(Math.sign(velocity.x) === -direction && Math.abs(velocity.x) > 300);
|
|
263
|
+
if (!flick && !far)
|
|
264
|
+
return false;
|
|
265
|
+
flings.current.set(key, velocity);
|
|
266
|
+
decide(direction > 0 ? "right" : "left");
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
function key(event) {
|
|
270
|
+
if (event.target !== event.currentTarget)
|
|
271
|
+
return;
|
|
272
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
273
|
+
event.preventDefault();
|
|
274
|
+
decide(event.key === "ArrowRight" ? "right" : "left");
|
|
275
|
+
}
|
|
276
|
+
else if (event.key === "Backspace" || ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "z")) {
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
undo();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const decided = new Map(history.map(entry => [entry.key, entry.decision]));
|
|
282
|
+
const windowKeys = new Set(items.slice(position, position + VISIBLE).map(getKey));
|
|
283
|
+
const rendered = items.filter(item => windowKeys.has(getKey(item)) || flying.includes(getKey(item)));
|
|
284
|
+
const empty = position >= items.length && flying.length === 0;
|
|
285
|
+
return _jsxs("div", { className: [styles.root, className].filter(Boolean).join(" "), children: [_jsxs("div", { className: styles.stage, role: "group", "aria-roledescription": "card stack", "aria-label": label, "aria-describedby": hintId, tabIndex: 0, onKeyDown: key, children: [rendered.map(item => {
|
|
286
|
+
const id = getKey(item), index = items.indexOf(item), decision = decided.get(id) ?? null;
|
|
287
|
+
const depth = decision ? 0 : index - position;
|
|
288
|
+
return _jsx(StackCard, { id: id, depth: depth, decision: decision, returnFrom: returning[id], interactive: !decision && depth === 0, zIndex: decision ? 20 + flying.indexOf(id) : VISIBLE - depth, label: `${getLabel(item)}, ${index + 1} of ${items.length}`, labels: labels, lift: lift, reduced: reduced, fling: cardKey => { const velocity = flings.current.get(cardKey); flings.current.delete(cardKey); return velocity; }, onRelease: release, onThrown: (cardKey, exit) => { exits.current[cardKey] = exit; }, onGone: cardKey => setFlying(current => current.filter(entry => entry !== cardKey)), children: renderCard(item) }, id);
|
|
289
|
+
}), _jsx(AnimatePresence, { initial: false, children: empty && _jsx(motion.div, { className: styles.empty, initial: reduced ? { opacity: 0 } : { opacity: 0, y: 8, filter: `blur(${motionTokens.blur.soft}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: { opacity: 0, transition: { duration: motionTokens.duration.instant } }, transition: reduced ? reducedFade : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }, children: renderEmpty ? _jsx(EmptySlot, { render: renderEmpty, onReset: reset }) : _jsxs(_Fragment, { children: [_jsx("p", { className: styles.emptyTitle, children: "All cards reviewed" }), _jsxs("button", { type: "button", className: styles.control, onClick: reset, children: [_jsx(RotateCcw, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), "Start over"] })] }) }, "empty") })] }), _jsxs("div", { className: styles.controls, children: [_jsxs(Control, { onClick: () => decide("left"), disabled: !top, reduced: reduced, children: [_jsx(X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), labels.left] }), _jsxs(Control, { onClick: undo, disabled: !history.length, reduced: reduced, tone: "quiet", children: [_jsx(RotateCcw, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("span", { className: styles.controlLabel, children: "Undo" })] }), _jsxs(Control, { onClick: () => decide("right"), disabled: !top, reduced: reduced, tone: "primary", children: [_jsx(Check, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), labels.right] })] }), _jsxs("p", { id: hintId, className: styles.srOnly, children: ["Drag the top card, or use the left and right arrow keys to ", labels.left.toLowerCase(), " or ", labels.right.toLowerCase(), " it. Backspace undoes the last decision."] }), _jsx("p", { className: styles.srOnly, role: "status", "aria-live": "polite", children: message })] });
|
|
290
|
+
}
|
|
291
|
+
export default CardStack;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
.root { --card-height: 19rem; --stack-peek: 24px; display: grid; width: 100%; justify-items: center; gap: var(--space-comfort); font-family: var(--font-body); letter-spacing: var(--tracking-body); }
|
|
2
|
+
|
|
3
|
+
/* The stage reserves the top card plus the edges peeking out below it, so nothing reflows as cards come and go. */
|
|
4
|
+
.stage { position: relative; isolation: isolate; width: min(100%, 20rem); height: calc(var(--card-height) + var(--stack-peek)); outline: none; }
|
|
5
|
+
/* Each slot scales from its bottom edge, so the cards behind show a thin, even edge below the one in front. */
|
|
6
|
+
.slot { position: absolute; top: 0; right: 0; left: 0; height: var(--card-height); pointer-events: none; transform-origin: 50% 100%; }
|
|
7
|
+
.card { position: absolute; inset: 0; border-radius: var(--radius-overlay); corner-shape: var(--corner-shape); pointer-events: auto; touch-action: pan-y; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; }
|
|
8
|
+
.card[data-top] { cursor: grab; }
|
|
9
|
+
.card[data-dragging] { cursor: grabbing; }
|
|
10
|
+
.card:not([data-top]) { pointer-events: none; }
|
|
11
|
+
/* Resting cards sit flat on their borders; a card in the hand or in flight floats and casts a shadow. */
|
|
12
|
+
.card::before { position: absolute; inset: 0; border-radius: inherit; corner-shape: inherit; box-shadow: var(--shadow-floating); content: ""; opacity: 0; pointer-events: none; transition: opacity var(--duration-fast) var(--ease-standard); }
|
|
13
|
+
.card[data-dragging]::before, .card[data-flying]::before { opacity: 1; }
|
|
14
|
+
.face { position: relative; height: 100%; overflow: hidden; border: 1px solid var(--border); border-radius: inherit; corner-shape: inherit; background: var(--surface); color: var(--foreground); }
|
|
15
|
+
.shade { position: absolute; inset: 0; background: oklch(0% 0 0 / .035); pointer-events: none; }
|
|
16
|
+
:global(:root[data-theme="dark"]) .shade { background: oklch(0% 0 0 / .22); }
|
|
17
|
+
|
|
18
|
+
.stamp { position: absolute; top: var(--space-inset); display: inline-flex; height: 2rem; align-items: center; gap: 6px; padding: 0 var(--space-group) 0 10px; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-raised); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; white-space: nowrap; pointer-events: none; }
|
|
19
|
+
.stamp[data-side="right"] { left: var(--space-inset); border-color: color-mix(in oklab, var(--success) 32%, var(--border)); background: color-mix(in oklab, var(--success) 9%, var(--surface-raised)); }
|
|
20
|
+
.stamp[data-side="right"] svg { color: var(--success); }
|
|
21
|
+
.stamp[data-side="left"] { right: var(--space-inset); }
|
|
22
|
+
.stamp[data-side="left"] svg { color: var(--text-secondary); }
|
|
23
|
+
/* Cards returning to the back of the stack carry no verdict. */
|
|
24
|
+
.card:not([data-top]):not([data-flying]) .stamp { visibility: hidden; }
|
|
25
|
+
|
|
26
|
+
/* Keyboard focus on the stage outlines the top card instead of the whole stage. */
|
|
27
|
+
.stage:focus-visible { --focus-outline-width: 0px; }
|
|
28
|
+
.stage:focus-visible .card[data-top], .stage:focus-visible .empty { outline: 2px solid var(--focus-outline, color-mix(in oklab, var(--accent-strong) 72%, transparent)); outline-offset: 3px; }
|
|
29
|
+
|
|
30
|
+
.empty { position: absolute; top: 0; right: 0; left: 0; display: grid; height: var(--card-height); place-content: center; justify-items: center; gap: var(--space-group); padding: var(--space-section); border: 1px solid var(--border); border-radius: var(--radius-overlay); corner-shape: var(--corner-shape); background: var(--surface); text-align: center; }
|
|
31
|
+
.emptyTitle { margin: 0; font-size: var(--text-base); font-weight: 500; }
|
|
32
|
+
|
|
33
|
+
.controls { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-related); }
|
|
34
|
+
.control { display: inline-flex; min-height: var(--control-height-md); align-items: center; justify-content: center; gap: var(--space-related); padding: 0 var(--space-inset); border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
|
|
35
|
+
.control svg { flex: none; }
|
|
36
|
+
/* The approving action follows the selected accent. */
|
|
37
|
+
.control[data-tone="primary"] { border-color: var(--accent); background: var(--accent); color: var(--accent-foreground); }
|
|
38
|
+
.control[data-tone="quiet"] { border-color: transparent; background: transparent; color: var(--text-secondary); }
|
|
39
|
+
@media (hover: hover) and (pointer: fine) {
|
|
40
|
+
.control:not([aria-disabled]):hover { background: var(--surface-muted); }
|
|
41
|
+
.control[data-tone="primary"]:not([aria-disabled]):hover { border-color: var(--accent-strong); background: var(--accent-strong); }
|
|
42
|
+
.control[data-tone="quiet"]:not([aria-disabled]):hover { color: var(--foreground); }
|
|
43
|
+
}
|
|
44
|
+
.control:not([aria-disabled]):active { background: var(--surface-muted); }
|
|
45
|
+
.control[data-tone="primary"]:not([aria-disabled]):active { border-color: var(--accent-strong); background: var(--accent-strong); }
|
|
46
|
+
.control[aria-disabled] { cursor: default; opacity: .42; }
|
|
47
|
+
|
|
48
|
+
.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
49
|
+
|
|
50
|
+
@media (max-width: 26rem) {
|
|
51
|
+
.root { --card-height: 17.5rem; gap: var(--space-inset); }
|
|
52
|
+
.controls { flex-wrap: nowrap; }
|
|
53
|
+
.control { padding-inline: var(--space-group); }
|
|
54
|
+
}
|
|
55
|
+
/* On the narrowest screens Undo keeps its icon and its accessible name, so all three controls stay on one row. */
|
|
56
|
+
@media (max-width: 22.5rem) {
|
|
57
|
+
.control[data-tone="quiet"] { width: var(--control-height-md); padding: 0; }
|
|
58
|
+
.control[data-tone="quiet"] .controlLabel { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@media (prefers-reduced-motion: reduce) {
|
|
62
|
+
.card::before, .control { transition: none; }
|
|
63
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A row of slides people browse by dragging, flicking, a trackpad swipe, the arrow keys, or the controls below it. Use it for a short,
|
|
4
|
+
* ordered set of peers such as products or case studies, where the neighbours stay in view as a hint that there is more.
|
|
5
|
+
* The active slide sits in the center, its neighbours shrink and dim with distance, and the page indicator stretches into a pill that
|
|
6
|
+
* travels with the slides frame by frame.
|
|
7
|
+
*/
|
|
8
|
+
export interface CarouselProps {
|
|
9
|
+
/** Names the carousel for assistive technology, for example "From the Harbour suites". */
|
|
10
|
+
label: string;
|
|
11
|
+
/** One child per slide. */
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
/** Controlled active slide. Pair with `onIndexChange`. */
|
|
14
|
+
index?: number;
|
|
15
|
+
defaultIndex?: number;
|
|
16
|
+
onIndexChange?: (index: number) => void;
|
|
17
|
+
/** Width of one slide as a CSS length. Use `cqw` for a share of the carousel width. */
|
|
18
|
+
slideSize?: string;
|
|
19
|
+
/** Names each slide and its indicator. Defaults to "2 of 5". */
|
|
20
|
+
slideLabel?: (index: number, count: number) => string;
|
|
21
|
+
/** Milliseconds per slide. Adds a play control that rotates through the slides; rotation pauses on hover, keyboard focus, and drag, and is unavailable with reduced motion. */
|
|
22
|
+
interval?: number;
|
|
23
|
+
/** Starts rotating on mount. Off by default, so motion begins with the viewer. */
|
|
24
|
+
autoplay?: boolean;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function Carousel({ label, children, index: controlledIndex, defaultIndex, onIndexChange, slideSize, slideLabel, interval, autoplay, className }: CarouselProps): import("react").JSX.Element;
|
|
28
|
+
export default Carousel;
|