@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,382 @@
|
|
|
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, frame, cancelFrame, motion, motionValue, useInView, useReducedMotion } from "motion/react";
|
|
5
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
6
|
+
import styles from "./donut-chart.module.css";
|
|
7
|
+
const OTHER = "__other";
|
|
8
|
+
const TAU = Math.PI * 2;
|
|
9
|
+
const { spring, ease, stagger, duration } = motionTokens;
|
|
10
|
+
/** Motion drops inherited velocity on time-defined springs, so values that retarget mid-flight run the same springs written as stiffness and damping. */
|
|
11
|
+
const physical = ({ visualDuration, bounce }, restDelta = .0005) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta, restSpeed: restDelta * 2 }; };
|
|
12
|
+
const reveal = physical({ visualDuration: .72, bounce: 0 });
|
|
13
|
+
const settle = physical({ visualDuration: .5, bounce: 0 });
|
|
14
|
+
const pop = physical(spring.snappy, .002);
|
|
15
|
+
/** Neutral steps after the four series colors, and the quietest step for Other. */
|
|
16
|
+
const NEUTRAL = [56, 42, 32];
|
|
17
|
+
const seriesColor = (index) => index < 4 ? `var(--series-${index + 1})` : `color-mix(in oklab, var(--foreground) ${NEUTRAL[(index - 4) % NEUTRAL.length]}%, var(--surface))`;
|
|
18
|
+
const OTHER_COLOR = "color-mix(in oklab, var(--foreground) 26%, var(--surface))";
|
|
19
|
+
/** Room left outside the ring for the lift; the gap between segments and their corner radius, in pixels. */
|
|
20
|
+
const LIFT = 4, GROW = 2, MARGIN = 7, GAP = 3, CORNER = 4;
|
|
21
|
+
const grouped = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });
|
|
22
|
+
const percent = new Intl.NumberFormat("en-US", { style: "percent", maximumFractionDigits: 0 });
|
|
23
|
+
const shareText = (share) => share > 0 && share < .01 ? "<1%" : percent.format(share);
|
|
24
|
+
/** Small parts join one "Other" segment, but only when at least two would; grouping a single part hides it for nothing. */
|
|
25
|
+
function sliceData(data, groupBelow, maxSegments, otherLabel) {
|
|
26
|
+
const parts = data.filter(item => item.value > 0);
|
|
27
|
+
const total = parts.reduce((sum, item) => sum + item.value, 0);
|
|
28
|
+
if (!total)
|
|
29
|
+
return [];
|
|
30
|
+
const ranked = [...parts].sort((a, b) => b.value - a.value);
|
|
31
|
+
const keep = new Set(ranked.filter((item, rank) => item.value / total >= groupBelow && rank < maxSegments - 1).map(item => item.key));
|
|
32
|
+
const rest = parts.filter(item => !keep.has(item.key));
|
|
33
|
+
if (rest.length < 2)
|
|
34
|
+
return parts.map(item => ({ ...item }));
|
|
35
|
+
return [...parts.filter(item => keep.has(item.key)).map(item => ({ ...item })), { key: OTHER, label: otherLabel, value: rest.reduce((sum, item) => sum + item.value, 0), members: rest }];
|
|
36
|
+
}
|
|
37
|
+
/** Turns of the ring for each slice in data order; a hidden slice closes to a point where it sits. */
|
|
38
|
+
function layout(slices, hidden) {
|
|
39
|
+
const total = slices.reduce((sum, item) => sum + (hidden.has(item.key) ? 0 : item.value), 0) || 1;
|
|
40
|
+
let at = 0;
|
|
41
|
+
return new Map(slices.map(item => { const arc = { start: at, end: at + (hidden.has(item.key) ? 0 : item.value / total) }; at = arc.end; return [item.key, arc]; }));
|
|
42
|
+
}
|
|
43
|
+
/** Half the width, in pixels, of a sector's outer edge once the gap is cut; below a couple of pixels a segment fades rather than drawing a hairline. */
|
|
44
|
+
function breadth(R, t0, t1, gap) {
|
|
45
|
+
const turns = t1 - t0;
|
|
46
|
+
if (turns >= 1 - 1e-6)
|
|
47
|
+
return Infinity;
|
|
48
|
+
return Math.sin(Math.min(turns * Math.PI, Math.PI / 2)) * R - Math.min(gap / 2, ((1 - turns) * TAU * R) / 2);
|
|
49
|
+
}
|
|
50
|
+
const n2 = (value) => (Math.round(value * 100) / 100).toString();
|
|
51
|
+
/**
|
|
52
|
+
* An annular sector between two turns of the ring, measured clockwise from the top, with rounded corners.
|
|
53
|
+
* Its sides run parallel to the radius at a fixed distance, so the gap between neighbours is the same number of pixels from the inner edge to the outer one.
|
|
54
|
+
* A thin sector narrows to a wedge and then to nothing, so a closing segment never pops.
|
|
55
|
+
*/
|
|
56
|
+
function sector(c, R, r, t0, t1, gap, corner) {
|
|
57
|
+
const turns = t1 - t0;
|
|
58
|
+
if (turns <= 1e-6)
|
|
59
|
+
return "";
|
|
60
|
+
const at = (radius, angle) => `${n2(c + radius * Math.cos(angle))} ${n2(c + radius * Math.sin(angle))}`;
|
|
61
|
+
if (turns >= 1 - 1e-6)
|
|
62
|
+
return `M${at(R, -Math.PI / 2)}A${n2(R)} ${n2(R)} 0 1 1 ${at(R, Math.PI / 2)}A${n2(R)} ${n2(R)} 0 1 1 ${at(R, -Math.PI / 2)}ZM${at(r, -Math.PI / 2)}A${n2(r)} ${n2(r)} 0 1 0 ${at(r, Math.PI / 2)}A${n2(r)} ${n2(r)} 0 1 0 ${at(r, -Math.PI / 2)}Z`;
|
|
63
|
+
// Half the gap on each side; a lone segment closes its gap as the rest of the ring empties, so it becomes a whole ring without a jump.
|
|
64
|
+
const p = Math.min(gap / 2, ((1 - turns) * TAU * R) / 2);
|
|
65
|
+
const a0 = t0 * TAU - Math.PI / 2, a1 = t1 * TAU - Math.PI / 2, half = (a1 - a0) / 2, s = Math.sin(Math.min(half, Math.PI / 2));
|
|
66
|
+
if (s * R <= p + 1e-6)
|
|
67
|
+
return "";
|
|
68
|
+
const side = (t, angle, sign) => `${n2(c + t * Math.cos(angle) - sign * p * Math.sin(angle))} ${n2(c + t * Math.sin(angle) + sign * p * Math.cos(angle))}`;
|
|
69
|
+
const band = (R - r) / 2;
|
|
70
|
+
const ro = Math.max(0, Math.min(corner, band, s >= .9999 ? Infinity : (s * R - p) / (1 + s)));
|
|
71
|
+
const dO = Math.asin(Math.min(1, (p + ro) / (R - ro))), tO = Math.sqrt(Math.max(0, (R - ro) ** 2 - (p + ro) ** 2));
|
|
72
|
+
const bigO = a1 - a0 - 2 * dO > Math.PI ? 1 : 0;
|
|
73
|
+
let d = `M${side(tO, a0, 1)}`;
|
|
74
|
+
if (ro > .01)
|
|
75
|
+
d += `A${n2(ro)} ${n2(ro)} 0 0 1 ${at(R, a0 + dO)}`;
|
|
76
|
+
d += `A${n2(R)} ${n2(R)} 0 ${bigO} 1 ${at(R, a1 - dO)}`;
|
|
77
|
+
if (ro > .01)
|
|
78
|
+
d += `A${n2(ro)} ${n2(ro)} 0 0 1 ${side(tO, a1, -1)}`;
|
|
79
|
+
if (s * r > p + 1e-6 || s >= .9999) {
|
|
80
|
+
const ri = Math.max(0, Math.min(corner, band, s >= .9999 ? Infinity : (s * r - p) / (1 - s)));
|
|
81
|
+
const dI = Math.asin(Math.min(1, (p + ri) / (r + ri))), tI = Math.sqrt(Math.max(0, (r + ri) ** 2 - (p + ri) ** 2));
|
|
82
|
+
const bigI = a1 - a0 - 2 * dI > Math.PI ? 1 : 0;
|
|
83
|
+
d += `L${side(tI, a1, -1)}`;
|
|
84
|
+
if (ri > .01)
|
|
85
|
+
d += `A${n2(ri)} ${n2(ri)} 0 0 1 ${at(r, a1 - dI)}`;
|
|
86
|
+
d += `A${n2(r)} ${n2(r)} 0 ${bigI} 0 ${at(r, a0 + dI)}`;
|
|
87
|
+
if (ri > .01)
|
|
88
|
+
d += `A${n2(ri)} ${n2(ri)} 0 0 1 ${side(tI, a0, 1)}`;
|
|
89
|
+
}
|
|
90
|
+
else
|
|
91
|
+
d += `L${at(p / s, (a0 + a1) / 2)}`;
|
|
92
|
+
return `${d}Z`;
|
|
93
|
+
}
|
|
94
|
+
/** A number that counts to each new value, written straight to the DOM so no frame re-renders React. Whole targets count in whole steps. */
|
|
95
|
+
function Count({ value, format, reduced }) {
|
|
96
|
+
const node = useRef(null);
|
|
97
|
+
const [initial] = useState(() => format(value));
|
|
98
|
+
const live = useRef(null);
|
|
99
|
+
useLayoutEffect(() => {
|
|
100
|
+
const el = node.current;
|
|
101
|
+
if (!el)
|
|
102
|
+
return;
|
|
103
|
+
if (!live.current) {
|
|
104
|
+
const state = { mv: motionValue(value), format, target: value };
|
|
105
|
+
state.mv.on("change", now => { el.textContent = state.format(Number.isInteger(state.target) ? Math.round(now) : now); });
|
|
106
|
+
live.current = state;
|
|
107
|
+
}
|
|
108
|
+
const state = live.current;
|
|
109
|
+
state.format = format;
|
|
110
|
+
if (state.target === value && !reduced) {
|
|
111
|
+
if (!state.mv.isAnimating())
|
|
112
|
+
el.textContent = format(value);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
state.target = value;
|
|
116
|
+
if (reduced) {
|
|
117
|
+
state.mv.jump(value);
|
|
118
|
+
el.textContent = format(value);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
animate(state.mv, value, physical({ visualDuration: .4, bounce: 0 }, Math.max(Math.abs(value - state.mv.get()) * .001, 1e-6)));
|
|
122
|
+
}, [value, format, reduced]);
|
|
123
|
+
useEffect(() => () => { live.current?.mv.destroy(); live.current = null; }, []);
|
|
124
|
+
return _jsx("span", { ref: node, children: initial });
|
|
125
|
+
}
|
|
126
|
+
/** Center readouts stay mounted in one grid cell and roll like a drum: the ones before the active segment wait above, the ones after it below. */
|
|
127
|
+
const drum = (place, reduced) => ({
|
|
128
|
+
animate: { opacity: place === 0 ? 1 : 0, y: `${.45 * Math.sign(place)}em` },
|
|
129
|
+
transition: reduced ? { duration: 0 } : place === 0 ? { duration: duration.standard, ease: [...ease.enter] } : { duration: duration.fast, ease: [...ease.standard] },
|
|
130
|
+
});
|
|
131
|
+
const subscribeNothing = () => () => { };
|
|
132
|
+
function useReducedMotionSafe() {
|
|
133
|
+
const hydrated = useSyncExternalStore(subscribeNothing, () => true, () => false);
|
|
134
|
+
return !!useReducedMotion() && hydrated;
|
|
135
|
+
}
|
|
136
|
+
export function DonutChart({ data, label, unit = "", formatValue = value => grouped.format(value), totalLabel = "Total", size = 208, thickness = 24, groupBelow = .04, maxSegments = 6, otherLabel = "Other", activeKey, defaultActiveKey = null, onActiveChange, hiddenKeys, defaultHiddenKeys, onHiddenKeysChange, legendAction = "toggle", legend = true, emptyLabel = "No data yet", ref, className }) {
|
|
137
|
+
const reduced = useReducedMotionSafe();
|
|
138
|
+
const figure = useRef(null);
|
|
139
|
+
useImperativeHandle(ref, () => figure.current);
|
|
140
|
+
const inView = useInView(figure, { once: true, amount: .35 });
|
|
141
|
+
const legendId = useId();
|
|
142
|
+
const slices = sliceData(data, groupBelow, maxSegments, otherLabel);
|
|
143
|
+
// Hidden segments are controlled or not, like selection.
|
|
144
|
+
const [ownHidden, setOwnHidden] = useState(defaultHiddenKeys ?? []);
|
|
145
|
+
const hiddenList = hiddenKeys ?? ownHidden;
|
|
146
|
+
const hidden = new Set(hiddenList.filter(key => slices.some(item => item.key === key)));
|
|
147
|
+
const visible = slices.filter(item => !hidden.has(item.key));
|
|
148
|
+
const total = visible.reduce((sum, item) => sum + item.value, 0);
|
|
149
|
+
const signature = `${slices.map(item => `${item.key}:${item.value}${hidden.has(item.key) ? "h" : ""}`).join("|")}`;
|
|
150
|
+
const suffix = unit ? ` ${unit}` : "";
|
|
151
|
+
// Colors follow a key from the first time it is seen, so a segment keeps its color through every dataset.
|
|
152
|
+
const [palette, setPalette] = useState(() => new Map(data.map((item, index) => [item.key, index])));
|
|
153
|
+
if (data.some(item => !palette.has(item.key))) {
|
|
154
|
+
const next = new Map(palette);
|
|
155
|
+
for (const item of data)
|
|
156
|
+
if (!next.has(item.key))
|
|
157
|
+
next.set(item.key, next.size);
|
|
158
|
+
setPalette(next);
|
|
159
|
+
}
|
|
160
|
+
const colorFor = (item) => item.color ?? (item.key === OTHER ? OTHER_COLOR : seriesColor(palette.get(item.key) ?? 0));
|
|
161
|
+
// Leaving segments stay drawn in their old place while they close, then drop out once they have.
|
|
162
|
+
const [drawn, setDrawn] = useState(slices);
|
|
163
|
+
const [seenSignature, setSeenSignature] = useState(signature);
|
|
164
|
+
if (seenSignature !== signature) {
|
|
165
|
+
setSeenSignature(signature);
|
|
166
|
+
const next = [...slices];
|
|
167
|
+
drawn.forEach((item, index) => {
|
|
168
|
+
if (next.some(slice => slice.key === item.key))
|
|
169
|
+
return;
|
|
170
|
+
const before = drawn.slice(0, index).reverse().find(prior => next.some(slice => slice.key === prior.key));
|
|
171
|
+
next.splice(before ? next.findIndex(slice => slice.key === before.key) + 1 : 0, 0, { ...item, value: 0 });
|
|
172
|
+
});
|
|
173
|
+
setDrawn(next);
|
|
174
|
+
}
|
|
175
|
+
// Selection is controlled or not; hover and focus preview on top of it.
|
|
176
|
+
const [ownActive, setOwnActive] = useState(defaultActiveKey);
|
|
177
|
+
const selected = activeKey !== undefined ? activeKey : ownActive;
|
|
178
|
+
const [preview, setPreview] = useState(null);
|
|
179
|
+
const select = (key) => { if (activeKey === undefined)
|
|
180
|
+
setOwnActive(key); onActiveChange?.(key); };
|
|
181
|
+
const current = [preview, selected].find(key => key && visible.some(item => item.key === key)) ?? null;
|
|
182
|
+
const activeSlice = visible.find(item => item.key === current) ?? null;
|
|
183
|
+
const [announcement, setAnnouncement] = useState("");
|
|
184
|
+
const setHidden = (key) => {
|
|
185
|
+
const isHidden = hidden.has(key);
|
|
186
|
+
if (!isHidden && visible.length <= 1)
|
|
187
|
+
return;
|
|
188
|
+
const next = isHidden ? hiddenList.filter(item => item !== key) : [...hiddenList, key];
|
|
189
|
+
if (hiddenKeys === undefined)
|
|
190
|
+
setOwnHidden(next);
|
|
191
|
+
onHiddenKeysChange?.(next);
|
|
192
|
+
const item = slices.find(slice => slice.key === key);
|
|
193
|
+
const nextTotal = slices.reduce((sum, slice) => sum + (next.includes(slice.key) ? 0 : slice.value), 0);
|
|
194
|
+
if (item)
|
|
195
|
+
setAnnouncement(`${item.label} ${isHidden ? "shown" : "hidden"}. ${totalLabel} ${formatValue(nextTotal)}${suffix}.`);
|
|
196
|
+
};
|
|
197
|
+
// Geometry in the chart's own units; the SVG scales with its container.
|
|
198
|
+
const c = size / 2, outer = c - MARGIN, inner = Math.max(8, outer - thickness);
|
|
199
|
+
const corner = Math.min(CORNER, thickness / 4);
|
|
200
|
+
const geometry = useRef({ c, outer, inner, corner });
|
|
201
|
+
const segments = useRef(new Map());
|
|
202
|
+
const paths = useRef(new Map());
|
|
203
|
+
const registerPath = useCallback((node) => {
|
|
204
|
+
const key = node?.dataset.key;
|
|
205
|
+
if (!node || !key)
|
|
206
|
+
return;
|
|
207
|
+
paths.current.set(key, node);
|
|
208
|
+
return () => { if (paths.current.get(key) === node)
|
|
209
|
+
paths.current.delete(key); };
|
|
210
|
+
}, []);
|
|
211
|
+
// One paint per frame, whatever moved: every path's shape and lift are written straight to the DOM.
|
|
212
|
+
const paint = useCallback(() => {
|
|
213
|
+
const { c, outer, inner, corner } = geometry.current;
|
|
214
|
+
for (const [key, node] of paths.current) {
|
|
215
|
+
const segment = segments.current.get(key);
|
|
216
|
+
if (!segment) {
|
|
217
|
+
node.setAttribute("d", "");
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const start = segment.start.get(), end = segment.end.get(), lift = Math.max(0, segment.lift.get());
|
|
221
|
+
const R = outer + lift * GROW;
|
|
222
|
+
node.setAttribute("d", sector(c, R, inner - lift * GROW * .5, start, end, GAP, corner));
|
|
223
|
+
node.setAttribute("fill-opacity", n2(Math.min(1, Math.max(0, (breadth(R, start, end, GAP) - 1) / 3))));
|
|
224
|
+
const middle = ((start + end) / 2) * TAU - Math.PI / 2, distance = lift * LIFT;
|
|
225
|
+
node.setAttribute("transform", `translate(${n2(Math.cos(middle) * distance)} ${n2(Math.sin(middle) * distance)})`);
|
|
226
|
+
}
|
|
227
|
+
}, []);
|
|
228
|
+
const schedule = useCallback(() => { frame.render(paint); }, [paint]);
|
|
229
|
+
useLayoutEffect(() => { geometry.current = { c, outer, inner, corner }; paint(); });
|
|
230
|
+
useEffect(() => () => { cancelFrame(paint); for (const segment of segments.current.values())
|
|
231
|
+
segment.off(); segments.current.clear(); }, [paint]);
|
|
232
|
+
const targets = useRef(new Map());
|
|
233
|
+
const latest = useRef({ drawn, slices });
|
|
234
|
+
useLayoutEffect(() => { latest.current = { drawn, slices }; });
|
|
235
|
+
// New data, or a segment shown or hidden, moves every arc from wherever it is on screen, keeping its velocity if it was already moving.
|
|
236
|
+
const shown = inView || reduced;
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
if (!shown)
|
|
239
|
+
return;
|
|
240
|
+
const { drawn: order, slices: now } = latest.current;
|
|
241
|
+
const goal = layout(now, hidden);
|
|
242
|
+
targets.current = goal;
|
|
243
|
+
const first = segments.current.size === 0;
|
|
244
|
+
const place = (index) => { const before = order.slice(0, index).reverse().find(item => goal.has(item.key)); return before ? goal.get(before.key).end : 0; };
|
|
245
|
+
const drop = (key) => {
|
|
246
|
+
if (latest.current.slices.some(item => item.key === key))
|
|
247
|
+
return;
|
|
248
|
+
segments.current.get(key)?.off();
|
|
249
|
+
segments.current.delete(key);
|
|
250
|
+
setDrawn(list => list.filter(item => item.key !== key));
|
|
251
|
+
};
|
|
252
|
+
order.forEach((item, index) => {
|
|
253
|
+
const leaving = !goal.has(item.key);
|
|
254
|
+
const to = goal.get(item.key) ?? { start: place(index), end: place(index) };
|
|
255
|
+
let segment = segments.current.get(item.key);
|
|
256
|
+
if (!segment) {
|
|
257
|
+
// A new segment opens from the edge of the one before it, as it is right now.
|
|
258
|
+
const prior = order.slice(0, index).reverse().map(entry => segments.current.get(entry.key)).find(Boolean);
|
|
259
|
+
const from = first ? 0 : prior ? prior.end.get() : 0;
|
|
260
|
+
const start = motionValue(from), end = motionValue(from), lift = motionValue(0);
|
|
261
|
+
const offs = [start.on("change", schedule), end.on("change", schedule), lift.on("change", schedule)];
|
|
262
|
+
segment = { start, end, lift, off: () => { offs.forEach(off => off()); start.destroy(); end.destroy(); lift.destroy(); } };
|
|
263
|
+
segments.current.set(item.key, segment);
|
|
264
|
+
}
|
|
265
|
+
if (reduced) {
|
|
266
|
+
segment.start.jump(to.start);
|
|
267
|
+
segment.end.jump(to.end);
|
|
268
|
+
if (leaving)
|
|
269
|
+
drop(item.key);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (first) {
|
|
273
|
+
// One sweep: every segment leaves the top together, and each trailing edge follows a beat behind the one before.
|
|
274
|
+
animate(segment.start, to.start, reveal);
|
|
275
|
+
animate(segment.end, to.end, { ...reveal, delay: .04 + index * stagger.item });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
animate(segment.start, to.start, settle);
|
|
279
|
+
animate(segment.end, to.end, { ...settle, onComplete: leaving ? () => drop(item.key) : undefined });
|
|
280
|
+
});
|
|
281
|
+
schedule();
|
|
282
|
+
}, [shown, signature, reduced, schedule]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
283
|
+
// The active segment slides out along its middle and thickens a little; the one it replaces settles back at the same time.
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
for (const [key, segment] of segments.current) {
|
|
286
|
+
const to = key === current ? 1 : 0;
|
|
287
|
+
if (reduced)
|
|
288
|
+
segment.lift.jump(to);
|
|
289
|
+
else if (segment.lift.get() !== to || segment.lift.isAnimating())
|
|
290
|
+
animate(segment.lift, to, pop);
|
|
291
|
+
}
|
|
292
|
+
}, [current, reduced, drawn, shown]);
|
|
293
|
+
// One pointer handler for the whole ring: the angle picks the segment, so crossing a gap never drops the hover.
|
|
294
|
+
const svg = useRef(null);
|
|
295
|
+
const hit = (event) => {
|
|
296
|
+
const box = svg.current?.getBoundingClientRect();
|
|
297
|
+
if (!box || !box.width)
|
|
298
|
+
return undefined;
|
|
299
|
+
const scale = size / box.width, x = (event.clientX - box.left) * scale - c, y = (event.clientY - box.top) * scale - c;
|
|
300
|
+
const radius = Math.hypot(x, y);
|
|
301
|
+
if (radius < inner - 6 || radius > c)
|
|
302
|
+
return null;
|
|
303
|
+
let turn = (Math.atan2(y, x) + Math.PI / 2) / TAU;
|
|
304
|
+
if (turn < 0)
|
|
305
|
+
turn += 1;
|
|
306
|
+
for (const [key, arc] of targets.current)
|
|
307
|
+
if (arc.end > arc.start && turn >= arc.start && turn < arc.end && visible.some(item => item.key === key))
|
|
308
|
+
return key;
|
|
309
|
+
return undefined;
|
|
310
|
+
};
|
|
311
|
+
const onRingMove = (event) => {
|
|
312
|
+
if (event.pointerType !== "mouse")
|
|
313
|
+
return;
|
|
314
|
+
const key = hit(event);
|
|
315
|
+
if (key !== undefined && key !== preview)
|
|
316
|
+
setPreview(key);
|
|
317
|
+
};
|
|
318
|
+
const onRingClick = (event) => {
|
|
319
|
+
const key = hit(event);
|
|
320
|
+
if (key)
|
|
321
|
+
select(selected === key ? null : key);
|
|
322
|
+
};
|
|
323
|
+
const format = (value) => `${formatValue(value)}${suffix}`;
|
|
324
|
+
const describe = (item) => `${item.label}, ${format(item.value)}, ${shareText(item.value / (total || 1))}${item.members ? `. Includes ${item.members.map(member => member.label).join(", ")}` : ""}`;
|
|
325
|
+
// Arrow keys walk the segments, in the legend or on the ring itself when there is no legend.
|
|
326
|
+
const rows = useRef([]);
|
|
327
|
+
const step = (key, index, count) => ({ ArrowDown: index + 1, ArrowRight: index + 1, ArrowUp: index - 1, ArrowLeft: index - 1, Home: 0, End: count - 1 }[key]);
|
|
328
|
+
const onLegendKey = (event, index) => {
|
|
329
|
+
if (event.key === "Escape" && selected) {
|
|
330
|
+
event.preventDefault();
|
|
331
|
+
select(null);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const next = step(event.key, index, slices.length);
|
|
335
|
+
if (next === undefined)
|
|
336
|
+
return;
|
|
337
|
+
event.preventDefault();
|
|
338
|
+
rows.current[(next + slices.length) % slices.length]?.focus();
|
|
339
|
+
};
|
|
340
|
+
const onRingKey = (event) => {
|
|
341
|
+
if (!visible.length)
|
|
342
|
+
return;
|
|
343
|
+
if (event.key === "Escape") {
|
|
344
|
+
event.preventDefault();
|
|
345
|
+
setPreview(null);
|
|
346
|
+
if (selected)
|
|
347
|
+
select(null);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if ((event.key === "Enter" || event.key === " ") && current) {
|
|
351
|
+
event.preventDefault();
|
|
352
|
+
select(selected === current ? null : current);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const index = visible.findIndex(item => item.key === current);
|
|
356
|
+
const next = step(event.key, index < 0 ? (event.key.endsWith("Up") || event.key.endsWith("Left") ? 0 : -1) : index, visible.length);
|
|
357
|
+
if (next === undefined)
|
|
358
|
+
return;
|
|
359
|
+
event.preventDefault();
|
|
360
|
+
const item = visible[(next + visible.length) % visible.length];
|
|
361
|
+
setPreview(item.key);
|
|
362
|
+
setAnnouncement(describe(item));
|
|
363
|
+
};
|
|
364
|
+
const onLegendBlur = (event) => { if (!(event.relatedTarget instanceof Node && event.currentTarget.closest("ul")?.contains(event.relatedTarget)))
|
|
365
|
+
setPreview(null); };
|
|
366
|
+
// One readout per segment plus the total; the active one rolls in from the side it sits on, so moving clockwise rolls the drum forward.
|
|
367
|
+
const identity = activeSlice ? activeSlice.key : total ? "__total" : "__empty";
|
|
368
|
+
const readouts = [{ key: "__total", label: total ? totalLabel : emptyLabel, value: total, meta: null }, ...slices.map(item => ({ key: item.key, label: item.label, value: item.value, meta: item }))];
|
|
369
|
+
const activeIndex = Math.max(0, readouts.findIndex(item => item.key === (identity === "__empty" ? "__total" : identity)));
|
|
370
|
+
const summary = total ? `${label}. ${totalLabel} ${format(total)}. ${visible.map(describe).join(". ")}.` : `${label}. ${emptyLabel}.`;
|
|
371
|
+
const members = (item) => item.members ? item.members.length > 2 ? `${item.members.slice(0, 2).map(member => member.label).join(", ")} and ${item.members.length - 2} more` : item.members.map(member => member.label).join(" and ") : null;
|
|
372
|
+
const ringLabel = `${label}. Use the arrow keys to read each segment.`;
|
|
373
|
+
return _jsxs("figure", { ref: figure, className: [styles.figure, className].filter(Boolean).join(" "), "aria-label": label, "data-legend": legend || undefined, children: [_jsxs("div", { className: styles.ring, style: { width: size, "--inner": `${(inner * 2) / size * 100}%` }, tabIndex: legend ? undefined : 0, role: legend ? undefined : "group", "aria-label": legend ? undefined : ringLabel, onKeyDown: legend ? undefined : onRingKey, onBlur: legend ? undefined : () => setPreview(null), children: [_jsxs("svg", { ref: svg, className: styles.svg, viewBox: `0 0 ${size} ${size}`, "aria-hidden": "true", focusable: "false", onPointerMove: onRingMove, onPointerLeave: () => setPreview(null), onClick: onRingClick, "data-hovering": current !== null || undefined, children: [_jsx("circle", { className: styles.track, cx: c, cy: c, r: (outer + inner) / 2, strokeWidth: outer - inner, "data-idle": (!shown || !total) || undefined }), drawn.map(item => _jsx("path", { ref: registerPath, "data-key": item.key, className: styles.segment, "data-active": item.key === current || undefined, "data-dim": (current !== null && item.key !== current) || undefined, style: { "--slice": colorFor(item) } }, item.key))] }), _jsx("div", { className: styles.center, "aria-hidden": "true", children: readouts.map((item, index) => _jsxs(motion.span, { className: styles.readout, initial: false, ...drum(index - activeIndex, reduced), children: [_jsx("span", { className: styles.centerLabel, children: item.label }), _jsx("span", { className: styles.centerValue, children: item.value || item.meta ? _jsx(Count, { value: item.value, format: formatValue, reduced: reduced }) : " " }), _jsx("span", { className: styles.centerMeta, children: item.meta ? _jsxs(_Fragment, { children: [_jsx(Count, { value: total && !hidden.has(item.key) ? item.value / total : 0, format: shareText, reduced: reduced }), " of ", totalLabel.toLowerCase()] }) : unit || " " })] }, item.key)) })] }), legend && slices.length > 0 && _jsx("ul", { className: styles.legend, "aria-label": `${label}, segments${legendAction === "toggle" ? ". Press a segment to show or hide it" : ""}`, id: legendId, onPointerLeave: () => setPreview(null), children: _jsx(AnimatePresence, { initial: false, children: slices.map((item, index) => {
|
|
374
|
+
const off = hidden.has(item.key);
|
|
375
|
+
// A row that joins or leaves (a part folding into Other, say) opens or closes its height, so the rows below and the card around them glide instead of jumping.
|
|
376
|
+
return _jsx(motion.li, { className: styles.item, initial: { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0 }, transition: reduced ? { duration: 0 } : spring.smooth, children: _jsxs("button", { ref: node => { if (node)
|
|
377
|
+
rows.current[index] = node; }, type: "button", className: styles.row, "aria-pressed": legendAction === "toggle" ? !off : selected === item.key, "aria-label": off ? `${item.label}, hidden` : describe(item), "data-active": item.key === current || undefined, "data-pinned": selected === item.key || undefined, "data-hidden": off || undefined, style: { "--slice": colorFor(item) }, onClick: () => legendAction === "toggle" ? setHidden(item.key) : select(selected === item.key ? null : item.key), onPointerEnter: event => { if (event.pointerType === "mouse")
|
|
378
|
+
setPreview(off ? null : item.key); }, onFocus: event => { if (event.currentTarget.matches(":focus-visible"))
|
|
379
|
+
setPreview(off ? null : item.key); }, onBlur: onLegendBlur, onKeyDown: event => onLegendKey(event, index), children: [_jsx("span", { className: styles.dot, "aria-hidden": "true" }), _jsxs("span", { className: styles.rowText, children: [_jsx("span", { className: styles.rowLabel, children: item.label }), item.members && _jsx("span", { className: styles.rowMembers, children: members(item) })] }), _jsx("span", { className: styles.rowValue, "aria-hidden": "true", children: _jsx(Count, { value: item.value, format: formatValue, reduced: reduced }) }), _jsx("span", { className: styles.rowShare, "aria-hidden": "true", children: _jsx(Count, { value: off || !total ? 0 : item.value / total, format: shareText, reduced: reduced }) })] }) }, item.key);
|
|
380
|
+
}) }) }), _jsx("p", { className: styles.srOnly, children: summary }), _jsx("p", { className: styles.srOnly, "aria-live": "polite", children: announcement }), total > 0 && _jsxs("table", { className: styles.srOnly, children: [_jsx("caption", { children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Segment" }), _jsx("th", { scope: "col", children: "Value" }), _jsx("th", { scope: "col", children: "Share" })] }) }), _jsx("tbody", { children: visible.flatMap(item => item.members ? item.members.map(member => ({ key: member.key, label: `${member.label} (${otherLabel})`, value: member.value })) : [item]).map(item => _jsxs("tr", { children: [_jsx("th", { scope: "row", children: item.label }), _jsx("td", { children: format(item.value) }), _jsx("td", { children: shareText(item.value / total) })] }, item.key)) })] })] });
|
|
381
|
+
}
|
|
382
|
+
export default DonutChart;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* The ring sits beside its legend when there is room, and above it on a phone. */
|
|
2
|
+
.figure { container-type: inline-size; display: grid; min-width: 0; margin: 0; color: var(--foreground); font-family: var(--font-body); letter-spacing: var(--tracking-body); }
|
|
3
|
+
.figure > * { min-width: 0; }
|
|
4
|
+
.figure[data-legend] { gap: var(--space-section); }
|
|
5
|
+
@container (min-width: 460px) {
|
|
6
|
+
.figure[data-legend] { grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: var(--space-block); }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.ring { position: relative; max-width: 100%; aspect-ratio: 1; justify-self: center; -webkit-tap-highlight-color: transparent; }
|
|
10
|
+
.svg { display: block; width: 100%; height: 100%; overflow: visible; cursor: default; }
|
|
11
|
+
.svg[data-hovering] { cursor: pointer; }
|
|
12
|
+
/* The track shows only before the first sweep and when there is nothing to draw, so the gaps between segments stay the page colour. */
|
|
13
|
+
.track { fill: none; stroke: var(--surface-muted); transition: opacity var(--duration-considered, .48s) var(--ease-standard); opacity: 0; }
|
|
14
|
+
.track[data-idle] { opacity: 1; }
|
|
15
|
+
/* Shapes are written every frame from script; only colour and dimming ease here, on the compositor. */
|
|
16
|
+
.segment { fill: var(--slice); shape-rendering: geometricPrecision; transition: fill var(--duration-standard) var(--ease-standard), opacity var(--duration-standard) var(--ease-standard); }
|
|
17
|
+
.segment[data-dim] { opacity: .45; }
|
|
18
|
+
|
|
19
|
+
/* The centre reads the active segment, or the total at rest. Old and new readouts share one grid cell, so a swap never moves or resizes anything. */
|
|
20
|
+
.center { position: absolute; top: 50%; left: 50%; display: grid; width: calc(var(--inner) - 16px); transform: translate(-50%, -50%); pointer-events: none; }
|
|
21
|
+
.readout { grid-area: 1 / 1; display: grid; min-width: 0; justify-items: center; gap: 2px; text-align: center; }
|
|
22
|
+
.centerLabel, .centerMeta { display: block; width: 100%; min-height: calc(1em * var(--leading-body)); overflow: hidden; font-size: var(--text-xs); line-height: var(--leading-body); text-overflow: ellipsis; white-space: nowrap; }
|
|
23
|
+
.centerLabel { color: var(--text-secondary); }
|
|
24
|
+
.centerMeta { color: var(--text-muted); font-variant-numeric: tabular-nums; }
|
|
25
|
+
.centerValue { display: block; max-width: 100%; min-height: calc(1em * var(--leading-display)); overflow: hidden; font-family: var(--font-display); font-size: var(--text-2xl); font-weight: 500; letter-spacing: var(--tracking-display); line-height: var(--leading-display); font-variant-numeric: tabular-nums; text-overflow: ellipsis; white-space: nowrap; }
|
|
26
|
+
|
|
27
|
+
/* Legend rows mirror the segments: hovering either lights both. */
|
|
28
|
+
/* Rows carry their own 2px spacing so a row closing its height takes its gap with it; the list's negative margin drops the last one. */
|
|
29
|
+
.legend { display: grid; margin: 0 0 -2px; padding: 0; list-style: none; }
|
|
30
|
+
.item { overflow: hidden; }
|
|
31
|
+
.item > .row { margin-bottom: 2px; }
|
|
32
|
+
.row { display: grid; width: 100%; min-height: 40px; grid-template-columns: 10px minmax(0, 1fr) auto 3.25em; align-items: center; column-gap: 10px; padding: 6px 10px; border: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); cursor: pointer; font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); text-align: left; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
|
|
33
|
+
.row:active { transform: scale(.985); }
|
|
34
|
+
.row[data-active], .row[data-pinned] { background: color-mix(in oklab, var(--foreground) 5%, transparent); }
|
|
35
|
+
.dot { width: 10px; height: 10px; border-radius: 50%; corner-shape: round; background: var(--slice); box-shadow: inset 0 0 0 1.5px var(--slice); transition: background-color var(--duration-standard) var(--ease-standard), box-shadow var(--duration-standard) var(--ease-standard); }
|
|
36
|
+
/* A hidden segment keeps its row: the dot empties to a ring and the text quiets, so it can be shown again. */
|
|
37
|
+
.row[data-hidden] .dot { background: transparent; }
|
|
38
|
+
.row[data-hidden] .rowLabel, .row[data-hidden] .rowValue, .row[data-hidden] .rowShare { color: var(--text-muted); }
|
|
39
|
+
.rowLabel, .rowValue, .rowShare { transition: color var(--duration-standard) var(--ease-standard); }
|
|
40
|
+
.rowText { display: grid; min-width: 0; }
|
|
41
|
+
.rowLabel, .rowMembers { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
42
|
+
.rowMembers { color: var(--text-muted); font-size: var(--text-xs); }
|
|
43
|
+
.rowValue { justify-self: end; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
|
|
44
|
+
.rowShare { justify-self: end; color: var(--foreground); font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
45
|
+
@media (hover: hover) and (pointer: fine) { .row:hover { background: color-mix(in oklab, var(--foreground) 5%, transparent); } }
|
|
46
|
+
|
|
47
|
+
.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
48
|
+
@media (prefers-reduced-motion: reduce) { .segment, .track, .row, .dot, .rowLabel, .rowValue, .rowShare { transition: none; } }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
2
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* The last item a drawer showed. Pass the selected item (or null when nothing is open) and render the drawer's content
|
|
5
|
+
* from the result, so the content stays on screen while the panel slides out instead of vanishing the moment it closes.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useDrawerItem<T>(item: T | null | undefined): T | null | undefined;
|
|
8
|
+
export declare function Drawer({ open: openProp, defaultOpen, onOpenChange, ...props }: ComponentPropsWithoutRef<typeof DialogPrimitive.Root>): import("react").JSX.Element;
|
|
9
|
+
export declare const DrawerTrigger: import("react").ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
10
|
+
export declare const DrawerClose: import("react").ForwardRefExoticComponent<DialogPrimitive.DialogCloseProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
11
|
+
export interface DrawerContentProps extends ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
side?: "left" | "right" | "top" | "bottom";
|
|
16
|
+
/** Renders the drawer inside this element instead of the page body, anchored to its edges. The element needs position: relative and overflow: hidden. */
|
|
17
|
+
container?: HTMLElement | null;
|
|
18
|
+
}
|
|
19
|
+
export declare function DrawerContent({ title, description, children, side, container, className, onInteractOutside, onEscapeKeyDown, onOpenAutoFocus, onCloseAutoFocus, ...props }: DrawerContentProps): import("react").JSX.Element;
|
|
20
|
+
export default Drawer;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
4
|
+
import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion } from "motion/react";
|
|
6
|
+
import { X } from "lucide-react";
|
|
7
|
+
import { ESCAPE_LAYER_ATTRIBUTE, layerEscape } from "../../lib/escape.js";
|
|
8
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
9
|
+
import { useReturnFocus } from "../../lib/return-focus.js";
|
|
10
|
+
import styles from "./drawer.module.css";
|
|
11
|
+
/**
|
|
12
|
+
* The last item a drawer showed. Pass the selected item (or null when nothing is open) and render the drawer's content
|
|
13
|
+
* from the result, so the content stays on screen while the panel slides out instead of vanishing the moment it closes.
|
|
14
|
+
*/
|
|
15
|
+
export function useDrawerItem(item) {
|
|
16
|
+
const [last, setLast] = useState(item);
|
|
17
|
+
if (item != null && item !== last)
|
|
18
|
+
setLast(item);
|
|
19
|
+
return item ?? last;
|
|
20
|
+
}
|
|
21
|
+
/** Mirrors the open state so the panel can stay mounted while it slides out, retarget mid-flight, and close itself after a drag. */
|
|
22
|
+
const DrawerContext = createContext(null);
|
|
23
|
+
export function Drawer({ open: openProp, defaultOpen = false, onOpenChange, ...props }) {
|
|
24
|
+
const [uncontrolled, setUncontrolled] = useState(defaultOpen);
|
|
25
|
+
// A drag that dismisses the panel hands its velocity to the exit spring; every other close uses the shorter tween.
|
|
26
|
+
const [flung, setFlung] = useState(false);
|
|
27
|
+
// When the drawer last opened, so a click that reopens it mid-close is not also read as a click outside the leaving panel.
|
|
28
|
+
const openedAt = useRef(0);
|
|
29
|
+
const open = openProp ?? uncontrolled;
|
|
30
|
+
const setOpen = useCallback((next) => {
|
|
31
|
+
setFlung(false);
|
|
32
|
+
if (next)
|
|
33
|
+
openedAt.current = performance.now();
|
|
34
|
+
if (openProp === undefined)
|
|
35
|
+
setUncontrolled(next);
|
|
36
|
+
onOpenChange?.(next);
|
|
37
|
+
}, [openProp, onOpenChange]);
|
|
38
|
+
const fling = useCallback(() => { setOpen(false); setFlung(true); }, [setOpen]);
|
|
39
|
+
const value = useMemo(() => ({ open, flung, setOpen, fling, openedAt }), [open, flung, setOpen, fling]);
|
|
40
|
+
return _jsx(DrawerContext.Provider, { value: value, children: _jsx(DialogPrimitive.Root, { ...props, open: open, onOpenChange: setOpen }) });
|
|
41
|
+
}
|
|
42
|
+
export const DrawerTrigger = DialogPrimitive.Trigger;
|
|
43
|
+
export const DrawerClose = DialogPrimitive.Close;
|
|
44
|
+
const fade = { duration: motionTokens.duration.instant };
|
|
45
|
+
/* A click or Escape returns the panel quickly; a flick keeps its velocity in a spring of the same length. The shadow fades over the last stretch so it leaves with the panel instead of popping away at unmount. */
|
|
46
|
+
const shadowOut = { duration: motionTokens.duration.standard, times: [0, .65, 1], ease: "linear" };
|
|
47
|
+
const leave = { default: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.standard] }, opacity: shadowOut };
|
|
48
|
+
const flingOut = { default: { ...motionTokens.spring.smooth, visualDuration: motionTokens.duration.standard }, opacity: shadowOut };
|
|
49
|
+
/** When the title or description changes while open, the new copy rises in and the old copy leaves upward. */
|
|
50
|
+
function SwapText({ text }) {
|
|
51
|
+
const reduced = useReducedMotion();
|
|
52
|
+
return (_jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(motion.span, { className: styles.swap, initial: reduced ? false : { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } }, transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }, children: text }, text) }));
|
|
53
|
+
}
|
|
54
|
+
export function DrawerContent({ title, description, children, side = "right", container, className, onInteractOutside, onEscapeKeyDown, onOpenAutoFocus, onCloseAutoFocus, ...props }) {
|
|
55
|
+
const drawer = useContext(DrawerContext);
|
|
56
|
+
const reduced = useReducedMotion();
|
|
57
|
+
// Closing puts focus back on what opened the drawer, Trigger or not (lib/return-focus.ts).
|
|
58
|
+
const returnFocus = useReturnFocus(drawer?.open, onOpenAutoFocus, onCloseAutoFocus);
|
|
59
|
+
// Escape inside belongs to an open list, calendar or question first (lib/escape.ts); the next one closes the drawer.
|
|
60
|
+
const escape = { [ESCAPE_LAYER_ATTRIBUTE]: "", onEscapeKeyDown: layerEscape(onEscapeKeyDown), ...returnFocus };
|
|
61
|
+
const panelRef = useRef(null);
|
|
62
|
+
const offset = useMotionValue(0);
|
|
63
|
+
const pan = useRef(null);
|
|
64
|
+
const axis = side === "left" || side === "right" ? "x" : "y";
|
|
65
|
+
const sign = side === "right" || side === "bottom" ? 1 : -1;
|
|
66
|
+
const offscreen = { [axis]: `${sign * 100}%` };
|
|
67
|
+
const classes = [styles.content, className].filter(Boolean).join(" ");
|
|
68
|
+
const draggable = drawer !== null && !reduced;
|
|
69
|
+
const panelSize = () => (axis === "x" ? panelRef.current?.offsetWidth : panelRef.current?.offsetHeight) ?? 480;
|
|
70
|
+
// The header is the grab handle. It follows the pointer toward the edge and rubber-bands the other way.
|
|
71
|
+
function panStart(event) {
|
|
72
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
73
|
+
if (!draggable || target?.closest("button, a, input, select, textarea, [role='button']"))
|
|
74
|
+
return;
|
|
75
|
+
// The entrance animates in percent of the panel, so a grab mid-flight converts it to pixels.
|
|
76
|
+
const value = offset.get();
|
|
77
|
+
pan.current = typeof value === "number" ? value : parseFloat(value) / 100 * panelSize();
|
|
78
|
+
offset.stop();
|
|
79
|
+
}
|
|
80
|
+
function panMove(_, info) {
|
|
81
|
+
if (pan.current === null)
|
|
82
|
+
return;
|
|
83
|
+
const toward = (pan.current + info.offset[axis]) * sign;
|
|
84
|
+
offset.set(sign * (toward >= 0 ? toward : -Math.sqrt(-toward)));
|
|
85
|
+
}
|
|
86
|
+
// Past a third of the panel, or on a quick flick toward the edge, the drawer closes and keeps its velocity; otherwise it springs back.
|
|
87
|
+
function panEnd(_, info) {
|
|
88
|
+
if (pan.current === null)
|
|
89
|
+
return;
|
|
90
|
+
pan.current = null;
|
|
91
|
+
const toward = Number(offset.get()) * sign;
|
|
92
|
+
if (toward > panelSize() / 3 || (toward > 0 && info.velocity[axis] * sign > 500))
|
|
93
|
+
drawer?.fling();
|
|
94
|
+
else
|
|
95
|
+
animate(offset, 0, motionTokens.spring.snappy);
|
|
96
|
+
}
|
|
97
|
+
const inner = _jsxs(_Fragment, { children: [_jsxs(motion.div, { className: [styles.header, draggable ? styles.handle : ""].filter(Boolean).join(" "), onPanStart: panStart, onPan: panMove, onPanEnd: panEnd, children: [_jsxs("div", { children: [_jsx(DialogPrimitive.Title, { className: styles.title, children: _jsx(SwapText, { text: title }) }), description ? (_jsx(DialogPrimitive.Description, { className: styles.description, children: _jsx(SwapText, { text: description }) })) : null] }), _jsx(DialogPrimitive.Close, { className: styles.close, "aria-label": "Close drawer", children: _jsx(X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }), _jsx("div", { className: styles.body, children: children })] });
|
|
98
|
+
// Under a bare Radix root the open state is unknown here, so CSS keyframes keyed off data-state animate the layers instead.
|
|
99
|
+
if (drawer === null) {
|
|
100
|
+
return (_jsxs(DialogPrimitive.Portal, { container: container, children: [_jsx(DialogPrimitive.Overlay, { className: `${styles.overlay} ${styles.keyframes}`, "data-contained": container ? "" : undefined }), _jsx(DialogPrimitive.Content, { ...props, ...escape, onInteractOutside: onInteractOutside, "data-side": side, "data-contained": container ? "" : undefined, className: `${classes} ${styles.keyframes}`, children: inner })] }));
|
|
101
|
+
}
|
|
102
|
+
// The panel springs fully opaque from its own edge and returns to it faster than it arrived, from wherever it is.
|
|
103
|
+
return (_jsx(AnimatePresence, { custom: drawer.flung, children: drawer.open && (_jsxs(DialogPrimitive.Portal, { forceMount: true, container: container, children: [_jsx(DialogPrimitive.Overlay, { asChild: true, forceMount: true, children: _jsx(motion.div, { className: styles.overlay, "data-contained": container ? "" : undefined, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0, transition: reduced ? fade : { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } }, transition: reduced ? fade : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } }) }), _jsx(DialogPrimitive.Content, { ...props, ...escape, asChild: true, forceMount: true,
|
|
104
|
+
// Radix reads a click outside on click, so the press that reopens a closing drawer would dismiss it again.
|
|
105
|
+
onInteractOutside: event => {
|
|
106
|
+
onInteractOutside?.(event);
|
|
107
|
+
if (event.detail.originalEvent.timeStamp < drawer.openedAt.current)
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
}, children: _jsx(motion.div, { ref: panelRef, className: classes, "data-side": side, "data-contained": container ? "" : undefined, style: { [axis]: offset }, variants: { exit: (flung) => reduced ? { opacity: 0, transition: fade } : { ...offscreen, opacity: [1, 1, 0], transition: flung ? flingOut : leave } }, initial: reduced ? { opacity: 0 } : { ...offscreen, opacity: 1 }, animate: reduced ? { opacity: 1 } : { [axis]: 0, opacity: 1 }, exit: "exit", transition: reduced ? fade : motionTokens.spring.smooth, children: inner }) })] }, "drawer")) }));
|
|
110
|
+
}
|
|
111
|
+
export default Drawer;
|