@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,355 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useMotionValueEvent, useReducedMotion, useTransform } from "motion/react";
|
|
5
|
+
import { CircleAlert, LoaderCircle } from "lucide-react";
|
|
6
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
7
|
+
import styles from "./confirm-morph.module.css";
|
|
8
|
+
/** A press this soon after the question appears is the tail of the press that opened it, not an answer. */
|
|
9
|
+
const ARM_MS = 280;
|
|
10
|
+
const { blur } = motionTokens;
|
|
11
|
+
const enter = [...motionTokens.ease.enter];
|
|
12
|
+
const standard = [...motionTokens.ease.standard];
|
|
13
|
+
/** Duration springs restated as stiffness and damping, so a retarget mid-flight keeps the velocity it already has. */
|
|
14
|
+
const physical = ({ visualDuration, bounce }) => {
|
|
15
|
+
const root = 2 * Math.PI / (visualDuration * 1.2);
|
|
16
|
+
return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };
|
|
17
|
+
};
|
|
18
|
+
/** Growing follows new content with a little life; shrinking settles without overshoot. */
|
|
19
|
+
const GROW = physical(motionTokens.spring.morph), SHRINK = physical(motionTokens.spring.smooth);
|
|
20
|
+
const subscribe = () => () => { };
|
|
21
|
+
function useReducedFlag() {
|
|
22
|
+
const hydrated = useSyncExternalStore(subscribe, () => true, () => false);
|
|
23
|
+
return !!useReducedMotion() && hydrated;
|
|
24
|
+
}
|
|
25
|
+
/*
|
|
26
|
+
* Faces never move and never scale: each one sits at its natural width, centred in the pill, so the baseline stays put and
|
|
27
|
+
* the pill's clip reveals it as the width springs. The old face fades out quickly; the new one's parts arrive in reading
|
|
28
|
+
* order, each clearing a small blur.
|
|
29
|
+
*/
|
|
30
|
+
const faceVariants = {
|
|
31
|
+
hidden: { opacity: 1 },
|
|
32
|
+
shown: { opacity: 1, transition: { staggerChildren: motionTokens.stagger.item, delayChildren: .05 } },
|
|
33
|
+
gone: { opacity: 0, filter: `blur(${blur.subtle}px)`, transition: { duration: motionTokens.duration.instant, ease: standard } },
|
|
34
|
+
};
|
|
35
|
+
const partVariants = {
|
|
36
|
+
hidden: { opacity: 0, filter: `blur(${blur.soft}px)` },
|
|
37
|
+
shown: { opacity: 1, filter: "blur(0px)", transition: { duration: .26, ease: enter } },
|
|
38
|
+
};
|
|
39
|
+
const reducedFace = { hidden: { opacity: 0 }, shown: { opacity: 1, transition: { duration: .14 } }, gone: { opacity: 0, transition: { duration: .1 } } };
|
|
40
|
+
const reducedPart = { hidden: { opacity: 1 }, shown: { opacity: 1 } };
|
|
41
|
+
function Face({ id, reduced, onSize, children, labelledBy }) {
|
|
42
|
+
const ref = useRef(null);
|
|
43
|
+
const present = useIsPresent();
|
|
44
|
+
useLayoutEffect(() => {
|
|
45
|
+
const node = ref.current;
|
|
46
|
+
if (!node || !present)
|
|
47
|
+
return;
|
|
48
|
+
// Fractional widths, so the spring lands exactly where the pill's natural width is and the hand back to auto is seamless.
|
|
49
|
+
const report = () => onSize(id, node.getBoundingClientRect().width);
|
|
50
|
+
report();
|
|
51
|
+
if (typeof ResizeObserver === "undefined")
|
|
52
|
+
return;
|
|
53
|
+
const observer = new ResizeObserver(report);
|
|
54
|
+
observer.observe(node);
|
|
55
|
+
return () => observer.disconnect();
|
|
56
|
+
}, [id, onSize, present]);
|
|
57
|
+
return _jsx(motion.div, { ref: ref, className: styles.face, "data-face": id, role: labelledBy ? "group" : undefined, "aria-labelledby": labelledBy, variants: reduced ? reducedFace : faceVariants, initial: "hidden", animate: "shown", exit: "gone", inert: !present || undefined, children: children });
|
|
58
|
+
}
|
|
59
|
+
function Part({ reduced, className, children }) {
|
|
60
|
+
return _jsx(motion.span, { className: [styles.part, className].filter(Boolean).join(" "), variants: reduced ? reducedPart : partVariants, children: children });
|
|
61
|
+
}
|
|
62
|
+
/** A success disc that pops in with a tick drawing across it, the moment the action lands. */
|
|
63
|
+
function Check({ reduced }) {
|
|
64
|
+
return _jsxs("svg", { className: styles.check, viewBox: "0 0 18 18", fill: "none", "aria-hidden": "true", children: [_jsx(motion.circle, { className: styles.checkDisc, cx: "9", cy: "9", r: "8", style: { transformOrigin: "9px 9px" }, initial: reduced ? false : { scale: .5, opacity: 0 }, animate: { scale: 1, opacity: 1 }, transition: { scale: physical(motionTokens.spring.snappy), opacity: { duration: .12 } } }), _jsx(motion.path, { className: styles.checkTick, d: "M5.6 9.3 7.8 11.4 12.4 6.7", strokeWidth: "1.9", strokeLinecap: "round", strokeLinejoin: "round", initial: reduced ? false : { pathLength: 0 }, animate: { pathLength: 1 }, transition: { duration: .3, ease: enter, delay: .14 } })] });
|
|
65
|
+
}
|
|
66
|
+
/** A ring that drains with the countdown. */
|
|
67
|
+
function Ring({ progress }) {
|
|
68
|
+
return _jsxs("svg", { className: styles.ring, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [_jsx("circle", { className: styles.ringTrack, cx: "8", cy: "8", r: "6.25", strokeWidth: "1.75" }), _jsx(motion.circle, { className: styles.ringFill, cx: "8", cy: "8", r: "6.25", strokeWidth: "1.75", strokeLinecap: "round", style: { pathLength: progress, rotate: -90, transformOrigin: "8px 8px" } })] });
|
|
69
|
+
}
|
|
70
|
+
/** Seconds that swap in place: the old digit fades up and out, the new one rises in, on tabular figures so the width holds. */
|
|
71
|
+
function Seconds({ value, reduced }) {
|
|
72
|
+
return _jsx("span", { className: styles.seconds, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: _jsx(motion.span, { className: styles.digit, initial: reduced ? { opacity: 0 } : { opacity: 0, y: 6, filter: `blur(${blur.subtle}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: reduced ? { opacity: 0 } : { opacity: 0, y: -6, filter: `blur(${blur.subtle}px)` }, transition: { duration: .22, ease: enter }, children: value }, value) }) });
|
|
73
|
+
}
|
|
74
|
+
export function ConfirmMorph({ label, icon, prompt, confirmLabel = "Delete", cancelLabel = "Cancel", pendingLabel = "Deleting", doneLabel = "Deleted", errorLabel = "Couldn’t finish", retryLabel = "Retry", undoLabel = "Undo", undoingLabel = "Restoring", tone = "danger", confirmMode = "press", holdDuration = 1000, countdown = 3, countdownLabel, onConfirm, onUndo, onCancel, state: stateProp, defaultState = "idle", onStateChange, confirmTimeout = 6000, resultTimeout = 5000, cancelOnOutsidePress = true, disabled = false, className, ref, }) {
|
|
75
|
+
const reduced = useReducedFlag();
|
|
76
|
+
const uid = useId();
|
|
77
|
+
const promptId = `${uid}-prompt`;
|
|
78
|
+
const holdHintId = `${uid}-hold`;
|
|
79
|
+
const rootRef = useRef(null);
|
|
80
|
+
useImperativeHandle(ref, () => rootRef.current, []);
|
|
81
|
+
const [inner, setInner] = useState(defaultState);
|
|
82
|
+
const state = stateProp ?? inner;
|
|
83
|
+
const [working, setWorking] = useState("confirm");
|
|
84
|
+
const [counting, setCounting] = useState(false);
|
|
85
|
+
const [announcement, setAnnouncement] = useState("");
|
|
86
|
+
const faceKey = state === "confirming" && counting ? "counting" : state;
|
|
87
|
+
const live = useRef({ state, onStateChange, controlled: stateProp !== undefined });
|
|
88
|
+
useLayoutEffect(() => { live.current = { state, onStateChange, controlled: stateProp !== undefined }; });
|
|
89
|
+
const pendingFocus = useRef(false);
|
|
90
|
+
const run = useRef(0);
|
|
91
|
+
const askedAt = useRef(0);
|
|
92
|
+
const keepFocus = useCallback(() => {
|
|
93
|
+
const root = rootRef.current;
|
|
94
|
+
// Focus follows the control between faces, but only when it was already inside; a timeout never steals focus from elsewhere.
|
|
95
|
+
pendingFocus.current = !!root && (root.contains(document.activeElement) || document.activeElement === document.body);
|
|
96
|
+
}, []);
|
|
97
|
+
const go = useCallback((next) => {
|
|
98
|
+
if (next !== "confirming")
|
|
99
|
+
setCounting(false);
|
|
100
|
+
if (next === live.current.state)
|
|
101
|
+
return;
|
|
102
|
+
keepFocus();
|
|
103
|
+
if (next === "confirming")
|
|
104
|
+
askedAt.current = performance.now();
|
|
105
|
+
if (!live.current.controlled)
|
|
106
|
+
setInner(next);
|
|
107
|
+
live.current.state = next;
|
|
108
|
+
live.current.onStateChange?.(next);
|
|
109
|
+
}, [keepFocus]);
|
|
110
|
+
// A controlled parent can move the state on its own; the countdown never survives that.
|
|
111
|
+
const [seenState, setSeenState] = useState(state);
|
|
112
|
+
if (seenState !== state) {
|
|
113
|
+
setSeenState(state);
|
|
114
|
+
if (state !== "confirming")
|
|
115
|
+
setCounting(false);
|
|
116
|
+
}
|
|
117
|
+
const toIdle = useCallback(() => { run.current++; go("idle"); }, [go]);
|
|
118
|
+
const perform = useCallback(async (kind) => {
|
|
119
|
+
const handler = kind === "confirm" ? onConfirm : onUndo;
|
|
120
|
+
const token = ++run.current;
|
|
121
|
+
setWorking(kind);
|
|
122
|
+
setCounting(false);
|
|
123
|
+
let result;
|
|
124
|
+
try {
|
|
125
|
+
result = handler?.();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
go("error");
|
|
129
|
+
setAnnouncement(errorLabel);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (result && typeof result.then === "function") {
|
|
133
|
+
go("pending");
|
|
134
|
+
setAnnouncement(kind === "confirm" ? pendingLabel : undoingLabel);
|
|
135
|
+
try {
|
|
136
|
+
await result;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
if (token !== run.current)
|
|
140
|
+
return;
|
|
141
|
+
go("error");
|
|
142
|
+
setAnnouncement(errorLabel);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (token !== run.current)
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (kind === "undo") {
|
|
149
|
+
go("idle");
|
|
150
|
+
setAnnouncement("Undone");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
go("done");
|
|
154
|
+
setAnnouncement(onUndo ? `${doneLabel}. ${undoLabel} is available.` : doneLabel);
|
|
155
|
+
}, [doneLabel, errorLabel, go, onConfirm, onUndo, pendingLabel, undoLabel, undoingLabel]);
|
|
156
|
+
const cancel = useCallback(() => { onCancel?.(); toIdle(); setAnnouncement("Cancelled"); }, [onCancel, toIdle]);
|
|
157
|
+
const expire = useRef(() => { });
|
|
158
|
+
useLayoutEffect(() => { expire.current = () => { if (live.current.state === "confirming")
|
|
159
|
+
cancel();
|
|
160
|
+
else
|
|
161
|
+
toIdle(); }; });
|
|
162
|
+
/* The shape: one pill whose width springs to whichever face is current. At rest it is auto, so it renders right before hydration. */
|
|
163
|
+
const width = useMotionValue("auto");
|
|
164
|
+
const target = useRef(0);
|
|
165
|
+
const flight = useRef(0);
|
|
166
|
+
// Only the present face reports (a leaving face stops observing), so every report is the current face's width.
|
|
167
|
+
const onFaceSize = useCallback((_id, w) => {
|
|
168
|
+
if (Math.abs(w - target.current) < .25)
|
|
169
|
+
return;
|
|
170
|
+
const from = target.current;
|
|
171
|
+
target.current = w;
|
|
172
|
+
if (!from || reduced) {
|
|
173
|
+
flight.current++;
|
|
174
|
+
width.jump("auto");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const now = width.get();
|
|
178
|
+
if (now === "auto")
|
|
179
|
+
width.jump(from);
|
|
180
|
+
const token = ++flight.current;
|
|
181
|
+
animate(width, w, w > (typeof now === "number" ? now : from) ? GROW : SHRINK).then(() => { if (token === flight.current)
|
|
182
|
+
width.jump("auto"); });
|
|
183
|
+
}, [reduced, width]);
|
|
184
|
+
/* The timeout runs as an invisible clock. A pointer resting on the control holds it, and so does a hidden tab. */
|
|
185
|
+
const drain = useMotionValue(1);
|
|
186
|
+
const clock = useRef(null);
|
|
187
|
+
const holds = useRef({ hover: false, hidden: false });
|
|
188
|
+
const timeout = state === "confirming" && !counting ? confirmTimeout : state === "done" || state === "error" ? resultTimeout : 0;
|
|
189
|
+
const sync = useCallback(() => {
|
|
190
|
+
const control = clock.current;
|
|
191
|
+
if (!control)
|
|
192
|
+
return;
|
|
193
|
+
const held = holds.current.hover || holds.current.hidden;
|
|
194
|
+
if (held)
|
|
195
|
+
control.pause();
|
|
196
|
+
else
|
|
197
|
+
control.play();
|
|
198
|
+
}, []);
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (!timeout)
|
|
201
|
+
return;
|
|
202
|
+
drain.jump(1);
|
|
203
|
+
const control = animate(drain, 0, { duration: timeout / 1000, ease: "linear" });
|
|
204
|
+
clock.current = control;
|
|
205
|
+
control.then(() => { if (clock.current === control) {
|
|
206
|
+
clock.current = null;
|
|
207
|
+
expire.current();
|
|
208
|
+
} });
|
|
209
|
+
sync();
|
|
210
|
+
return () => { if (clock.current === control)
|
|
211
|
+
clock.current = null; control.stop(); };
|
|
212
|
+
}, [drain, state, sync, timeout]);
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
const onVisibility = () => { holds.current.hidden = document.hidden; sync(); };
|
|
215
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
216
|
+
return () => document.removeEventListener("visibilitychange", onVisibility);
|
|
217
|
+
}, [sync]);
|
|
218
|
+
/* Countdown: a short grace period after the answer, drawn as a draining ring and ticking seconds. */
|
|
219
|
+
const remaining = useMotionValue(1);
|
|
220
|
+
const [seconds, setSeconds] = useState(countdown);
|
|
221
|
+
const performRef = useRef(perform);
|
|
222
|
+
useLayoutEffect(() => { performRef.current = perform; });
|
|
223
|
+
useMotionValueEvent(remaining, "change", value => setSeconds(Math.max(1, Math.ceil(value * countdown))));
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (!counting)
|
|
226
|
+
return;
|
|
227
|
+
remaining.jump(1);
|
|
228
|
+
const control = animate(remaining, 0, { duration: countdown, ease: "linear" });
|
|
229
|
+
control.then(() => { void performRef.current("confirm"); });
|
|
230
|
+
return () => control.stop();
|
|
231
|
+
}, [counting, countdown, remaining]);
|
|
232
|
+
/* Hold: the confirm button fills with the danger colour from the left while pressed, and drains back if let go early. */
|
|
233
|
+
const hold = useMotionValue(0);
|
|
234
|
+
const holding = useRef(null);
|
|
235
|
+
const fillClip = useTransform(hold, value => `inset(0 ${(1 - value) * 100}% 0 0 round 999px)`);
|
|
236
|
+
const startHold = () => {
|
|
237
|
+
if (holding.current)
|
|
238
|
+
return;
|
|
239
|
+
const control = animate(hold, 1, { duration: holdDuration / 1000 * (1 - hold.get()), ease: "linear" });
|
|
240
|
+
holding.current = control;
|
|
241
|
+
control.then(() => {
|
|
242
|
+
if (holding.current !== control)
|
|
243
|
+
return;
|
|
244
|
+
holding.current = null;
|
|
245
|
+
void perform("confirm");
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
const releaseHold = () => {
|
|
249
|
+
const control = holding.current;
|
|
250
|
+
if (!control)
|
|
251
|
+
return;
|
|
252
|
+
holding.current = null;
|
|
253
|
+
control.stop();
|
|
254
|
+
if (hold.get() < 1) {
|
|
255
|
+
animate(hold, 0, reduced ? { duration: 0 } : { type: "spring", visualDuration: .3, bounce: 0 });
|
|
256
|
+
setAnnouncement(`Keep holding to ${confirmLabel.toLowerCase()}`);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
useEffect(() => { if (state !== "confirming") {
|
|
260
|
+
holding.current?.stop();
|
|
261
|
+
holding.current = null;
|
|
262
|
+
hold.jump(0);
|
|
263
|
+
} }, [hold, state]);
|
|
264
|
+
// A distinct keyboard activation is deliberate; only suppress the opening pointer press tail.
|
|
265
|
+
const answer = (keyboard = false) => {
|
|
266
|
+
if (!keyboard && performance.now() - askedAt.current < ARM_MS)
|
|
267
|
+
return;
|
|
268
|
+
if (confirmMode === "countdown") {
|
|
269
|
+
keepFocus();
|
|
270
|
+
setCounting(true);
|
|
271
|
+
setAnnouncement(`${countdownLabel ?? `${pendingLabel} in`} ${countdown} seconds`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
void perform("confirm");
|
|
275
|
+
};
|
|
276
|
+
// An outside press answers the question with no.
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
if (state !== "confirming" || !cancelOnOutsidePress)
|
|
279
|
+
return;
|
|
280
|
+
const down = (event) => { if (!rootRef.current?.contains(event.target))
|
|
281
|
+
cancel(); };
|
|
282
|
+
document.addEventListener("pointerdown", down);
|
|
283
|
+
return () => document.removeEventListener("pointerdown", down);
|
|
284
|
+
}, [cancel, cancelOnOutsidePress, state]);
|
|
285
|
+
// Focus moves with the morph: Confirm while asking, the root while counting or working, Undo or Retry on a result, the trigger at rest.
|
|
286
|
+
useLayoutEffect(() => {
|
|
287
|
+
if (!pendingFocus.current)
|
|
288
|
+
return;
|
|
289
|
+
pendingFocus.current = false;
|
|
290
|
+
const root = rootRef.current;
|
|
291
|
+
if (!root)
|
|
292
|
+
return;
|
|
293
|
+
const face = root.querySelector(`[data-face="${faceKey}"]`);
|
|
294
|
+
const autofocus = face?.querySelector("[data-autofocus]:not(:disabled)");
|
|
295
|
+
(autofocus ?? root).focus({ preventScroll: true });
|
|
296
|
+
}, [faceKey]);
|
|
297
|
+
const onKeyDown = (event) => {
|
|
298
|
+
if (event.key === "Escape") {
|
|
299
|
+
if (state === "confirming") {
|
|
300
|
+
event.preventDefault();
|
|
301
|
+
event.stopPropagation();
|
|
302
|
+
cancel();
|
|
303
|
+
}
|
|
304
|
+
else if (state === "done" || state === "error") {
|
|
305
|
+
event.preventDefault();
|
|
306
|
+
event.stopPropagation();
|
|
307
|
+
toIdle();
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// Enter answers yes from anywhere in the question except a button of its own (Cancel keeps meaning Cancel).
|
|
312
|
+
if (event.key === "Enter" && state === "confirming" && !event.repeat && !(event.target instanceof HTMLButtonElement)) {
|
|
313
|
+
event.preventDefault();
|
|
314
|
+
if (counting)
|
|
315
|
+
void perform("confirm");
|
|
316
|
+
else if (confirmMode !== "hold")
|
|
317
|
+
answer(true);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
const holdKeys = (event, down) => {
|
|
321
|
+
if (event.key !== "Enter" && event.key !== " ")
|
|
322
|
+
return;
|
|
323
|
+
event.preventDefault();
|
|
324
|
+
if (down) {
|
|
325
|
+
if (!event.repeat)
|
|
326
|
+
startHold();
|
|
327
|
+
}
|
|
328
|
+
else
|
|
329
|
+
releaseHold();
|
|
330
|
+
};
|
|
331
|
+
const holdPointer = (event) => {
|
|
332
|
+
if (event.button !== 0)
|
|
333
|
+
return;
|
|
334
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
335
|
+
startHold();
|
|
336
|
+
};
|
|
337
|
+
const shownPrompt = prompt ?? _jsxs(_Fragment, { children: [label, "?"] });
|
|
338
|
+
const shownCountdown = countdownLabel ?? `${pendingLabel} in`;
|
|
339
|
+
const confirmButton = confirmMode === "hold"
|
|
340
|
+
? _jsxs("button", { type: "button", className: styles.confirm, "data-autofocus": true, "aria-describedby": holdHintId, onPointerDown: holdPointer, onPointerUp: releaseHold, onPointerCancel: releaseHold, onLostPointerCapture: releaseHold, onKeyDown: event => holdKeys(event, true), onKeyUp: event => holdKeys(event, false), onBlur: releaseHold, onContextMenu: event => event.preventDefault(), children: [_jsx("span", { children: confirmLabel }), _jsx(motion.span, { className: styles.fill, style: { clipPath: fillClip }, "aria-hidden": "true", children: _jsx("span", { children: confirmLabel }) })] })
|
|
341
|
+
: _jsx("button", { type: "button", className: styles.confirm, "data-autofocus": true, onClick: event => answer(event.detail === 0), onKeyDown: event => { if (event.repeat)
|
|
342
|
+
event.preventDefault(); }, children: confirmLabel });
|
|
343
|
+
const face = (() => {
|
|
344
|
+
switch (faceKey) {
|
|
345
|
+
case "confirming": return _jsxs(_Fragment, { children: [_jsx(Part, { reduced: reduced, className: styles.prompt, children: _jsx("span", { id: promptId, children: shownPrompt }) }), _jsx(Part, { reduced: reduced, children: _jsx("button", { type: "button", className: styles.secondary, onClick: cancel, children: cancelLabel }) }), _jsx(Part, { reduced: reduced, children: confirmButton }), confirmMode === "hold" && _jsx("span", { id: holdHintId, className: styles.srOnly, children: "Press and hold" })] });
|
|
346
|
+
case "counting": return _jsxs(_Fragment, { children: [_jsxs(Part, { reduced: reduced, className: styles.status, children: [_jsx(Ring, { progress: remaining }), _jsxs("span", { className: styles.statusText, children: [shownCountdown, " ", _jsx(Seconds, { value: seconds, reduced: reduced })] })] }), _jsx(Part, { reduced: reduced, children: _jsx("button", { type: "button", className: styles.secondary, onClick: cancel, children: cancelLabel }) })] });
|
|
347
|
+
case "pending": return _jsxs(Part, { reduced: reduced, className: styles.status, children: [_jsx(LoaderCircle, { className: styles.spinner, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("span", { className: styles.statusText, children: working === "undo" ? undoingLabel : pendingLabel })] });
|
|
348
|
+
case "done": return _jsxs(_Fragment, { children: [_jsxs(Part, { reduced: reduced, className: styles.status, children: [_jsx(Check, { reduced: reduced }), _jsx("span", { className: styles.statusText, children: doneLabel })] }), onUndo && _jsx(Part, { reduced: reduced, children: _jsx("button", { type: "button", className: styles.secondary, "data-autofocus": true, onClick: () => void perform("undo"), children: undoLabel }) })] });
|
|
349
|
+
case "error": return _jsxs(_Fragment, { children: [_jsxs(Part, { reduced: reduced, className: styles.status, children: [_jsx(CircleAlert, { className: styles.alert, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("span", { className: styles.statusText, children: errorLabel })] }), _jsx(Part, { reduced: reduced, children: _jsx("button", { type: "button", className: styles.secondary, "data-autofocus": true, onClick: () => void perform(working), children: retryLabel }) })] });
|
|
350
|
+
default: return _jsx(Part, { reduced: reduced, children: _jsxs("button", { type: "button", className: styles.trigger, "data-autofocus": true, disabled: disabled, onClick: () => { setAnnouncement(typeof shownPrompt === "string" ? shownPrompt : ""); go("confirming"); }, children: [icon && _jsx("span", { className: styles.icon, "aria-hidden": "true", children: icon }), _jsx("span", { children: label })] }) });
|
|
351
|
+
}
|
|
352
|
+
})();
|
|
353
|
+
return _jsxs("div", { ref: rootRef, className: [styles.root, className].filter(Boolean).join(" "), "data-state": state, "data-face": faceKey, "data-tone": tone, "data-mode": confirmMode, "data-disabled": disabled || undefined, tabIndex: -1, onKeyDown: onKeyDown, "aria-busy": state === "pending" || undefined, onPointerEnter: () => { holds.current.hover = true; sync(); }, onPointerLeave: () => { holds.current.hover = false; sync(); }, onPointerCancel: () => { holds.current.hover = false; sync(); }, children: [_jsx(motion.div, { className: styles.surface, style: { width }, children: _jsx(AnimatePresence, { initial: false, children: _jsx(Face, { id: faceKey, reduced: reduced, onSize: onFaceSize, labelledBy: faceKey === "confirming" ? promptId : undefined, children: face }, faceKey) }) }), _jsx("span", { className: styles.srOnly, role: "status", "aria-live": "polite", children: announcement })] });
|
|
354
|
+
}
|
|
355
|
+
export default ConfirmMorph;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/* The root holds the control in the flow of the page; the pill inside it springs between faces. */
|
|
2
|
+
.root {
|
|
3
|
+
position: relative;
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
min-width: 0;
|
|
6
|
+
max-width: 100%;
|
|
7
|
+
vertical-align: middle;
|
|
8
|
+
color: var(--foreground);
|
|
9
|
+
font-family: var(--font-body);
|
|
10
|
+
font-size: var(--text-sm);
|
|
11
|
+
letter-spacing: var(--tracking-body);
|
|
12
|
+
line-height: 20px;
|
|
13
|
+
-webkit-tap-highlight-color: transparent;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* One neutral pill for every state. Only its width moves; corners, text and border never scale, so nothing distorts.
|
|
18
|
+
* Every face sits at its natural width against the pill's leading edge, so text never slides or squeezes: the question
|
|
19
|
+
* starts where the label was, and the springing edge reveals the rest. The border is an overlay so it never changes a measurement.
|
|
20
|
+
*/
|
|
21
|
+
.surface {
|
|
22
|
+
position: relative;
|
|
23
|
+
display: flex;
|
|
24
|
+
height: var(--control-height-sm);
|
|
25
|
+
max-width: 100%;
|
|
26
|
+
justify-content: flex-start;
|
|
27
|
+
overflow: clip;
|
|
28
|
+
border-radius: var(--radius-full); corner-shape: round;
|
|
29
|
+
background: var(--surface-raised);
|
|
30
|
+
box-shadow: var(--shadow-resting);
|
|
31
|
+
transition: background-color var(--duration-standard) var(--ease-standard), box-shadow var(--duration-standard) var(--ease-standard);
|
|
32
|
+
}
|
|
33
|
+
.surface::after { content: ""; position: absolute; inset: 0; border: 1px solid var(--border); border-radius: inherit; corner-shape: inherit; pointer-events: none; transition: border-color var(--duration-standard) var(--ease-standard); }
|
|
34
|
+
.root[data-disabled][data-state="idle"] .surface { box-shadow: none; }
|
|
35
|
+
.root[data-disabled][data-state="idle"] .surface::after { border-color: var(--border-subtle); }
|
|
36
|
+
|
|
37
|
+
.face { display: flex; flex: none; height: 100%; align-items: center; gap: 2px; padding: 0 4px; white-space: nowrap; }
|
|
38
|
+
.face[data-face="idle"] { padding: 0; }
|
|
39
|
+
/* A leaving face steps out of the flow, exactly where it was, and fades while the pill springs to the next one. */
|
|
40
|
+
.face[inert] { position: absolute; top: 0; inset-inline-start: 0; pointer-events: none; }
|
|
41
|
+
.part { display: inline-flex; height: 100%; align-items: center; }
|
|
42
|
+
|
|
43
|
+
.trigger {
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
height: 100%;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: 8px;
|
|
48
|
+
padding: 0 16px 0 14px;
|
|
49
|
+
border: 0;
|
|
50
|
+
border-radius: var(--radius-full); corner-shape: round;
|
|
51
|
+
background: transparent;
|
|
52
|
+
color: var(--foreground);
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
font: inherit;
|
|
55
|
+
font-weight: 500;
|
|
56
|
+
transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
|
|
57
|
+
}
|
|
58
|
+
.trigger:active:not(:disabled) { background: color-mix(in oklab, var(--foreground) 9%, transparent); transition-duration: 0ms; }
|
|
59
|
+
.trigger:disabled { color: var(--text-muted); cursor: not-allowed; }
|
|
60
|
+
.trigger:disabled .icon { opacity: .7; }
|
|
61
|
+
.icon { display: grid; width: 16px; height: 16px; place-items: center; color: var(--text-secondary); }
|
|
62
|
+
.trigger:disabled .icon { color: inherit; }
|
|
63
|
+
.icon > svg { width: 16px; height: 16px; }
|
|
64
|
+
|
|
65
|
+
.prompt { max-width: 16rem; overflow: hidden; padding: 0 8px 0 12px; color: var(--foreground); font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
66
|
+
.prompt > span { overflow: hidden; text-overflow: ellipsis; }
|
|
67
|
+
|
|
68
|
+
.secondary,
|
|
69
|
+
.confirm {
|
|
70
|
+
position: relative;
|
|
71
|
+
display: inline-flex;
|
|
72
|
+
flex: none;
|
|
73
|
+
height: calc(var(--control-height-sm) - 8px);
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
padding: 0 12px;
|
|
77
|
+
border: 0;
|
|
78
|
+
border-radius: var(--radius-full); corner-shape: round;
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
font: inherit;
|
|
81
|
+
font-weight: 500;
|
|
82
|
+
transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
|
|
83
|
+
touch-action: manipulation;
|
|
84
|
+
}
|
|
85
|
+
.secondary { background: transparent; color: var(--text-secondary); }
|
|
86
|
+
.face:is([data-face="done"], [data-face="error"]) .secondary { color: var(--foreground); }
|
|
87
|
+
.secondary:active { background: color-mix(in oklab, var(--foreground) 11%, transparent); color: var(--foreground); transition-duration: 0ms; }
|
|
88
|
+
|
|
89
|
+
/* The only coloured thing in the control: danger for a destructive answer, the accent for a reversible one. */
|
|
90
|
+
.confirm { overflow: clip; background: color-mix(in oklab, var(--danger) 13%, transparent); color: var(--danger); }
|
|
91
|
+
.confirm:active { background: color-mix(in oklab, var(--danger) 22%, transparent); transition-duration: 0ms; }
|
|
92
|
+
.root[data-tone="neutral"] .confirm { background: var(--accent); color: var(--accent-foreground); }
|
|
93
|
+
.root[data-tone="neutral"] .confirm:active { background: var(--accent-strong); }
|
|
94
|
+
|
|
95
|
+
/* Hold: a solid copy of the button, label and all, revealed from the left as the hold fills; the label inverts as the edge passes. */
|
|
96
|
+
.root[data-mode="hold"] .confirm { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
|
|
97
|
+
.fill { position: absolute; inset: 0; display: inline-flex; align-items: center; justify-content: center; background: var(--danger); color: var(--background); pointer-events: none; }
|
|
98
|
+
.root[data-tone="neutral"] .fill { background: var(--accent-strong); color: var(--accent-foreground); }
|
|
99
|
+
|
|
100
|
+
.status { gap: 8px; padding: 0 10px 0 12px; color: var(--foreground); font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
101
|
+
.status > svg { flex: none; }
|
|
102
|
+
.statusText { display: inline-flex; align-items: baseline; gap: .25em; }
|
|
103
|
+
.face[data-face="pending"] .status { padding-inline: 12px 16px; color: var(--text-secondary); }
|
|
104
|
+
.face[data-face="done"]:not(:has(.secondary)) .status { padding-right: 14px; }
|
|
105
|
+
.face[data-face="counting"] .status { padding-right: 6px; color: var(--text-secondary); }
|
|
106
|
+
.alert { color: var(--danger); }
|
|
107
|
+
.check { width: 18px; height: 18px; }
|
|
108
|
+
.checkDisc { fill: var(--success); }
|
|
109
|
+
.checkTick { stroke: var(--background); }
|
|
110
|
+
.spinner { animation: spin .7s linear infinite; }
|
|
111
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
112
|
+
|
|
113
|
+
.ring { width: 16px; height: 16px; }
|
|
114
|
+
.ringTrack { stroke: var(--border); }
|
|
115
|
+
.ringFill { stroke: var(--danger); }
|
|
116
|
+
.root[data-tone="neutral"] .ringFill { stroke: var(--accent); }
|
|
117
|
+
/* Seconds keep one tabular width while the digits swap. */
|
|
118
|
+
.seconds { display: inline-grid; min-width: 1ch; color: var(--foreground); font-variant-numeric: tabular-nums; }
|
|
119
|
+
.digit { grid-area: 1 / 1; }
|
|
120
|
+
|
|
121
|
+
/* No focus rings anywhere in the control; keyboard focus reads through fill (below). */
|
|
122
|
+
.root, .root :focus { outline: none; }
|
|
123
|
+
|
|
124
|
+
.srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
125
|
+
|
|
126
|
+
@media (hover: hover) and (pointer: fine) {
|
|
127
|
+
.trigger:hover:not(:disabled):not(:active) { background: color-mix(in oklab, var(--foreground) 5%, transparent); }
|
|
128
|
+
.secondary:hover:not(:active) { background: color-mix(in oklab, var(--foreground) 7%, transparent); color: var(--foreground); }
|
|
129
|
+
.confirm:hover:not(:active) { background: color-mix(in oklab, var(--danger) 18%, transparent); }
|
|
130
|
+
.root[data-tone="neutral"] .confirm:hover:not(:active) { background: var(--accent-strong); }
|
|
131
|
+
}
|
|
132
|
+
/* Keyboard focus reads through fill, never a ring. */
|
|
133
|
+
.trigger:focus-visible { background: color-mix(in oklab, var(--foreground) 6%, transparent); }
|
|
134
|
+
.secondary:focus-visible { background: color-mix(in oklab, var(--foreground) 8%, transparent); color: var(--foreground); }
|
|
135
|
+
.confirm:focus-visible { background: color-mix(in oklab, var(--danger) 20%, transparent); }
|
|
136
|
+
.root[data-tone="neutral"] .confirm:focus-visible { background: var(--accent-strong); }
|
|
137
|
+
|
|
138
|
+
@media (prefers-reduced-motion: reduce) {
|
|
139
|
+
.surface, .surface::after, .trigger, .secondary, .confirm { transition-duration: 0ms; }
|
|
140
|
+
.spinner { animation-duration: 1.6s; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@media (prefers-contrast: more) {
|
|
144
|
+
.surface::after { border-color: var(--border-strong); }
|
|
145
|
+
.confirm { background: color-mix(in oklab, var(--danger) 22%, transparent); }
|
|
146
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface ContextMenuItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
onSelect?: () => void;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
destructive?: boolean;
|
|
9
|
+
checked?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ContextMenuProps {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
items: ContextMenuItem[];
|
|
14
|
+
label?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function ContextMenu({ children, items, label }: ContextMenuProps): import("react").JSX.Element;
|
|
17
|
+
export declare const contextMenuExampleItems: ContextMenuItem[];
|
|
18
|
+
export default ContextMenu;
|