@seekalfred/primitives 0.6.0 → 0.7.3
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/README.md +214 -15
- package/dist/activity-heatmap/activity-heatmap.d.ts +92 -0
- package/dist/activity-heatmap/activity-heatmap.js +699 -0
- package/dist/activity-heatmap/activity-heatmap.js.map +1 -0
- package/dist/activity-heatmap/index.d.ts +2 -0
- package/dist/activity-heatmap/index.js +2 -0
- package/dist/alert/alert.d.ts +53 -4
- package/dist/alert/alert.js +100 -11
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert/index.d.ts +2 -2
- package/dist/alert/index.js +2 -2
- package/dist/alert-dialog/alert-dialog.d.ts +58 -18
- package/dist/alert-dialog/alert-dialog.js +152 -9
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/alert-dialog/index.d.ts +2 -2
- package/dist/alert-dialog/index.js +2 -2
- package/dist/animated-counter/animated-counter.d.ts +38 -0
- package/dist/animated-counter/animated-counter.js +230 -0
- package/dist/animated-counter/animated-counter.js.map +1 -0
- package/dist/animated-counter/index.d.ts +2 -0
- package/dist/animated-counter/index.js +2 -0
- package/dist/announcement-bar/announcement-bar.d.ts +67 -0
- package/dist/announcement-bar/announcement-bar.js +364 -0
- package/dist/announcement-bar/announcement-bar.js.map +1 -0
- package/dist/announcement-bar/index.d.ts +2 -0
- package/dist/announcement-bar/index.js +2 -0
- package/dist/badge/badge.d.ts +1 -1
- package/dist/bar-chart/bar-chart.d.ts +51 -0
- package/dist/bar-chart/bar-chart.js +565 -0
- package/dist/bar-chart/bar-chart.js.map +1 -0
- package/dist/bar-chart/index.d.ts +2 -0
- package/dist/bar-chart/index.js +2 -0
- package/dist/breadcrumb/breadcrumb.d.ts +2 -2
- package/dist/breadcrumb/breadcrumb.js +12 -3
- package/dist/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/brush-chart/brush-chart.d.ts +64 -0
- package/dist/brush-chart/brush-chart.js +1056 -0
- package/dist/brush-chart/brush-chart.js.map +1 -0
- package/dist/brush-chart/index.d.ts +2 -0
- package/dist/brush-chart/index.js +2 -0
- package/dist/button/button.d.ts +1 -1
- package/dist/button/button.js +31 -21
- package/dist/button/button.js.map +1 -1
- package/dist/button-group/button-group.js +3 -4
- package/dist/button-group/button-group.js.map +1 -1
- package/dist/calendar/calendar.d.ts +1 -1
- package/dist/card/card.d.ts +22 -1
- package/dist/card/card.js +60 -7
- package/dist/card/card.js.map +1 -1
- package/dist/card-stack/card-stack.d.ts +54 -0
- package/dist/card-stack/card-stack.js +492 -0
- package/dist/card-stack/card-stack.js.map +1 -0
- package/dist/card-stack/index.d.ts +2 -0
- package/dist/card-stack/index.js +2 -0
- package/dist/carousel/carousel.d.ts +77 -0
- package/dist/carousel/carousel.js +415 -0
- package/dist/carousel/carousel.js.map +1 -0
- package/dist/carousel/index.d.ts +2 -0
- package/dist/carousel/index.js +2 -0
- package/dist/checkbox/checkbox.js +4 -4
- package/dist/checkbox/checkbox.js.map +1 -1
- package/dist/chip-group/chip-group.d.ts +37 -0
- package/dist/chip-group/chip-group.js +148 -0
- package/dist/chip-group/chip-group.js.map +1 -0
- package/dist/chip-group/index.d.ts +2 -0
- package/dist/chip-group/index.js +2 -0
- package/dist/copy-button/copy-button.d.ts +18 -2
- package/dist/copy-button/copy-button.js +29 -8
- package/dist/copy-button/copy-button.js.map +1 -1
- package/dist/countdown/countdown.d.ts +45 -0
- package/dist/countdown/countdown.js +298 -0
- package/dist/countdown/countdown.js.map +1 -0
- package/dist/countdown/index.d.ts +2 -0
- package/dist/countdown/index.js +2 -0
- package/dist/date-picker/picker-parts.js +1 -1
- package/dist/date-picker/picker-parts.js.map +1 -1
- package/dist/dialog/dialog.js +1 -1
- package/dist/dialog/dialog.js.map +1 -1
- package/dist/donut-chart/donut-chart.d.ts +86 -0
- package/dist/donut-chart/donut-chart.js +593 -0
- package/dist/donut-chart/donut-chart.js.map +1 -0
- package/dist/donut-chart/index.d.ts +2 -0
- package/dist/donut-chart/index.js +2 -0
- package/dist/empty-state/empty-state.d.ts +42 -10
- package/dist/empty-state/empty-state.js +44 -26
- package/dist/empty-state/empty-state.js.map +1 -1
- package/dist/empty-state/index.d.ts +2 -2
- package/dist/empty-state/index.js +2 -2
- package/dist/file-drop-zone/file-drop-zone.js +1 -1
- package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
- package/dist/filter-bar/filter-bar.d.ts +50 -26
- package/dist/filter-bar/filter-bar.js +317 -157
- package/dist/filter-bar/filter-bar.js.map +1 -1
- package/dist/gauge/gauge.d.ts +45 -0
- package/dist/gauge/gauge.js +219 -0
- package/dist/gauge/gauge.js.map +1 -0
- package/dist/gauge/index.d.ts +2 -0
- package/dist/gauge/index.js +2 -0
- package/dist/in-view-title/in-view-title.d.ts +33 -0
- package/dist/in-view-title/in-view-title.js +119 -0
- package/dist/in-view-title/in-view-title.js.map +1 -0
- package/dist/in-view-title/index.d.ts +2 -0
- package/dist/in-view-title/index.js +2 -0
- package/dist/index.d.ts +53 -10
- package/dist/index.js +52 -12
- package/dist/input/input.js +4 -4
- package/dist/input/input.js.map +1 -1
- package/dist/lib/cn.js +11 -1
- package/dist/lib/cn.js.map +1 -1
- package/dist/lib/field-message.js +28 -10
- package/dist/lib/field-message.js.map +1 -1
- package/dist/lib/field-shell.js +10 -3
- package/dist/lib/field-shell.js.map +1 -1
- package/dist/lib/field-surface.js +31 -9
- package/dist/lib/field-surface.js.map +1 -1
- package/dist/lib/flag-emoji.js +50 -0
- package/dist/lib/flag-emoji.js.map +1 -0
- package/dist/lib/flip.js +11 -3
- package/dist/lib/flip.js.map +1 -1
- package/dist/lib/menu-highlight.js +64 -0
- package/dist/lib/menu-highlight.js.map +1 -0
- package/dist/lib/motion.js +9 -2
- package/dist/lib/motion.js.map +1 -1
- package/dist/lib/rolling-value.js +1 -1
- package/dist/lib/rolling-value.js.map +1 -1
- package/dist/lib/spring-tween.js +500 -0
- package/dist/lib/spring-tween.js.map +1 -0
- package/dist/lib/swap-text.js +3 -1
- package/dist/lib/swap-text.js.map +1 -1
- package/dist/lib/use-copy-feedback.js +17 -3
- package/dist/lib/use-copy-feedback.js.map +1 -1
- package/dist/line-chart/index.d.ts +2 -0
- package/dist/line-chart/index.js +2 -0
- package/dist/line-chart/line-chart.d.ts +116 -0
- package/dist/line-chart/line-chart.js +740 -0
- package/dist/line-chart/line-chart.js.map +1 -0
- package/dist/mention-input/index.d.ts +2 -0
- package/dist/mention-input/index.js +2 -0
- package/dist/mention-input/mention-input.d.ts +98 -0
- package/dist/mention-input/mention-input.js +576 -0
- package/dist/mention-input/mention-input.js.map +1 -0
- package/dist/menu/menu.d.ts +2 -0
- package/dist/menu/menu.js +23 -12
- package/dist/menu/menu.js.map +1 -1
- package/dist/money-input/index.d.ts +2 -0
- package/dist/money-input/index.js +2 -0
- package/dist/money-input/money-input.d.ts +86 -0
- package/dist/money-input/money-input.js +613 -0
- package/dist/money-input/money-input.js.map +1 -0
- package/dist/morph-select/index.d.ts +2 -0
- package/dist/morph-select/index.js +2 -0
- package/dist/morph-select/morph-select.d.ts +89 -0
- package/dist/morph-select/morph-select.js +712 -0
- package/dist/morph-select/morph-select.js.map +1 -0
- package/dist/multi-select/index.d.ts +2 -0
- package/dist/multi-select/index.js +2 -0
- package/dist/multi-select/multi-select.d.ts +77 -0
- package/dist/multi-select/multi-select.js +209 -0
- package/dist/multi-select/multi-select.js.map +1 -0
- package/dist/number-field/index.d.ts +2 -0
- package/dist/number-field/index.js +2 -0
- package/dist/number-field/number-field.d.ts +42 -0
- package/dist/number-field/number-field.js +199 -0
- package/dist/number-field/number-field.js.map +1 -0
- package/dist/onboarding-checklist/index.d.ts +2 -0
- package/dist/onboarding-checklist/index.js +2 -0
- package/dist/onboarding-checklist/onboarding-checklist.d.ts +55 -0
- package/dist/onboarding-checklist/onboarding-checklist.js +577 -0
- package/dist/onboarding-checklist/onboarding-checklist.js.map +1 -0
- package/dist/password-field/password-field.d.ts +16 -0
- package/dist/password-field/password-field.js +39 -10
- package/dist/password-field/password-field.js.map +1 -1
- package/dist/phone-input/index.d.ts +3 -0
- package/dist/phone-input/index.js +3 -0
- package/dist/phone-input/phone-countries.d.ts +29 -0
- package/dist/phone-input/phone-countries.js +170 -0
- package/dist/phone-input/phone-countries.js.map +1 -0
- package/dist/phone-input/phone-input.d.ts +66 -0
- package/dist/phone-input/phone-input.js +440 -0
- package/dist/phone-input/phone-input.js.map +1 -0
- package/dist/popover/index.d.ts +2 -2
- package/dist/popover/index.js +2 -2
- package/dist/popover/popover.d.ts +51 -2
- package/dist/popover/popover.js +59 -8
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/progress.d.ts +16 -2
- package/dist/progress/progress.js +16 -9
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/index.d.ts +2 -2
- package/dist/radio-group/index.js +2 -2
- package/dist/radio-group/radio-group.d.ts +33 -3
- package/dist/radio-group/radio-group.js +110 -10
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/reactions/index.d.ts +2 -0
- package/dist/reactions/index.js +2 -0
- package/dist/reactions/reactions.d.ts +50 -0
- package/dist/reactions/reactions.js +315 -0
- package/dist/reactions/reactions.js.map +1 -0
- package/dist/rich-text-editor/index.d.ts +3 -0
- package/dist/rich-text-editor/index.js +2 -0
- package/dist/rich-text-editor/rich-text-editor.d.ts +70 -0
- package/dist/rich-text-editor/rich-text-editor.js +909 -0
- package/dist/rich-text-editor/rich-text-editor.js.map +1 -0
- package/dist/rich-text-editor/rich-text-engine.d.ts +16 -0
- package/dist/rich-text-editor/rich-text-engine.js +366 -0
- package/dist/rich-text-editor/rich-text-engine.js.map +1 -0
- package/dist/rich-text-editor/rich-text-html.js +174 -0
- package/dist/rich-text-editor/rich-text-html.js.map +1 -0
- package/dist/rich-text-editor/rich-text-icons.js +43 -0
- package/dist/rich-text-editor/rich-text-icons.js.map +1 -0
- package/dist/ridgeline/index.d.ts +2 -0
- package/dist/ridgeline/index.js +2 -0
- package/dist/ridgeline/ridgeline.d.ts +55 -0
- package/dist/ridgeline/ridgeline.js +701 -0
- package/dist/ridgeline/ridgeline.js.map +1 -0
- package/dist/scroll-area/index.d.ts +2 -2
- package/dist/scroll-area/index.js +2 -2
- package/dist/scroll-area/scroll-area.d.ts +68 -31
- package/dist/scroll-area/scroll-area.js +212 -62
- package/dist/scroll-area/scroll-area.js.map +1 -1
- package/dist/scrub-input/evaluate.d.ts +14 -0
- package/dist/scrub-input/evaluate.js +72 -0
- package/dist/scrub-input/evaluate.js.map +1 -0
- package/dist/scrub-input/index.d.ts +3 -0
- package/dist/scrub-input/index.js +3 -0
- package/dist/scrub-input/scrub-input.d.ts +74 -0
- package/dist/scrub-input/scrub-input.js +486 -0
- package/dist/scrub-input/scrub-input.js.map +1 -0
- package/dist/search-field/search-field.d.ts +24 -2
- package/dist/search-field/search-field.js +24 -9
- package/dist/search-field/search-field.js.map +1 -1
- package/dist/select/select.d.ts +3 -1
- package/dist/select/select.js +28 -14
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +22 -10
- package/dist/separator/separator.js +33 -11
- package/dist/separator/separator.js.map +1 -1
- package/dist/sheet/sheet.js +2 -1
- package/dist/sheet/sheet.js.map +1 -1
- package/dist/shortcut-recorder/index.d.ts +3 -0
- package/dist/shortcut-recorder/index.js +3 -0
- package/dist/shortcut-recorder/shortcut-model.d.ts +27 -0
- package/dist/shortcut-recorder/shortcut-model.js +206 -0
- package/dist/shortcut-recorder/shortcut-model.js.map +1 -0
- package/dist/shortcut-recorder/shortcut-recorder.d.ts +93 -0
- package/dist/shortcut-recorder/shortcut-recorder.js +489 -0
- package/dist/shortcut-recorder/shortcut-recorder.js.map +1 -0
- package/dist/slider/index.d.ts +2 -2
- package/dist/slider/slider.d.ts +46 -14
- package/dist/slider/slider.js +151 -67
- package/dist/slider/slider.js.map +1 -1
- package/dist/slope-chart/index.d.ts +2 -0
- package/dist/slope-chart/index.js +2 -0
- package/dist/slope-chart/slope-chart.d.ts +54 -0
- package/dist/slope-chart/slope-chart.js +568 -0
- package/dist/slope-chart/slope-chart.js.map +1 -0
- package/dist/sparkline/index.d.ts +2 -0
- package/dist/sparkline/index.js +2 -0
- package/dist/sparkline/sparkline.d.ts +46 -0
- package/dist/sparkline/sparkline.js +390 -0
- package/dist/sparkline/sparkline.js.map +1 -0
- package/dist/split-bar/index.d.ts +2 -0
- package/dist/split-bar/index.js +2 -0
- package/dist/split-bar/split-bar.d.ts +62 -0
- package/dist/split-bar/split-bar.js +513 -0
- package/dist/split-bar/split-bar.js.map +1 -0
- package/dist/split-button/split-button.d.ts +1 -1
- package/dist/split-button/split-button.js +12 -11
- package/dist/split-button/split-button.js.map +1 -1
- package/dist/stat-card/index.d.ts +2 -2
- package/dist/stat-card/index.js +2 -2
- package/dist/stat-card/stat-card.d.ts +58 -5
- package/dist/stat-card/stat-card.js +144 -17
- package/dist/stat-card/stat-card.js.map +1 -1
- package/dist/stepper/index.d.ts +2 -0
- package/dist/stepper/index.js +2 -0
- package/dist/stepper/stepper.d.ts +51 -0
- package/dist/stepper/stepper.js +215 -0
- package/dist/stepper/stepper.js.map +1 -0
- package/dist/streamgraph/index.d.ts +2 -0
- package/dist/streamgraph/index.js +2 -0
- package/dist/streamgraph/streamgraph.d.ts +70 -0
- package/dist/streamgraph/streamgraph.js +613 -0
- package/dist/streamgraph/streamgraph.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/swipe-actions/index.d.ts +2 -0
- package/dist/swipe-actions/index.js +2 -0
- package/dist/swipe-actions/swipe-actions.d.ts +67 -0
- package/dist/swipe-actions/swipe-actions.js +552 -0
- package/dist/swipe-actions/swipe-actions.js.map +1 -0
- package/dist/switch/switch.js +2 -2
- package/dist/switch/switch.js.map +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +2 -2
- package/dist/table/table.d.ts +55 -3
- package/dist/table/table.js +340 -165
- package/dist/table/table.js.map +1 -1
- package/dist/tabs/tabs.js +2 -2
- package/dist/tabs/tabs.js.map +1 -1
- package/dist/tag-input/index.d.ts +2 -0
- package/dist/tag-input/index.js +2 -0
- package/dist/tag-input/tag-input.d.ts +40 -0
- package/dist/tag-input/tag-input.js +269 -0
- package/dist/tag-input/tag-input.js.map +1 -0
- package/dist/text-morph/index.d.ts +2 -0
- package/dist/text-morph/index.js +2 -0
- package/dist/text-morph/text-morph.d.ts +23 -0
- package/dist/text-morph/text-morph.js +21 -0
- package/dist/text-morph/text-morph.js.map +1 -0
- package/dist/text-reveal/index.d.ts +2 -0
- package/dist/text-reveal/index.js +2 -0
- package/dist/text-reveal/text-reveal.d.ts +27 -0
- package/dist/text-reveal/text-reveal.js +51 -0
- package/dist/text-reveal/text-reveal.js.map +1 -0
- package/dist/text-shimmer/index.d.ts +2 -0
- package/dist/text-shimmer/index.js +2 -0
- package/dist/text-shimmer/text-shimmer.d.ts +29 -0
- package/dist/text-shimmer/text-shimmer.js +29 -0
- package/dist/text-shimmer/text-shimmer.js.map +1 -0
- package/dist/time-picker/time-picker.d.ts +36 -1
- package/dist/time-picker/time-picker.js +35 -7
- package/dist/time-picker/time-picker.js.map +1 -1
- package/dist/toast/index.d.ts +2 -2
- package/dist/toast/index.js +2 -2
- package/dist/toast/toast.d.ts +7 -1
- package/dist/toast/toast.js +82 -40
- package/dist/toast/toast.js.map +1 -1
- package/dist/tooltip/tooltip.d.ts +9 -2
- package/dist/tooltip/tooltip.js +7 -7
- package/dist/tooltip/tooltip.js.map +1 -1
- package/dist/treemap/index.d.ts +2 -0
- package/dist/treemap/index.js +2 -0
- package/dist/treemap/treemap.d.ts +53 -0
- package/dist/treemap/treemap.js +699 -0
- package/dist/treemap/treemap.js.map +1 -0
- package/dist/usage-meter/index.d.ts +2 -0
- package/dist/usage-meter/index.js +2 -0
- package/dist/usage-meter/usage-meter.d.ts +80 -0
- package/dist/usage-meter/usage-meter.js +458 -0
- package/dist/usage-meter/usage-meter.js.map +1 -0
- package/dist/waffle-chart/index.d.ts +2 -0
- package/dist/waffle-chart/index.js +2 -0
- package/dist/waffle-chart/waffle-chart.d.ts +57 -0
- package/dist/waffle-chart/waffle-chart.js +426 -0
- package/dist/waffle-chart/waffle-chart.js.map +1 -0
- package/package.json +165 -3
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { mergeRefs } from "../lib/merge-refs.js";
|
|
4
|
+
import { useMorphWidth } from "../lib/motion.js";
|
|
5
|
+
import { SwapText } from "../lib/swap-text.js";
|
|
6
|
+
import { useHeightFollow } from "../lib/height-follow.js";
|
|
7
|
+
import { forwardRef, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
//#region src/countdown/countdown.tsx
|
|
10
|
+
const DEFAULT_UNITS = [
|
|
11
|
+
"days",
|
|
12
|
+
"hours",
|
|
13
|
+
"minutes",
|
|
14
|
+
"seconds"
|
|
15
|
+
];
|
|
16
|
+
const SECONDS = {
|
|
17
|
+
days: 86400,
|
|
18
|
+
hours: 3600,
|
|
19
|
+
minutes: 60,
|
|
20
|
+
seconds: 1
|
|
21
|
+
};
|
|
22
|
+
/** How far each unit counts before carrying into the next, when a larger unit is shown. */
|
|
23
|
+
const LIMIT = {
|
|
24
|
+
days: Infinity,
|
|
25
|
+
hours: 24,
|
|
26
|
+
minutes: 60,
|
|
27
|
+
seconds: 60
|
|
28
|
+
};
|
|
29
|
+
/** The unit's name, its one-letter form, and its singular for speech. */
|
|
30
|
+
const NAMES = {
|
|
31
|
+
days: [
|
|
32
|
+
"Days",
|
|
33
|
+
"d",
|
|
34
|
+
"day"
|
|
35
|
+
],
|
|
36
|
+
hours: [
|
|
37
|
+
"Hours",
|
|
38
|
+
"h",
|
|
39
|
+
"hour"
|
|
40
|
+
],
|
|
41
|
+
minutes: [
|
|
42
|
+
"Minutes",
|
|
43
|
+
"m",
|
|
44
|
+
"minute"
|
|
45
|
+
],
|
|
46
|
+
seconds: [
|
|
47
|
+
"Seconds",
|
|
48
|
+
"s",
|
|
49
|
+
"second"
|
|
50
|
+
]
|
|
51
|
+
};
|
|
52
|
+
/** setTimeout cannot wait longer than this (2^31 - 1 ms, about 24.8 days); a longer wait re-arms itself. */
|
|
53
|
+
const MAX_TIMEOUT = 2147483647;
|
|
54
|
+
/** Wake a few ms after the second turns, so the clock has surely crossed it. */
|
|
55
|
+
const WAKE_MARGIN_MS = 8;
|
|
56
|
+
/** Stagger between digits on the first reading (ms), at most 300 in all. */
|
|
57
|
+
const REVEAL_STAGGER_MS = 35;
|
|
58
|
+
const REVEAL_MAX_DELAY_MS = 300;
|
|
59
|
+
/** How long the first reading takes at most: the longest delay, the smooth spring, and a margin. */
|
|
60
|
+
const REVEAL_SETTLE_MS = 1200;
|
|
61
|
+
const systemClock = () => Date.now();
|
|
62
|
+
/** Offset of a zone from UTC at an instant, in milliseconds. */
|
|
63
|
+
function zoneOffset(epoch, timeZone) {
|
|
64
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
65
|
+
timeZone,
|
|
66
|
+
hourCycle: "h23",
|
|
67
|
+
year: "numeric",
|
|
68
|
+
month: "2-digit",
|
|
69
|
+
day: "2-digit",
|
|
70
|
+
hour: "2-digit",
|
|
71
|
+
minute: "2-digit",
|
|
72
|
+
second: "2-digit"
|
|
73
|
+
}).formatToParts(new Date(epoch));
|
|
74
|
+
const get = (type) => Number(parts.find((part) => part.type === type)?.value ?? 0);
|
|
75
|
+
return Date.UTC(get("year"), get("month") - 1, get("day"), get("hour"), get("minute"), get("second")) - Math.floor(epoch / 1e3) * 1e3;
|
|
76
|
+
}
|
|
77
|
+
/** Resolves a countdown target to epoch milliseconds without ever reading the device's own time zone. */
|
|
78
|
+
function toEpoch(target, timeZone) {
|
|
79
|
+
if (typeof target === "number") return target;
|
|
80
|
+
if (target instanceof Date) return target.getTime();
|
|
81
|
+
const wall = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?)?$/.exec(target.trim());
|
|
82
|
+
if (!wall) return Date.parse(target);
|
|
83
|
+
const utc = Date.UTC(+wall[1], +wall[2] - 1, +wall[3], +(wall[4] ?? 0), +(wall[5] ?? 0), +(wall[6] ?? 0));
|
|
84
|
+
if (!timeZone) return utc;
|
|
85
|
+
return utc - zoneOffset(utc - zoneOffset(utc, timeZone), timeZone);
|
|
86
|
+
}
|
|
87
|
+
/** Whole seconds left, split across the units on show. */
|
|
88
|
+
function partsFor(seconds, units) {
|
|
89
|
+
return units.map((unit, index) => ({
|
|
90
|
+
unit,
|
|
91
|
+
value: index === 0 ? Math.floor(seconds / SECONDS[unit]) : Math.floor(seconds / SECONDS[unit]) % LIMIT[unit]
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
/** Wheel sizes per column: a tens column that only ever shows 0 to 5 turns on a six-face wheel, so 00 to 59 is a single step. */
|
|
95
|
+
function basesFor(unit, largest, length) {
|
|
96
|
+
return Array.from({ length }, (_, index) => {
|
|
97
|
+
return index === length - 2 && length === 2 && !largest ? unit === "hours" ? 3 : 6 : 10;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const subscribeVisibility = (notify) => {
|
|
101
|
+
document.addEventListener("visibilitychange", notify);
|
|
102
|
+
return () => document.removeEventListener("visibilitychange", notify);
|
|
103
|
+
};
|
|
104
|
+
const pageIsVisible = () => document.visibilityState === "visible";
|
|
105
|
+
const alwaysVisible = () => true;
|
|
106
|
+
/** Whether the node is on screen (at once where IntersectionObserver is missing). */
|
|
107
|
+
function useOnScreen(node) {
|
|
108
|
+
const [onScreen, setOnScreen] = useState(typeof IntersectionObserver === "undefined");
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const target = node.current;
|
|
111
|
+
if (!target || typeof IntersectionObserver === "undefined") return;
|
|
112
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
113
|
+
if (entry) setOnScreen(entry.isIntersecting);
|
|
114
|
+
});
|
|
115
|
+
observer.observe(target);
|
|
116
|
+
return () => observer.disconnect();
|
|
117
|
+
}, [node]);
|
|
118
|
+
return onScreen;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* One digit wheel. It remembers how far it has turned and takes the short way
|
|
122
|
+
* round to each new digit, so it always keeps going the way the time moved.
|
|
123
|
+
*/
|
|
124
|
+
function Wheel({ digit, base, delay, phase }) {
|
|
125
|
+
const [wheel, setWheel] = useState({
|
|
126
|
+
digit: 0,
|
|
127
|
+
turns: 0
|
|
128
|
+
});
|
|
129
|
+
let { turns } = wheel;
|
|
130
|
+
if (wheel.digit !== digit) {
|
|
131
|
+
const up = (digit - wheel.digit + base) % base;
|
|
132
|
+
const down = base - up;
|
|
133
|
+
turns = wheel.turns + (down <= up ? -down : up);
|
|
134
|
+
setWheel({
|
|
135
|
+
digit,
|
|
136
|
+
turns
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const style = {
|
|
140
|
+
"--wheel": turns,
|
|
141
|
+
"--base": base
|
|
142
|
+
};
|
|
143
|
+
if (phase === "revealing") {
|
|
144
|
+
style.transitionDelay = `${delay}ms`;
|
|
145
|
+
style.transitionDuration = "var(--duration-smooth)";
|
|
146
|
+
style.transitionTimingFunction = "var(--ease-smooth)";
|
|
147
|
+
}
|
|
148
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
149
|
+
style,
|
|
150
|
+
className: "countdown-wheel",
|
|
151
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
152
|
+
className: "counter-sizer",
|
|
153
|
+
children: "0"
|
|
154
|
+
}), Array.from({ length: base }, (_, face) => /* @__PURE__ */ jsx("span", {
|
|
155
|
+
style: { "--face": face },
|
|
156
|
+
className: "countdown-glyph",
|
|
157
|
+
children: face
|
|
158
|
+
}, face))]
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const Countdown = forwardRef(function Countdown({ target, timeZone, units = DEFAULT_UNITS, variant = "large", completeLabel = "Live now", onComplete, label = "Time remaining", clock = systemClock, className }, ref) {
|
|
162
|
+
const root = useRef(null);
|
|
163
|
+
const refs = useMemo(() => mergeRefs(ref, root), [ref]);
|
|
164
|
+
const end = toEpoch(target, timeZone);
|
|
165
|
+
const valid = Number.isFinite(end);
|
|
166
|
+
const shownUnits = units.length ? [...units].sort((a, b) => SECONDS[b] - SECONDS[a]) : DEFAULT_UNITS;
|
|
167
|
+
const pageVisible = useSyncExternalStore(subscribeVisibility, pageIsVisible, alwaysVisible);
|
|
168
|
+
const onScreen = useOnScreen(root);
|
|
169
|
+
const running = pageVisible && onScreen && valid;
|
|
170
|
+
const [now, setNow] = useState(null);
|
|
171
|
+
const clockRef = useRef(clock);
|
|
172
|
+
useLayoutEffect(() => {
|
|
173
|
+
clockRef.current = clock;
|
|
174
|
+
});
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!running) return;
|
|
177
|
+
let timer;
|
|
178
|
+
const tick = () => {
|
|
179
|
+
const time = clockRef.current();
|
|
180
|
+
setNow(time);
|
|
181
|
+
const left = end - time;
|
|
182
|
+
if (left > 0) timer = setTimeout(tick, (left % 1e3 || 1e3) + WAKE_MARGIN_MS);
|
|
183
|
+
};
|
|
184
|
+
timer = setTimeout(tick, 0);
|
|
185
|
+
return () => clearTimeout(timer);
|
|
186
|
+
}, [end, running]);
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!valid) return;
|
|
189
|
+
const left = end - clockRef.current();
|
|
190
|
+
if (left <= 0) return;
|
|
191
|
+
const timer = setTimeout(() => setNow(clockRef.current()), Math.min(MAX_TIMEOUT, left + WAKE_MARGIN_MS));
|
|
192
|
+
return () => clearTimeout(timer);
|
|
193
|
+
}, [end, valid]);
|
|
194
|
+
const left = now === null || !valid ? null : Math.max(0, end - now);
|
|
195
|
+
const complete = left === 0;
|
|
196
|
+
const pending = left === null;
|
|
197
|
+
const onCompleteRef = useRef(onComplete);
|
|
198
|
+
useLayoutEffect(() => {
|
|
199
|
+
onCompleteRef.current = onComplete;
|
|
200
|
+
});
|
|
201
|
+
const wasRunning = useRef(null);
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (left === null) return;
|
|
204
|
+
if (left > 0) {
|
|
205
|
+
wasRunning.current = end;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (wasRunning.current === end) {
|
|
209
|
+
wasRunning.current = null;
|
|
210
|
+
onCompleteRef.current?.();
|
|
211
|
+
}
|
|
212
|
+
}, [end, left]);
|
|
213
|
+
const [phase, setPhase] = useState("pending");
|
|
214
|
+
if (phase === "pending" && !pending) setPhase("revealing");
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
if (phase !== "revealing") return;
|
|
217
|
+
const timer = setTimeout(() => setPhase("settled"), REVEAL_SETTLE_MS);
|
|
218
|
+
return () => clearTimeout(timer);
|
|
219
|
+
}, [phase]);
|
|
220
|
+
const seconds = left === null ? 0 : Math.ceil(left / 1e3);
|
|
221
|
+
const parts = partsFor(seconds, shownUnits);
|
|
222
|
+
const compact = variant === "compact";
|
|
223
|
+
const frame = useRef(null);
|
|
224
|
+
const content = useRef(null);
|
|
225
|
+
const shown = complete ? "done" : "running";
|
|
226
|
+
useMorphWidth(content, shown);
|
|
227
|
+
useHeightFollow(frame, content, shown);
|
|
228
|
+
const completeText = typeof completeLabel === "string" ? completeLabel : "Complete";
|
|
229
|
+
const spoken = complete ? completeText : pending ? "" : parts.filter((part, index) => part.value > 0 || index === parts.length - 1).filter((part) => seconds < 60 || part.unit !== "seconds").map((part) => `${part.value} ${NAMES[part.unit][2]}${part.value === 1 ? "" : "s"}`).join(", ");
|
|
230
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
231
|
+
ref: refs,
|
|
232
|
+
"data-slot": "countdown",
|
|
233
|
+
"data-variant": variant,
|
|
234
|
+
"data-complete": complete ? "" : void 0,
|
|
235
|
+
"data-pending": pending ? "" : void 0,
|
|
236
|
+
className: cn("countdown", className),
|
|
237
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
238
|
+
ref: frame,
|
|
239
|
+
role: "timer",
|
|
240
|
+
"aria-label": label,
|
|
241
|
+
className: "countdown-frame height-follow",
|
|
242
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
243
|
+
className: "button-slot",
|
|
244
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
245
|
+
ref: content,
|
|
246
|
+
className: "countdown-content",
|
|
247
|
+
"aria-hidden": "true",
|
|
248
|
+
children: /* @__PURE__ */ jsx(SwapText, {
|
|
249
|
+
swapKey: shown,
|
|
250
|
+
motion: "glyph",
|
|
251
|
+
children: complete ? /* @__PURE__ */ jsxs("span", {
|
|
252
|
+
className: "countdown-done",
|
|
253
|
+
children: [/* @__PURE__ */ jsx("span", { className: "countdown-pulse" }), completeLabel]
|
|
254
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
255
|
+
className: "countdown-units",
|
|
256
|
+
children: parts.map((part, group) => {
|
|
257
|
+
const text = String(part.value).padStart(2, "0");
|
|
258
|
+
const bases = basesFor(part.unit, group === 0, text.length);
|
|
259
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
260
|
+
className: "countdown-unit",
|
|
261
|
+
children: [!compact && group > 0 ? /* @__PURE__ */ jsx("span", {
|
|
262
|
+
className: "countdown-colon",
|
|
263
|
+
children: ":"
|
|
264
|
+
}) : null, /* @__PURE__ */ jsxs("span", {
|
|
265
|
+
className: "countdown-group",
|
|
266
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
267
|
+
className: "countdown-number",
|
|
268
|
+
children: [...text].map((char, index) => /* @__PURE__ */ jsx(Wheel, {
|
|
269
|
+
digit: Number(char),
|
|
270
|
+
base: bases[index],
|
|
271
|
+
phase,
|
|
272
|
+
delay: Math.min((group * 2 + index) * REVEAL_STAGGER_MS, REVEAL_MAX_DELAY_MS)
|
|
273
|
+
}, `${part.unit}${text.length - index}`))
|
|
274
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
275
|
+
className: "countdown-name",
|
|
276
|
+
children: NAMES[part.unit][compact ? 1 : 0]
|
|
277
|
+
})]
|
|
278
|
+
})]
|
|
279
|
+
}, part.unit);
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
285
|
+
className: "sr-only",
|
|
286
|
+
children: spoken
|
|
287
|
+
})]
|
|
288
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
289
|
+
className: "sr-only",
|
|
290
|
+
role: "status",
|
|
291
|
+
children: complete ? completeText : ""
|
|
292
|
+
})]
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
//#endregion
|
|
296
|
+
export { Countdown };
|
|
297
|
+
|
|
298
|
+
//# sourceMappingURL=countdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"countdown.js","names":[],"sources":["../../src/countdown/countdown.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n forwardRef,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type CSSProperties,\n type ReactNode,\n} from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useHeightFollow } from \"../lib/height-follow.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { useMorphWidth } from \"../lib/motion.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\nexport type CountdownUnit = \"days\" | \"hours\" | \"minutes\" | \"seconds\";\n\n/**\n * Countdown: the time left until a fixed moment people wait for, such as a\n * launch, a live event or a deadline. Each digit is a wheel that takes the short\n * way round, so a tick turns downward and 00 to 59 is a single step; the first\n * reading rolls up from zero, one digit after another. When the moment passes\n * the digits give way to `completeLabel` and the box follows the new size.\n *\n * The clock runs only while the countdown is on screen and the tab is visible,\n * and reads the clock again on return, so it never drifts. Completion is armed\n * separately, so `onComplete` fires on time even while the ticking is paused.\n *\n * The digits are a picture. The timer is read as plain text (\"1 day, 2 hours,\n * 5 minutes\") that is not announced every second, and finishing is announced\n * once, politely. Reduced motion jumps.\n */\nexport interface CountdownProps {\n /**\n * The moment to count down to. A number is epoch milliseconds, a Date is used as is, and an ISO string with `Z` or an\n * offset such as `2026-10-06T10:00:00-07:00` is exact. A string without an offset is wall time in `timeZone`, or UTC when\n * no zone is given, so the result never depends on the visitor's own time zone.\n */\n target: Date | number | string;\n /** IANA zone for a target string without an offset, such as \"America/Los_Angeles\". Daylight saving is resolved for the target date. */\n timeZone?: string;\n /** Units to show, largest first. The largest unit absorbs the rest, so hours can pass 24 when days are left out. @default [\"days\", \"hours\", \"minutes\", \"seconds\"] */\n units?: CountdownUnit[];\n /** Large digits with unit names, or one compact line for banners and table cells. @default \"large\" */\n variant?: \"large\" | \"compact\";\n /** Shown once the target passes. The digits swap into it. @default \"Live now\" */\n completeLabel?: ReactNode;\n /** Fires once, the moment the countdown reaches zero while mounted. It fires on time even when the tab is hidden. */\n onComplete?: () => void;\n /** Accessible name, such as \"Keynote starts in\". @default \"Time remaining\" */\n label?: string;\n /** Clock source in epoch milliseconds. Pass a server-synced clock when the visitor's device time cannot be trusted. @default Date.now */\n clock?: () => number;\n /** Extra classes for the countdown. Merged last. */\n className?: string;\n}\n\nconst DEFAULT_UNITS: CountdownUnit[] = [\"days\", \"hours\", \"minutes\", \"seconds\"];\nconst SECONDS: Record<CountdownUnit, number> = {\n days: 86_400,\n hours: 3_600,\n minutes: 60,\n seconds: 1,\n};\n/** How far each unit counts before carrying into the next, when a larger unit is shown. */\nconst LIMIT: Record<CountdownUnit, number> = {\n days: Infinity,\n hours: 24,\n minutes: 60,\n seconds: 60,\n};\n/** The unit's name, its one-letter form, and its singular for speech. */\nconst NAMES: Record<CountdownUnit, [string, string, string]> = {\n days: [\"Days\", \"d\", \"day\"],\n hours: [\"Hours\", \"h\", \"hour\"],\n minutes: [\"Minutes\", \"m\", \"minute\"],\n seconds: [\"Seconds\", \"s\", \"second\"],\n};\n/** setTimeout cannot wait longer than this (2^31 - 1 ms, about 24.8 days); a longer wait re-arms itself. */\nconst MAX_TIMEOUT = 2_147_483_647;\n/** Wake a few ms after the second turns, so the clock has surely crossed it. */\nconst WAKE_MARGIN_MS = 8;\n/** Stagger between digits on the first reading (ms), at most 300 in all. */\nconst REVEAL_STAGGER_MS = 35;\nconst REVEAL_MAX_DELAY_MS = 300;\n/** How long the first reading takes at most: the longest delay, the smooth spring, and a margin. */\nconst REVEAL_SETTLE_MS = 1200;\n\nconst systemClock = () => Date.now();\n\n/** Offset of a zone from UTC at an instant, in milliseconds. */\nfunction zoneOffset(epoch: number, timeZone: string) {\n const parts = new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n hourCycle: \"h23\",\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n }).formatToParts(new Date(epoch));\n const get = (type: string) => Number(parts.find((part) => part.type === type)?.value ?? 0);\n return (\n Date.UTC(get(\"year\"), get(\"month\") - 1, get(\"day\"), get(\"hour\"), get(\"minute\"), get(\"second\")) -\n Math.floor(epoch / 1000) * 1000\n );\n}\n\n/** Resolves a countdown target to epoch milliseconds without ever reading the device's own time zone. */\nfunction toEpoch(target: Date | number | string, timeZone?: string) {\n if (typeof target === \"number\") return target;\n if (target instanceof Date) return target.getTime();\n const wall = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?)?$/.exec(\n target.trim(),\n );\n if (!wall) return Date.parse(target);\n const utc = Date.UTC(\n +wall[1]!,\n +wall[2]! - 1,\n +wall[3]!,\n +(wall[4] ?? 0),\n +(wall[5] ?? 0),\n +(wall[6] ?? 0),\n );\n if (!timeZone) return utc;\n // Two passes settle the offset across a daylight saving change.\n const guess = utc - zoneOffset(utc, timeZone);\n return utc - zoneOffset(guess, timeZone);\n}\n\n/** Whole seconds left, split across the units on show. */\nfunction partsFor(seconds: number, units: CountdownUnit[]) {\n return units.map((unit, index) => ({\n unit,\n value:\n index === 0\n ? Math.floor(seconds / SECONDS[unit])\n : Math.floor(seconds / SECONDS[unit]) % LIMIT[unit],\n }));\n}\n\n/** Wheel sizes per column: a tens column that only ever shows 0 to 5 turns on a six-face wheel, so 00 to 59 is a single step. */\nfunction basesFor(unit: CountdownUnit, largest: boolean, length: number) {\n return Array.from({ length }, (_, index) => {\n const tens = index === length - 2 && length === 2 && !largest;\n return tens ? (unit === \"hours\" ? 3 : 6) : 10;\n });\n}\n\nconst subscribeVisibility = (notify: () => void) => {\n document.addEventListener(\"visibilitychange\", notify);\n return () => document.removeEventListener(\"visibilitychange\", notify);\n};\nconst pageIsVisible = () => document.visibilityState === \"visible\";\nconst alwaysVisible = () => true;\n\n/** Whether the node is on screen (at once where IntersectionObserver is missing). */\nfunction useOnScreen(node: { current: HTMLElement | null }) {\n const [onScreen, setOnScreen] = useState(typeof IntersectionObserver === \"undefined\");\n useEffect(() => {\n const target = node.current;\n if (!target || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver(([entry]) => {\n if (entry) setOnScreen(entry.isIntersecting);\n });\n observer.observe(target);\n return () => observer.disconnect();\n }, [node]);\n return onScreen;\n}\n\ntype Phase = \"pending\" | \"revealing\" | \"settled\";\n\n/**\n * One digit wheel. It remembers how far it has turned and takes the short way\n * round to each new digit, so it always keeps going the way the time moved.\n */\nfunction Wheel({\n digit,\n base,\n delay,\n phase,\n}: {\n digit: number;\n base: number;\n delay: number;\n phase: Phase;\n}) {\n const [wheel, setWheel] = useState({ digit: 0, turns: 0 });\n let { turns } = wheel;\n if (wheel.digit !== digit) {\n const up = (digit - wheel.digit + base) % base;\n const down = base - up;\n turns = wheel.turns + (down <= up ? -down : up);\n setWheel({ digit, turns });\n }\n const style: CSSProperties & Record<string, string | number> = {\n \"--wheel\": turns,\n \"--base\": base,\n };\n // The first reading rolls on the smooth spring, each digit a beat after the last; every tick after that is snappy.\n if (phase === \"revealing\") {\n style.transitionDelay = `${delay}ms`;\n style.transitionDuration = \"var(--duration-smooth)\";\n style.transitionTimingFunction = \"var(--ease-smooth)\";\n }\n return (\n <span style={style} className=\"countdown-wheel\">\n <span className=\"counter-sizer\">0</span>\n {Array.from({ length: base }, (_, face) => (\n <span key={face} style={{ \"--face\": face } as CSSProperties} className=\"countdown-glyph\">\n {face}\n </span>\n ))}\n </span>\n );\n}\n\nexport const Countdown = forwardRef<HTMLDivElement, CountdownProps>(function Countdown(\n {\n target,\n timeZone,\n units = DEFAULT_UNITS,\n variant = \"large\",\n completeLabel = \"Live now\",\n onComplete,\n label = \"Time remaining\",\n clock = systemClock,\n className,\n },\n ref,\n) {\n const root = useRef<HTMLDivElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, root), [ref]);\n const end = toEpoch(target, timeZone);\n const valid = Number.isFinite(end);\n const shownUnits = units.length\n ? [...units].sort((a, b) => SECONDS[b] - SECONDS[a])\n : DEFAULT_UNITS;\n\n // The clock only ticks while the countdown is on screen and the tab is visible; coming back reads the clock again, so nothing drifts.\n const pageVisible = useSyncExternalStore(subscribeVisibility, pageIsVisible, alwaysVisible);\n const onScreen = useOnScreen(root);\n const running = pageVisible && onScreen && valid;\n const [now, setNow] = useState<number | null>(null);\n const clockRef = useRef(clock);\n useLayoutEffect(() => {\n clockRef.current = clock;\n });\n useEffect(() => {\n if (!running) return;\n let timer: ReturnType<typeof setTimeout>;\n const tick = () => {\n const time = clockRef.current();\n setNow(time);\n const left = end - time;\n // Wake just after the next whole second, aligned to the target rather than to when the page loaded.\n if (left > 0) timer = setTimeout(tick, (left % 1000 || 1000) + WAKE_MARGIN_MS);\n };\n timer = setTimeout(tick, 0);\n return () => clearTimeout(timer);\n }, [end, running]);\n\n // Completion is armed separately, so it lands on time even while ticking is paused.\n useEffect(() => {\n if (!valid) return;\n const left = end - clockRef.current();\n if (left <= 0) return;\n const timer = setTimeout(\n () => setNow(clockRef.current()),\n Math.min(MAX_TIMEOUT, left + WAKE_MARGIN_MS),\n );\n return () => clearTimeout(timer);\n }, [end, valid]);\n\n const left = now === null || !valid ? null : Math.max(0, end - now);\n const complete = left === 0;\n const pending = left === null;\n const onCompleteRef = useRef(onComplete);\n useLayoutEffect(() => {\n onCompleteRef.current = onComplete;\n });\n const wasRunning = useRef<number | null>(null);\n useEffect(() => {\n if (left === null) return;\n if (left > 0) {\n wasRunning.current = end;\n return;\n }\n if (wasRunning.current === end) {\n wasRunning.current = null;\n onCompleteRef.current?.();\n }\n }, [end, left]);\n\n const [phase, setPhase] = useState<Phase>(\"pending\");\n if (phase === \"pending\" && !pending) setPhase(\"revealing\");\n useEffect(() => {\n if (phase !== \"revealing\") return;\n const timer = setTimeout(() => setPhase(\"settled\"), REVEAL_SETTLE_MS);\n return () => clearTimeout(timer);\n }, [phase]);\n\n const seconds = left === null ? 0 : Math.ceil(left / 1000);\n const parts = partsFor(seconds, shownUnits);\n const compact = variant === \"compact\";\n\n // The box follows the size of whatever it holds, so the digits can swap into the finished label without a jump.\n const frame = useRef<HTMLDivElement>(null);\n const content = useRef<HTMLDivElement>(null);\n const shown = complete ? \"done\" : \"running\";\n useMorphWidth(content, shown);\n useHeightFollow(frame, content, shown);\n\n const completeText = typeof completeLabel === \"string\" ? completeLabel : \"Complete\";\n const spoken = complete\n ? completeText\n : pending\n ? \"\"\n : parts\n .filter((part, index) => part.value > 0 || index === parts.length - 1)\n .filter((part) => seconds < 60 || part.unit !== \"seconds\")\n .map((part) => `${part.value} ${NAMES[part.unit][2]}${part.value === 1 ? \"\" : \"s\"}`)\n .join(\", \");\n\n return (\n <div\n ref={refs}\n data-slot=\"countdown\"\n data-variant={variant}\n data-complete={complete ? \"\" : undefined}\n data-pending={pending ? \"\" : undefined}\n className={cn(\"countdown\", className)}\n >\n <div ref={frame} role=\"timer\" aria-label={label} className=\"countdown-frame height-follow\">\n <div className=\"button-slot\">\n <div ref={content} className=\"countdown-content\" aria-hidden=\"true\">\n <SwapText swapKey={shown} motion=\"glyph\">\n {complete ? (\n <span className=\"countdown-done\">\n <span className=\"countdown-pulse\" />\n {completeLabel}\n </span>\n ) : (\n <span className=\"countdown-units\">\n {parts.map((part, group) => {\n const text = String(part.value).padStart(2, \"0\");\n const bases = basesFor(part.unit, group === 0, text.length);\n return (\n <span key={part.unit} className=\"countdown-unit\">\n {!compact && group > 0 ? <span className=\"countdown-colon\">:</span> : null}\n <span className=\"countdown-group\">\n <span className=\"countdown-number\">\n {[...text].map((char, index) => (\n <Wheel\n key={`${part.unit}${text.length - index}`}\n digit={Number(char)}\n base={bases[index]!}\n phase={phase}\n delay={Math.min(\n (group * 2 + index) * REVEAL_STAGGER_MS,\n REVEAL_MAX_DELAY_MS,\n )}\n />\n ))}\n </span>\n <span className=\"countdown-name\">\n {NAMES[part.unit][compact ? 1 : 0]}\n </span>\n </span>\n </span>\n );\n })}\n </span>\n )}\n </SwapText>\n </div>\n </div>\n <span className=\"sr-only\">{spoken}</span>\n </div>\n {/* Ticking is never announced; finishing is, once. */}\n <span className=\"sr-only\" role=\"status\">\n {complete ? completeText : \"\"}\n </span>\n </div>\n );\n});\n"],"mappings":"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"}
|
|
@@ -11,7 +11,7 @@ const datePickerLabelVariants = cva(["text-sm leading-(--leading-normal) text-fo
|
|
|
11
11
|
const datePickerTriggerVariants = cva([
|
|
12
12
|
"group-data-[invalid]:border-destructive-strong",
|
|
13
13
|
fieldSurfaceVariants(),
|
|
14
|
-
"[&:where([data-pressed])]:border-
|
|
14
|
+
"[&:where([data-pressed])]:border-border-strong",
|
|
15
15
|
"data-[hovered]:not-data-[disabled]:bg-muted data-[pressed]:bg-muted",
|
|
16
16
|
"group/trigger",
|
|
17
17
|
"flex items-center justify-between gap-2 text-start",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"picker-parts.js","names":["AriaDateField","AriaLabel","AriaDateInput","AriaDateSegment"],"sources":["../../src/date-picker/picker-parts.tsx"],"sourcesContent":["\"use client\";\n\nimport type { DateValue } from \"@internationalized/date\";\nimport { cva } from \"class-variance-authority\";\nimport { useMemo, useState, type ReactNode } from \"react\";\nimport {\n DateField as AriaDateField,\n DateInput as AriaDateInput,\n DateSegment as AriaDateSegment,\n Label as AriaLabel,\n useLocale,\n} from \"react-aria-components\";\n\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\n\n/*\n * The pieces DatePicker and DateRangePicker share: the field's classes, the\n * glyphs, the typed date field, and the value helpers. Neither file imports the\n * other, so the two pickers can each own their motion.\n */\n\nexport const datePickerVariants = cva([\"group flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\nexport const datePickerLabelVariants = cva([\"text-sm leading-(--leading-normal) text-foreground\"]);\n\n/** The trigger: the field surface Input, Textarea and Select share. */\nexport const datePickerTriggerVariants = cva(\n [\n \"group-data-[invalid]:border-destructive-strong\",\n fieldSurfaceVariants(),\n // While the list is open the trigger is pressed and focus is inside it, so the\n // border and fill keep what they had. Ordinary variants: the base background is\n // a plain class, which a zero-specificity rule would lose to.\n \"[&:where([data-pressed])]:border-
|
|
1
|
+
{"version":3,"file":"picker-parts.js","names":["AriaDateField","AriaLabel","AriaDateInput","AriaDateSegment"],"sources":["../../src/date-picker/picker-parts.tsx"],"sourcesContent":["\"use client\";\n\nimport type { DateValue } from \"@internationalized/date\";\nimport { cva } from \"class-variance-authority\";\nimport { useMemo, useState, type ReactNode } from \"react\";\nimport {\n DateField as AriaDateField,\n DateInput as AriaDateInput,\n DateSegment as AriaDateSegment,\n Label as AriaLabel,\n useLocale,\n} from \"react-aria-components\";\n\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\n\n/*\n * The pieces DatePicker and DateRangePicker share: the field's classes, the\n * glyphs, the typed date field, and the value helpers. Neither file imports the\n * other, so the two pickers can each own their motion.\n */\n\nexport const datePickerVariants = cva([\"group flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\nexport const datePickerLabelVariants = cva([\"text-sm leading-(--leading-normal) text-foreground\"]);\n\n/** The trigger: the field surface Input, Textarea and Select share. */\nexport const datePickerTriggerVariants = cva(\n [\n \"group-data-[invalid]:border-destructive-strong\",\n fieldSurfaceVariants(),\n // While the list is open the trigger is pressed and focus is inside it, so the\n // border and fill keep what they had. Ordinary variants: the base background is\n // a plain class, which a zero-specificity rule would lose to.\n \"[&:where([data-pressed])]:border-border-strong\",\n \"data-[hovered]:not-data-[disabled]:bg-muted data-[pressed]:bg-muted\",\n \"group/trigger\",\n \"flex items-center justify-between gap-2 text-start\",\n \"text-base leading-(--leading-normal) cursor-pointer\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n ],\n {\n variants: {\n /** The same rungs and tokens as Input, Select and Button. */\n size: {\n sm: \"h-(--density-control-h-sm) px-(--density-control-pad-sm)\",\n md: \"h-(--density-control-h-md) px-(--density-control-pad-md)\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n/** The typed date's box, inside the popover: the field surface at the small rung. */\nexport const datePickerInputVariants = cva([\n fieldSurfaceVariants(),\n \"flex items-center\",\n \"h-(--density-control-h-sm) px-(--density-control-pad-sm)\",\n \"text-base leading-(--leading-normal)\",\n]);\n\n/** One part (day, month, year) of the typed date. */\nexport const datePickerSegmentVariants = cva([\n \"rounded-2xs px-0.5 tabular-nums outline-none\",\n \"data-[placeholder]:text-muted-foreground\",\n \"data-[focused]:bg-primary data-[focused]:text-primary-foreground\",\n \"data-[type=literal]:px-0\",\n]);\n\nexport const descriptionClass = \"text-sm leading-(--leading-normal) text-muted-foreground\";\n// The open list covers the hint; fading it keeps its first letters from showing past the list's corner.\nexport const hintClass =\n \"motion-safe:transition-opacity motion-safe:duration-(--duration-quick) motion-safe:ease-settle group-has-[[aria-expanded=true]]:opacity-0\";\nexport const errorClass = \"text-sm leading-(--leading-normal) text-destructive-strong\";\n\nexport function CalendarGlyph() {\n return (\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"text-muted-foreground\"\n >\n <rect x=\"2.5\" y=\"3.5\" width=\"11\" height=\"10\" rx=\"1.5\" />\n <path d=\"M2.5 6.5h11M5.5 2v3M10.5 2v3\" />\n </svg>\n );\n}\n\n/** \"15 Sept 2026\" in the locale React Aria reports. A calendar day has no zone, so UTC is exact. */\n/** The chevron turns half a turn while the list is open, on the gentle spring. */\nexport function Chevron() {\n return (\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"text-muted-foreground motion-safe:transition-[rotate] motion-safe:duration-(--duration-gentle) motion-safe:ease-gentle group-aria-expanded/trigger:rotate-180\"\n >\n <path d=\"m4 6 4 4 4-4\" />\n </svg>\n );\n}\n\nexport const pickerFooterButtonVariants = cva([\n \"picker-footer-button cursor-pointer rounded-md border-0 bg-transparent px-2 py-1\",\n \"text-xs text-muted-foreground outline-none\",\n \"data-[hovered]:bg-muted data-[hovered]:text-foreground\",\n \"data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring)\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n]);\n\nexport function useDayFormat(): (value: DateValue) => string {\n const { locale } = useLocale();\n return useMemo(() => {\n const format = new Intl.DateTimeFormat(locale, { dateStyle: \"medium\", timeZone: \"UTC\" });\n return (value: DateValue) => format.format(value.toDate(\"UTC\"));\n }, [locale]);\n}\n\n/** A value that may be controlled or not, without a dependency on react-stately. */\nexport function useValue<T>(value: T | undefined, defaultValue: T, onChange?: (next: T) => void) {\n const [inner, setInner] = useState<T>(defaultValue);\n const current = value === undefined ? inner : value;\n const set = (next: T) => {\n if (value === undefined) setInner(next);\n onChange?.(next);\n };\n return [current, set] as const;\n}\n\n/** The typed date inside the popover. Reports only whole dates, or null once every part is cleared. */\nexport function TypedDate({\n label,\n value,\n onChange,\n minValue,\n maxValue,\n}: {\n label: string;\n value: DateValue | null;\n onChange: (value: DateValue | null) => void;\n minValue?: DateValue;\n maxValue?: DateValue;\n}) {\n return (\n <AriaDateField\n value={value}\n onChange={onChange}\n minValue={minValue}\n maxValue={maxValue}\n data-slot=\"date-picker-typed\"\n className=\"flex min-w-0 flex-1 flex-col gap-1\"\n >\n <AriaLabel className={descriptionClass}>{label}</AriaLabel>\n <AriaDateInput data-slot=\"date-picker-input\" className={datePickerInputVariants()}>\n {(segment) => <AriaDateSegment segment={segment} className={datePickerSegmentVariants()} />}\n </AriaDateInput>\n </AriaDateField>\n );\n}\n\nexport interface PickerSharedProps {\n /**\n * Accessible name, shown above the trigger. **Required**, as on Select: the\n * trigger's name is this label followed by the value it shows.\n */\n label: string;\n /** Hide the label visually while keeping it on the accessibility tree. @default false */\n labelHidden?: boolean;\n /** A quieter line under the trigger, read with it. */\n description?: ReactNode;\n /** Why the value is wrong, under the trigger. Draws the invalid edge. */\n errorMessage?: ReactNode;\n /** Shown on the trigger while nothing is chosen. @default \"Choose a date\" */\n placeholder?: string;\n /** The earliest day that may be chosen, typed or picked. */\n minValue?: DateValue;\n /** The latest day that may be chosen, typed or picked. */\n maxValue?: DateValue;\n /** Offer the Calendar's Clear button, for a value that may be left empty. @default false */\n clearable?: boolean;\n /** Visible text of the Clear button. @default \"Clear\" */\n clearLabel?: string;\n /** Disable the trigger. */\n isDisabled?: boolean;\n /** Height rung, shared with Input and Select. @default \"md\" */\n size?: \"sm\" | \"md\" | \"lg\";\n /** How a chosen day is written on the trigger. @default the locale's medium date */\n formatValue?: (value: DateValue) => string;\n /** Extra classes on the root. Merged last through `cn()`, so the consumer wins. */\n className?: string;\n}\n\n"],"mappings":";;;;;;;AAqBA,MAAa,qBAAqB,IAAI,CAAC,yDAAyD,CAAC;AAEjG,MAAa,0BAA0B,IAAI,CAAC,oDAAoD,CAAC;;AAGjG,MAAa,4BAA4B,IACvC;CACE;CACA,qBAAqB;CAIrB;CACA;CACA;CACA;CACA;CACA;CACA;AACF,GACA;CACE,UAAU;;AAER,MAAM;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;CACN,EACF;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;;AAGA,MAAa,0BAA0B,IAAI;CACzC,qBAAqB;CACrB;CACA;CACA;AACF,CAAC;;AAGD,MAAa,4BAA4B,IAAI;CAC3C;CACA;CACA;CACA;AACF,CAAC;AAED,MAAa,mBAAmB;AAEhC,MAAa,YACX;AACF,MAAa,aAAa;AAE1B,SAAgB,gBAAgB;CAC9B,OACE,qBAAC,OAAD;EACE,eAAY;EACZ,SAAQ;EACR,MAAK;EACL,QAAO;EACP,aAAY;EACZ,eAAc;EACd,gBAAe;EACf,WAAU;EARZ,UAAA,CAUE,oBAAC,QAAD;GAAM,GAAE;GAAM,GAAE;GAAM,OAAM;GAAK,QAAO;GAAK,IAAG;EAAO,CAAA,GACvD,oBAAC,QAAD,EAAM,GAAE,+BAAgC,CAAA,CACrC;;AAET;;;AAIA,SAAgB,UAAU;CACxB,OACE,oBAAC,OAAD;EACE,eAAY;EACZ,SAAQ;EACR,MAAK;EACL,QAAO;EACP,aAAY;EACZ,eAAc;EACd,gBAAe;EACf,WAAU;EAEV,UAAA,oBAAC,QAAD,EAAM,GAAE,eAAgB,CAAA;CACrB,CAAA;AAET;AAEA,MAAa,6BAA6B,IAAI;CAC5C;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,SAAgB,eAA6C;CAC3D,MAAM,EAAE,WAAW,UAAU;CAC7B,OAAO,cAAc;EACnB,MAAM,SAAS,IAAI,KAAK,eAAe,QAAQ;GAAE,WAAW;GAAU,UAAU;EAAM,CAAC;EACvF,QAAQ,UAAqB,OAAO,OAAO,MAAM,OAAO,KAAK,CAAC;CAChE,GAAG,CAAC,MAAM,CAAC;AACb;;AAGA,SAAgB,SAAY,OAAsB,cAAiB,UAA8B;CAC/F,MAAM,CAAC,OAAO,YAAY,SAAY,YAAY;CAClD,MAAM,UAAU,UAAU,KAAA,IAAY,QAAQ;CAC9C,MAAM,OAAO,SAAY;EACvB,IAAI,UAAU,KAAA,GAAW,SAAS,IAAI;EACtC,WAAW,IAAI;CACjB;CACA,OAAO,CAAC,SAAS,GAAG;AACtB;;AAGA,SAAgB,UAAU,EACxB,OACA,OACA,UACA,UACA,YAOC;CACD,OACE,qBAACA,WAAD;EACS;EACG;EACA;EACA;EACV,aAAU;EACV,WAAU;EANZ,UAAA,CAQE,oBAACC,OAAD;GAAW,WAAW;GAAmB,UAAA;EAAiB,CAAA,GAC1D,oBAACC,WAAD;GAAe,aAAU;GAAoB,WAAW,wBAAwB;GAC5E,WAAA,YAAY,oBAACC,aAAD;IAA0B;IAAS,WAAW,0BAA0B;GAAI,CAAA;EAC7E,CAAA,CACF;;AAEnB"}
|
package/dist/dialog/dialog.js
CHANGED
|
@@ -46,7 +46,7 @@ const dialogOverlayVariants = cva([
|
|
|
46
46
|
]);
|
|
47
47
|
const dialogPanelVariants = cva([
|
|
48
48
|
"relative w-full max-w-128",
|
|
49
|
-
"rounded-
|
|
49
|
+
"rounded-panel border-(length:--border-width) border-border p-6",
|
|
50
50
|
"bg-card text-card-foreground shadow-modal",
|
|
51
51
|
"flex flex-col gap-4",
|
|
52
52
|
"box-border font-sans",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialog.js","names":["Dialog","AriaModalOverlay","AriaModal","AriaDialog","AriaHeading","AriaText","AriaButton"],"sources":["../../src/dialog/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Dialog as AriaDialog,\n Heading as AriaHeading,\n Modal as AriaModal,\n ModalOverlay as AriaModalOverlay,\n Text as AriaText,\n type ModalOverlayProps as AriaModalOverlayProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Dialog, ported from the legacy shadcn/Radix component onto React Aria's\n * Modal primitives.\n *\n * FOUR LEGACY PARTS COLLAPSE INTO TWO PROPS. The census (9 importing files,\n * but only **7** that render anything -- header.tsx and PollQuizCreator.tsx\n * import the parts and use none) reads: DialogContent 9, DialogTitle 8,\n * DialogHeader 7, DialogDescription 2. Those four always appear in the same\n * shape, so `title` and `description` are props of the dialog itself.\n *\n * That is not tidying. `title` is REQUIRED, which makes an unnamed dialog\n * impossible to write -- and the legacy shipped one: LiveQAOverlay.tsx\n * renders a DialogContent with no DialogTitle, so its `aria-labelledby`\n * points at an id that matches no element and the dialog is announced with\n * no name.\n *\n * It also fixes the dangling-description defect for free. Radix set\n * `aria-describedby` unconditionally from a `useId`, so seven of the nine\n * legacy dialogs referenced an id that did not exist. React Aria wires it\n * only when a description slot is actually rendered.\n *\n * `DialogPortal` and `DialogOverlay` are not ported: zero external call\n * sites, and React Aria's ModalOverlay does both jobs.\n *\n * NOT PORTED, and deliberately: the enter/exit animation. The legacy leans\n * on tailwindcss-animate's `animate-in` / `zoom-in-95` utilities, which this\n * repo does not ship, and the motion tokens are a separate task. React Aria\n * exposes `data-entering` / `data-exiting` for whoever adds them.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const dialogOverlayVariants = cva([\n // Centring by flex, not by `top-1/2 left-1/2 -translate-1/2`. The legacy's\n // translate pair is a bracket value that check:hardcoding rejects, and\n // flex centring needs no transform to undo.\n \"fixed inset-0 z-50 flex items-center justify-center\",\n // The inset that keeps a full-width panel off the viewport edge. The\n // legacy did this with max-w-[calc(100%-2rem)] -- also a bracket value.\n \"p-4\",\n \"bg-scrim\",\n]);\n\nexport const dialogPanelVariants = cva([\n // max-w-128 (32rem), NOT max-w-lg. `--container-lg` does not exist in this\n // repo -- the container scale holds only page-shell widths (max 75rem,\n // wide 90rem) -- so `max-w-lg` compiles to NOTHING and the panel spans the\n // viewport. It emitted no rule at all, tsc and ESLint were both blind to\n // it, and a screenshot is what caught it. 128 spacing units is 32rem,\n // exactly the legacy sm:max-w-lg.\n \"relative w-full max-w-128\",\n \"rounded-xl border-(length:--border-width) border-border p-6\",\n // bg-card, not bg-background: a dialog is a raised surface, and card is\n // the role that already steps up the ink ramp on dark, where a shadow made\n // of ink is invisible. Its contrast pairs are in the contract already.\n \"bg-card text-card-foreground shadow-modal\",\n \"flex flex-col gap-4\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none:\n // content-box would add the 24px padding to max-w-lg, and an unstyled\n // document renders text in a serif. The Accordion shipped its panel that\n // way and the Alert overflowed by 46px.\n \"box-border font-sans\",\n // React Aria gives the dialog tabindex=-1 and focuses it when nothing\n // inside is autofocused, so it can receive a visible focus ring it does\n // not want.\n \"outline-none\",\n // A dialog taller than the viewport has to scroll, or its footer is\n // unreachable. max-h is on the panel rather than the overlay so the\n // padding above stays visible.\n \"max-h-full overflow-y-auto\",\n]);\n\nexport const dialogTitleVariants = cva([\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"text-lg leading-(--leading-tight) font-semibold\",\n // The user-agent <h2> ships a 1em block margin. The legacy was inside a\n // Preflight'd app and never met it.\n \"m-0\",\n]);\n\nexport const dialogDescriptionVariants = cva([\n \"text-sm leading-(--leading-normal) text-muted-foreground\",\n // Same: the user-agent <p> block margin.\n \"m-0\",\n]);\n\nexport const dialogFooterVariants = cva([\n // NOT flex-col-reverse. The legacy reversed the footer below `sm`, so a\n // DOM order of Cancel-then-Confirm rendered Confirm-then-Cancel on a\n // phone -- WCAG 1.3.2 Meaningful Sequence, and both alert-dialog call\n // sites hit it. Wrapping keeps narrow viewports working without lying\n // about the order.\n \"flex flex-wrap items-center justify-end gap-2\",\n]);\n\nconst dialogCloseVariants = cva([\n \"absolute end-4 top-4 inline-flex size-6 items-center justify-center\",\n \"rounded-2xs box-border cursor-pointer\",\n \"border-(length:--border-width) border-transparent bg-transparent\",\n \"text-muted-foreground\",\n // Fill on hover, a quick press to .96 and a spring release (see calendar-nav-motion).\n \"calendar-nav-motion\",\n // The legacy sat at opacity-70 until hover, dimming the only close\n // affordance below its already-marginal contrast. A token that is legible\n // at rest does the same job without that.\n \"data-[hovered]:bg-muted data-[hovered]:text-foreground\",\n // outline-solid is load-bearing: outline-none sets --tw-outline-style:none,\n // and the width utility resolves its style from that variable, so a focus\n // rule can apply a width and a colour and paint nothing. Button shipped\n // exactly that.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4\",\n]);\n\n/**\n * A modal dialog.\n *\n * @example\n * <Dialog isOpen={open} onOpenChange={setOpen} title=\"Delete user\"\n * description=\"This cannot be undone.\">\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n * </Dialog>\n */\n/** A string title or description swaps as a whole when its text changes while the dialog is open. */\nfunction Swappable({ value, children }: { value: ReactNode; children: ReactNode }) {\n if (typeof value !== \"string\") return <>{children}</>;\n return (\n <SwapText swapKey={value} className=\"block\">\n {children}\n </SwapText>\n );\n}\n\nexport interface DialogProps\n extends Omit<AriaModalOverlayProps, \"className\" | \"style\" | \"children\"> {\n /**\n * The dialog's heading, and its accessible name.\n *\n * **Required.** The legacy allowed a dialog with no title and one call\n * site shipped that, producing an `aria-labelledby` pointing at nothing.\n * Making it a required prop turns that into something you cannot express.\n */\n title: ReactNode;\n /**\n * Rendered ABOVE the heading, inside the panel.\n *\n * Exists for Sheet's back control. A panel that replaces its own contents\n * rather than stacking a second modal needs a way back, and that control\n * cannot live in `title`: React Aria wires aria-labelledby to the heading,\n * so a button inside it becomes part of the dialog's accessible name --\n * \"Back Campaign details\" instead of \"Campaign details\".\n */\n beforeTitle?: ReactNode;\n /** Optional supporting text, wired to `aria-describedby` when present. */\n description?: ReactNode;\n /** The dialog body. */\n children?: ReactNode;\n /** Extra classes for the panel. Merged last, so they win conflicts. */\n className?: string;\n /**\n * Extra classes for the scrim, merged last.\n *\n * A centred modal never needs this. A Sheet does: it has to replace the\n * overlay's centring with an edge alignment, and it is the only way to\n * do that without a second copy of the whole overlay.\n */\n overlayClassName?: string;\n /**\n * Whether to render the corner close button.\n *\n * `true` by default. An `alertdialog` sets it `false`: a confirmation must\n * be answered, not dismissed silently.\n */\n closeButton?: boolean;\n /**\n * Accessible name for the close button.\n *\n * A prop because the legacy hard-coded the English string `Close` in an\n * `sr-only` span with no way to override it, which makes the component\n * unlocalisable.\n *\n * @default \"Close\"\n */\n closeLabel?: string;\n /** `alertdialog` for a destructive confirmation. @default \"dialog\" */\n role?: \"dialog\" | \"alertdialog\";\n /**\n * Whether clicking outside closes the dialog.\n *\n * `true`, which is NOT React Aria's default. React Aria ships\n * `isDismissable = false` (useOverlay.mjs:26); Radix, which the legacy\n * component wraps, closes on outside click unless told otherwise. Taking\n * the headless default here would have silently changed the behaviour of\n * all seven legacy call sites, and nothing in the suite would have\n * noticed.\n *\n * An `alertdialog` should set this `false`: a confirmation must be\n * answered, not clicked away.\n *\n * @default true\n */\n isDismissable?: boolean;\n /**\n * How the dialog comes in: `rise` fades the overlay (blurring the page) and lifts\n * the panel in on a spring; `none` leaves the entrance to the caller, as an\n * edge-anchored preset (Sheet) does.\n *\n * @default \"rise\"\n */\n entrance?: \"rise\" | \"none\";\n}\n\nexport const Dialog = forwardRef<HTMLElement, DialogProps>(function Dialog(\n {\n title,\n beforeTitle,\n description,\n children,\n className,\n closeButton = true,\n closeLabel = \"Close\",\n role = \"dialog\",\n isDismissable = true,\n overlayClassName,\n entrance = \"rise\",\n ...props\n },\n ref,\n) {\n return (\n // ModalOverlay explicitly, not a bare <Modal>. A bare Modal still renders\n // an overlay div, but className lands on the INNER element -- so the\n // scrim would be unstyleable and would paint nothing at all.\n <AriaModalOverlay\n {...props}\n isDismissable={isDismissable}\n data-slot=\"dialog-overlay\"\n className={cnAll(\n dialogOverlayVariants(),\n entrance === \"rise\" && \"dialog-overlay-motion\",\n overlayClassName,\n )}\n >\n <AriaModal className=\"contents\">\n <AriaDialog\n // Modal is a transparent pass-through (className=\"contents\", no\n // box of its own) and ModalOverlay is the scrim -- AriaDialog is\n // the only one of the three that is the visible panel, so the ref\n // goes here and not on either ancestor.\n ref={ref}\n role={role}\n data-slot=\"dialog\"\n className={cnAll(dialogPanelVariants(), entrance === \"rise\" && \"dialog-panel-motion\", className)}\n >\n {/*\n Above the heading, not inside it. Sheet's back control lives here:\n folding it into `title` would put a button inside the <h2> React\n Aria wires aria-labelledby to, so the dialog's accessible name\n would become \"Back Campaign details\".\n */}\n {beforeTitle}\n {/*\n slot=\"title\" is what React Aria wires aria-labelledby to. A plain\n <h2> here would render identically and leave the dialog unnamed.\n */}\n <AriaHeading slot=\"title\" data-slot=\"dialog-title\" className={dialogTitleVariants()}>\n <Swappable value={title}>{title}</Swappable>\n </AriaHeading>\n {description == null ? null : (\n <AriaText\n slot=\"description\"\n data-slot=\"dialog-description\"\n className={dialogDescriptionVariants()}\n >\n <Swappable value={description}>{description}</Swappable>\n </AriaText>\n )}\n {children}\n {closeButton ? (\n // slot=\"close\" is React Aria's own dismiss wiring; an onPress\n // that called onOpenChange by hand would not work inside an\n // uncontrolled DialogTrigger.\n <AriaButton slot=\"close\" aria-label={closeLabel} className={dialogCloseVariants()}>\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n ) : null}\n </AriaDialog>\n </AriaModal>\n </AriaModalOverlay>\n );\n});\n\n/**\n * Actions at the foot of a dialog.\n *\n * @example\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n */\nexport interface DialogFooterProps {\n children?: ReactNode;\n className?: string;\n}\n\nexport const DialogFooter = forwardRef<HTMLDivElement, DialogFooterProps>(function DialogFooter(\n { children, className },\n ref,\n) {\n return (\n <div ref={ref} data-slot=\"dialog-footer\" className={cn(dialogFooterVariants(), className)}>\n {children}\n </div>\n );\n});\n\n/**\n * Pairs a trigger with a dialog and owns the open state.\n *\n * Re-exported from React Aria unchanged. Only 3 of the 9 legacy call sites\n * use a trigger at all -- the rest drive `isOpen` themselves -- so wrapping\n * it would add a layer with nothing to do.\n */\nexport { DialogTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,MAAa,wBAAwB,IAAI;CAIvC;CAGA;CACA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI;CAOrC;CACA;CAIA;CACA;CAKA;CAIA;CAIA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI,CAGrC,mDAGA,KACF,CAAC;AAED,MAAa,4BAA4B,IAAI,CAC3C,4DAEA,KACF,CAAC;AAED,MAAa,uBAAuB,IAAI,CAMtC,+CACF,CAAC;AAED,MAAM,sBAAsB,IAAI;CAC9B;CACA;CACA;CACA;CAEA;CAIA;CAKA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;;AAeD,SAAS,UAAU,EAAE,OAAO,YAAuD;CACjF,IAAI,OAAO,UAAU,UAAU,OAAO,oBAAA,YAAA,EAAG,SAAW,CAAA;CACpD,OACE,oBAAC,UAAD;EAAU,SAAS;EAAO,WAAU;EACjC;CACO,CAAA;AAEd;AAiFA,MAAaA,WAAS,WAAqC,SAASA,SAClE,EACE,OACA,aACA,aACA,UACA,WACA,cAAc,MACd,aAAa,SACb,OAAO,UACP,gBAAgB,MAChB,kBACA,WAAW,QACX,GAAG,SAEL,KACA;CACA,OAIE,oBAACC,cAAD;EACE,GAAI;EACW;EACf,aAAU;EACV,WAAW,MACT,sBAAsB,GACtB,aAAa,UAAU,yBACvB,gBACF;EAEA,UAAA,oBAACC,OAAD;GAAW,WAAU;GACnB,UAAA,qBAACC,QAAD;IAKO;IACC;IACN,aAAU;IACV,WAAW,MAAM,oBAAoB,GAAG,aAAa,UAAU,uBAAuB,SAAS;IARjG,UAAA;KAgBG;KAKD,oBAACC,SAAD;MAAa,MAAK;MAAQ,aAAU;MAAe,WAAW,oBAAoB;MAChF,UAAA,oBAAC,WAAD;OAAW,OAAO;OAAQ,UAAA;MAAiB,CAAA;KAChC,CAAA;KACZ,eAAe,OAAO,OACrB,oBAACC,MAAD;MACE,MAAK;MACL,aAAU;MACV,WAAW,0BAA0B;MAErC,UAAA,oBAAC,WAAD;OAAW,OAAO;OAAc,UAAA;MAAuB,CAAA;KAC/C,CAAA;KAEX;KACA,cAIC,oBAACC,QAAD;MAAY,MAAK;MAAQ,cAAY;MAAY,WAAW,oBAAoB;MAC9E,UAAA,oBAAC,OAAD;OACE,eAAY;OACZ,SAAQ;OACR,MAAK;OACL,QAAO;OACP,aAAY;OACZ,eAAc;OAEd,UAAA,oBAAC,QAAD,EAAM,GAAE,qBAAsB,CAAA;MAC3B,CAAA;KACK,CAAA,IACV;IACM;;EACH,CAAA;CACK,CAAA;AAEtB,CAAC;AAgBD,MAAa,eAAe,WAA8C,SAAS,aACjF,EAAE,UAAU,aACZ,KACA;CACA,OACE,oBAAC,OAAD;EAAU;EAAK,aAAU;EAAgB,WAAW,GAAG,qBAAqB,GAAG,SAAS;EACrF;CACE,CAAA;AAET,CAAC"}
|
|
1
|
+
{"version":3,"file":"dialog.js","names":["Dialog","AriaModalOverlay","AriaModal","AriaDialog","AriaHeading","AriaText","AriaButton"],"sources":["../../src/dialog/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Dialog as AriaDialog,\n Heading as AriaHeading,\n Modal as AriaModal,\n ModalOverlay as AriaModalOverlay,\n Text as AriaText,\n type ModalOverlayProps as AriaModalOverlayProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Dialog, ported from the legacy shadcn/Radix component onto React Aria's\n * Modal primitives.\n *\n * FOUR LEGACY PARTS COLLAPSE INTO TWO PROPS. The census (9 importing files,\n * but only **7** that render anything -- header.tsx and PollQuizCreator.tsx\n * import the parts and use none) reads: DialogContent 9, DialogTitle 8,\n * DialogHeader 7, DialogDescription 2. Those four always appear in the same\n * shape, so `title` and `description` are props of the dialog itself.\n *\n * That is not tidying. `title` is REQUIRED, which makes an unnamed dialog\n * impossible to write -- and the legacy shipped one: LiveQAOverlay.tsx\n * renders a DialogContent with no DialogTitle, so its `aria-labelledby`\n * points at an id that matches no element and the dialog is announced with\n * no name.\n *\n * It also fixes the dangling-description defect for free. Radix set\n * `aria-describedby` unconditionally from a `useId`, so seven of the nine\n * legacy dialogs referenced an id that did not exist. React Aria wires it\n * only when a description slot is actually rendered.\n *\n * `DialogPortal` and `DialogOverlay` are not ported: zero external call\n * sites, and React Aria's ModalOverlay does both jobs.\n *\n * NOT PORTED, and deliberately: the enter/exit animation. The legacy leans\n * on tailwindcss-animate's `animate-in` / `zoom-in-95` utilities, which this\n * repo does not ship, and the motion tokens are a separate task. React Aria\n * exposes `data-entering` / `data-exiting` for whoever adds them.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const dialogOverlayVariants = cva([\n // Centring by flex, not by `top-1/2 left-1/2 -translate-1/2`. The legacy's\n // translate pair is a bracket value that check:hardcoding rejects, and\n // flex centring needs no transform to undo.\n \"fixed inset-0 z-50 flex items-center justify-center\",\n // The inset that keeps a full-width panel off the viewport edge. The\n // legacy did this with max-w-[calc(100%-2rem)] -- also a bracket value.\n \"p-4\",\n \"bg-scrim\",\n]);\n\nexport const dialogPanelVariants = cva([\n // max-w-128 (32rem), NOT max-w-lg. `--container-lg` does not exist in this\n // repo -- the container scale holds only page-shell widths (max 75rem,\n // wide 90rem) -- so `max-w-lg` compiles to NOTHING and the panel spans the\n // viewport. It emitted no rule at all, tsc and ESLint were both blind to\n // it, and a screenshot is what caught it. 128 spacing units is 32rem,\n // exactly the legacy sm:max-w-lg.\n \"relative w-full max-w-128\",\n \"rounded-panel border-(length:--border-width) border-border p-6\",\n // bg-card, not bg-background: a dialog is a raised surface, and card is\n // the role that already steps up the ink ramp on dark, where a shadow made\n // of ink is invisible. Its contrast pairs are in the contract already.\n \"bg-card text-card-foreground shadow-modal\",\n \"flex flex-col gap-4\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none:\n // content-box would add the 24px padding to max-w-lg, and an unstyled\n // document renders text in a serif. The Accordion shipped its panel that\n // way and the Alert overflowed by 46px.\n \"box-border font-sans\",\n // React Aria gives the dialog tabindex=-1 and focuses it when nothing\n // inside is autofocused, so it can receive a visible focus ring it does\n // not want.\n \"outline-none\",\n // A dialog taller than the viewport has to scroll, or its footer is\n // unreachable. max-h is on the panel rather than the overlay so the\n // padding above stays visible.\n \"max-h-full overflow-y-auto\",\n]);\n\nexport const dialogTitleVariants = cva([\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"text-lg leading-(--leading-tight) font-semibold\",\n // The user-agent <h2> ships a 1em block margin. The legacy was inside a\n // Preflight'd app and never met it.\n \"m-0\",\n]);\n\nexport const dialogDescriptionVariants = cva([\n \"text-sm leading-(--leading-normal) text-muted-foreground\",\n // Same: the user-agent <p> block margin.\n \"m-0\",\n]);\n\nexport const dialogFooterVariants = cva([\n // NOT flex-col-reverse. The legacy reversed the footer below `sm`, so a\n // DOM order of Cancel-then-Confirm rendered Confirm-then-Cancel on a\n // phone -- WCAG 1.3.2 Meaningful Sequence, and both alert-dialog call\n // sites hit it. Wrapping keeps narrow viewports working without lying\n // about the order.\n \"flex flex-wrap items-center justify-end gap-2\",\n]);\n\nconst dialogCloseVariants = cva([\n \"absolute end-4 top-4 inline-flex size-6 items-center justify-center\",\n \"rounded-2xs box-border cursor-pointer\",\n \"border-(length:--border-width) border-transparent bg-transparent\",\n \"text-muted-foreground\",\n // Fill on hover, a quick press to .96 and a spring release (see calendar-nav-motion).\n \"calendar-nav-motion\",\n // The legacy sat at opacity-70 until hover, dimming the only close\n // affordance below its already-marginal contrast. A token that is legible\n // at rest does the same job without that.\n \"data-[hovered]:bg-muted data-[hovered]:text-foreground\",\n // outline-solid is load-bearing: outline-none sets --tw-outline-style:none,\n // and the width utility resolves its style from that variable, so a focus\n // rule can apply a width and a colour and paint nothing. Button shipped\n // exactly that.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4\",\n]);\n\n/**\n * A modal dialog.\n *\n * @example\n * <Dialog isOpen={open} onOpenChange={setOpen} title=\"Delete user\"\n * description=\"This cannot be undone.\">\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n * </Dialog>\n */\n/** A string title or description swaps as a whole when its text changes while the dialog is open. */\nfunction Swappable({ value, children }: { value: ReactNode; children: ReactNode }) {\n if (typeof value !== \"string\") return <>{children}</>;\n return (\n <SwapText swapKey={value} className=\"block\">\n {children}\n </SwapText>\n );\n}\n\nexport interface DialogProps\n extends Omit<AriaModalOverlayProps, \"className\" | \"style\" | \"children\"> {\n /**\n * The dialog's heading, and its accessible name.\n *\n * **Required.** The legacy allowed a dialog with no title and one call\n * site shipped that, producing an `aria-labelledby` pointing at nothing.\n * Making it a required prop turns that into something you cannot express.\n */\n title: ReactNode;\n /**\n * Rendered ABOVE the heading, inside the panel.\n *\n * Exists for Sheet's back control. A panel that replaces its own contents\n * rather than stacking a second modal needs a way back, and that control\n * cannot live in `title`: React Aria wires aria-labelledby to the heading,\n * so a button inside it becomes part of the dialog's accessible name --\n * \"Back Campaign details\" instead of \"Campaign details\".\n */\n beforeTitle?: ReactNode;\n /** Optional supporting text, wired to `aria-describedby` when present. */\n description?: ReactNode;\n /** The dialog body. */\n children?: ReactNode;\n /** Extra classes for the panel. Merged last, so they win conflicts. */\n className?: string;\n /**\n * Extra classes for the scrim, merged last.\n *\n * A centred modal never needs this. A Sheet does: it has to replace the\n * overlay's centring with an edge alignment, and it is the only way to\n * do that without a second copy of the whole overlay.\n */\n overlayClassName?: string;\n /**\n * Whether to render the corner close button.\n *\n * `true` by default. An `alertdialog` sets it `false`: a confirmation must\n * be answered, not dismissed silently.\n */\n closeButton?: boolean;\n /**\n * Accessible name for the close button.\n *\n * A prop because the legacy hard-coded the English string `Close` in an\n * `sr-only` span with no way to override it, which makes the component\n * unlocalisable.\n *\n * @default \"Close\"\n */\n closeLabel?: string;\n /** `alertdialog` for a destructive confirmation. @default \"dialog\" */\n role?: \"dialog\" | \"alertdialog\";\n /**\n * Whether clicking outside closes the dialog.\n *\n * `true`, which is NOT React Aria's default. React Aria ships\n * `isDismissable = false` (useOverlay.mjs:26); Radix, which the legacy\n * component wraps, closes on outside click unless told otherwise. Taking\n * the headless default here would have silently changed the behaviour of\n * all seven legacy call sites, and nothing in the suite would have\n * noticed.\n *\n * An `alertdialog` should set this `false`: a confirmation must be\n * answered, not clicked away.\n *\n * @default true\n */\n isDismissable?: boolean;\n /**\n * How the dialog comes in: `rise` fades the overlay (blurring the page) and lifts\n * the panel in on a spring; `none` leaves the entrance to the caller, as an\n * edge-anchored preset (Sheet) does.\n *\n * @default \"rise\"\n */\n entrance?: \"rise\" | \"none\";\n}\n\nexport const Dialog = forwardRef<HTMLElement, DialogProps>(function Dialog(\n {\n title,\n beforeTitle,\n description,\n children,\n className,\n closeButton = true,\n closeLabel = \"Close\",\n role = \"dialog\",\n isDismissable = true,\n overlayClassName,\n entrance = \"rise\",\n ...props\n },\n ref,\n) {\n return (\n // ModalOverlay explicitly, not a bare <Modal>. A bare Modal still renders\n // an overlay div, but className lands on the INNER element -- so the\n // scrim would be unstyleable and would paint nothing at all.\n <AriaModalOverlay\n {...props}\n isDismissable={isDismissable}\n data-slot=\"dialog-overlay\"\n className={cnAll(\n dialogOverlayVariants(),\n entrance === \"rise\" && \"dialog-overlay-motion\",\n overlayClassName,\n )}\n >\n <AriaModal className=\"contents\">\n <AriaDialog\n // Modal is a transparent pass-through (className=\"contents\", no\n // box of its own) and ModalOverlay is the scrim -- AriaDialog is\n // the only one of the three that is the visible panel, so the ref\n // goes here and not on either ancestor.\n ref={ref}\n role={role}\n data-slot=\"dialog\"\n className={cnAll(dialogPanelVariants(), entrance === \"rise\" && \"dialog-panel-motion\", className)}\n >\n {/*\n Above the heading, not inside it. Sheet's back control lives here:\n folding it into `title` would put a button inside the <h2> React\n Aria wires aria-labelledby to, so the dialog's accessible name\n would become \"Back Campaign details\".\n */}\n {beforeTitle}\n {/*\n slot=\"title\" is what React Aria wires aria-labelledby to. A plain\n <h2> here would render identically and leave the dialog unnamed.\n */}\n <AriaHeading slot=\"title\" data-slot=\"dialog-title\" className={dialogTitleVariants()}>\n <Swappable value={title}>{title}</Swappable>\n </AriaHeading>\n {description == null ? null : (\n <AriaText\n slot=\"description\"\n data-slot=\"dialog-description\"\n className={dialogDescriptionVariants()}\n >\n <Swappable value={description}>{description}</Swappable>\n </AriaText>\n )}\n {children}\n {closeButton ? (\n // slot=\"close\" is React Aria's own dismiss wiring; an onPress\n // that called onOpenChange by hand would not work inside an\n // uncontrolled DialogTrigger.\n <AriaButton slot=\"close\" aria-label={closeLabel} className={dialogCloseVariants()}>\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n ) : null}\n </AriaDialog>\n </AriaModal>\n </AriaModalOverlay>\n );\n});\n\n/**\n * Actions at the foot of a dialog.\n *\n * @example\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n */\nexport interface DialogFooterProps {\n children?: ReactNode;\n className?: string;\n}\n\nexport const DialogFooter = forwardRef<HTMLDivElement, DialogFooterProps>(function DialogFooter(\n { children, className },\n ref,\n) {\n return (\n <div ref={ref} data-slot=\"dialog-footer\" className={cn(dialogFooterVariants(), className)}>\n {children}\n </div>\n );\n});\n\n/**\n * Pairs a trigger with a dialog and owns the open state.\n *\n * Re-exported from React Aria unchanged. Only 3 of the 9 legacy call sites\n * use a trigger at all -- the rest drive `isOpen` themselves -- so wrapping\n * it would add a layer with nothing to do.\n */\nexport { DialogTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,MAAa,wBAAwB,IAAI;CAIvC;CAGA;CACA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI;CAOrC;CACA;CAIA;CACA;CAKA;CAIA;CAIA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI,CAGrC,mDAGA,KACF,CAAC;AAED,MAAa,4BAA4B,IAAI,CAC3C,4DAEA,KACF,CAAC;AAED,MAAa,uBAAuB,IAAI,CAMtC,+CACF,CAAC;AAED,MAAM,sBAAsB,IAAI;CAC9B;CACA;CACA;CACA;CAEA;CAIA;CAKA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;;AAeD,SAAS,UAAU,EAAE,OAAO,YAAuD;CACjF,IAAI,OAAO,UAAU,UAAU,OAAO,oBAAA,YAAA,EAAG,SAAW,CAAA;CACpD,OACE,oBAAC,UAAD;EAAU,SAAS;EAAO,WAAU;EACjC;CACO,CAAA;AAEd;AAiFA,MAAaA,WAAS,WAAqC,SAASA,SAClE,EACE,OACA,aACA,aACA,UACA,WACA,cAAc,MACd,aAAa,SACb,OAAO,UACP,gBAAgB,MAChB,kBACA,WAAW,QACX,GAAG,SAEL,KACA;CACA,OAIE,oBAACC,cAAD;EACE,GAAI;EACW;EACf,aAAU;EACV,WAAW,MACT,sBAAsB,GACtB,aAAa,UAAU,yBACvB,gBACF;EAEA,UAAA,oBAACC,OAAD;GAAW,WAAU;GACnB,UAAA,qBAACC,QAAD;IAKO;IACC;IACN,aAAU;IACV,WAAW,MAAM,oBAAoB,GAAG,aAAa,UAAU,uBAAuB,SAAS;IARjG,UAAA;KAgBG;KAKD,oBAACC,SAAD;MAAa,MAAK;MAAQ,aAAU;MAAe,WAAW,oBAAoB;MAChF,UAAA,oBAAC,WAAD;OAAW,OAAO;OAAQ,UAAA;MAAiB,CAAA;KAChC,CAAA;KACZ,eAAe,OAAO,OACrB,oBAACC,MAAD;MACE,MAAK;MACL,aAAU;MACV,WAAW,0BAA0B;MAErC,UAAA,oBAAC,WAAD;OAAW,OAAO;OAAc,UAAA;MAAuB,CAAA;KAC/C,CAAA;KAEX;KACA,cAIC,oBAACC,QAAD;MAAY,MAAK;MAAQ,cAAY;MAAY,WAAW,oBAAoB;MAC9E,UAAA,oBAAC,OAAD;OACE,eAAY;OACZ,SAAQ;OACR,MAAK;OACL,QAAO;OACP,aAAY;OACZ,eAAc;OAEd,UAAA,oBAAC,QAAD,EAAM,GAAE,qBAAsB,CAAA;MAC3B,CAAA;KACK,CAAA,IACV;IACM;;EACH,CAAA;CACK,CAAA;AAEtB,CAAC;AAgBD,MAAa,eAAe,WAA8C,SAAS,aACjF,EAAE,UAAU,aACZ,KACA;CACA,OACE,oBAAC,OAAD;EAAU;EAAK,aAAU;EAAgB,WAAW,GAAG,qBAAqB,GAAG,SAAS;EACrF;CACE,CAAA;AAET,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//#region src/donut-chart/donut-chart.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* DonutChart: a few parts of one whole, when the share of each part matters
|
|
4
|
+
* more than a precise comparison.
|
|
5
|
+
*
|
|
6
|
+
* The ring sweeps open the first time it is seen: every segment leaves the top
|
|
7
|
+
* together and each trailing edge follows a beat behind the one before. New
|
|
8
|
+
* data, or a segment shown or hidden, moves every arc from where it is on
|
|
9
|
+
* screen; a leaving segment closes where it sits. The active segment (hover,
|
|
10
|
+
* focus, selection) slides out along its middle and thickens; the rest dim.
|
|
11
|
+
* The centre rolls like a drum between the total and each segment, and every
|
|
12
|
+
* number counts to its new value. Small parts fold into one "Other" segment.
|
|
13
|
+
* Where the parts do not add up to anything (a rate such as CTR, CPC or ROAS
|
|
14
|
+
* per source), `center` hides the total or puts a figure of the caller's own
|
|
15
|
+
* at rest instead; the segment readouts still roll in on hover and focus.
|
|
16
|
+
*
|
|
17
|
+
* Plain SVG and CSS; one small spring tween moves the arcs. The legend is the
|
|
18
|
+
* keyboard path (or the ring itself, without a legend), and a visually hidden
|
|
19
|
+
* table carries every value.
|
|
20
|
+
*/
|
|
21
|
+
export interface DonutChartDatum {
|
|
22
|
+
/** Stable identity. A segment whose key survives a data change keeps its place and colour. */
|
|
23
|
+
key: string;
|
|
24
|
+
label: string;
|
|
25
|
+
value: number;
|
|
26
|
+
/** Any CSS colour. Segments default to chart-1 to chart-4 in data order, then neutral steps. */
|
|
27
|
+
color?: string;
|
|
28
|
+
}
|
|
29
|
+
/** A figure of the caller's own for the centre at rest, in place of the summed total. */
|
|
30
|
+
export interface DonutChartCenter {
|
|
31
|
+
/** The line above the figure, such as "Blended CTR". */
|
|
32
|
+
label: string;
|
|
33
|
+
/**
|
|
34
|
+
* The figure. A number is formatted with `formatValue` and counts to each new
|
|
35
|
+
* value, with `unit` under it; a string is shown as written (`"2.4%"`).
|
|
36
|
+
*/
|
|
37
|
+
value: number | string;
|
|
38
|
+
}
|
|
39
|
+
export interface DonutChartProps {
|
|
40
|
+
data: DonutChartDatum[];
|
|
41
|
+
/** What the whole is, such as "Visits by source". Names the chart for assistive technology. */
|
|
42
|
+
label: string;
|
|
43
|
+
/** Unit after values, such as "visits". */
|
|
44
|
+
unit?: string;
|
|
45
|
+
formatValue?: (value: number) => string;
|
|
46
|
+
/** Centre label at rest, above the total. @default "Total" */
|
|
47
|
+
totalLabel?: string;
|
|
48
|
+
/**
|
|
49
|
+
* What the centre shows at rest. `"total"` sums the visible segments under
|
|
50
|
+
* `totalLabel`. `"none"` leaves it empty, for parts whose sum means nothing
|
|
51
|
+
* (CTR, CPC or ROAS by source). An object puts a figure of your own there
|
|
52
|
+
* instead, such as the blended rate. Hovering or focusing a segment still
|
|
53
|
+
* shows that segment's readout either way.
|
|
54
|
+
* @default "total"
|
|
55
|
+
*/
|
|
56
|
+
center?: "total" | "none" | DonutChartCenter;
|
|
57
|
+
/** Diameter in pixels. The chart scales down to fit narrower containers. @default 208 */
|
|
58
|
+
size?: number;
|
|
59
|
+
/** Ring thickness in pixels. @default 24 */
|
|
60
|
+
thickness?: number;
|
|
61
|
+
/** Segments below this share of the total join "Other". @default 0.04 */
|
|
62
|
+
groupBelow?: number;
|
|
63
|
+
/** The most segments drawn, counting "Other". @default 6 */
|
|
64
|
+
maxSegments?: number;
|
|
65
|
+
/** @default "Other" */
|
|
66
|
+
otherLabel?: string;
|
|
67
|
+
/** Controlled selected segment key. Hover and focus preview other segments without changing it. */
|
|
68
|
+
activeKey?: string | null;
|
|
69
|
+
defaultActiveKey?: string | null;
|
|
70
|
+
onActiveChange?: (key: string | null) => void;
|
|
71
|
+
/** Controlled hidden segment keys. A hidden segment closes and the rest of the ring redistributes. */
|
|
72
|
+
hiddenKeys?: string[];
|
|
73
|
+
defaultHiddenKeys?: string[];
|
|
74
|
+
onHiddenKeysChange?: (keys: string[]) => void;
|
|
75
|
+
/** What clicking a legend row does: show or hide its segment, or pin it as selected. @default "toggle" */
|
|
76
|
+
legendAction?: "toggle" | "select";
|
|
77
|
+
/** The synced legend beside or below the ring. @default true */
|
|
78
|
+
legend?: boolean;
|
|
79
|
+
/** @default "No data yet" */
|
|
80
|
+
emptyLabel?: string;
|
|
81
|
+
/** Extra classes for the root. Merged last. */
|
|
82
|
+
className?: string;
|
|
83
|
+
}
|
|
84
|
+
export declare const DonutChart: import("react").ForwardRefExoticComponent<DonutChartProps & import("react").RefAttributes<HTMLElement>>;
|
|
85
|
+
//#endregion
|
|
86
|
+
//# sourceMappingURL=donut-chart.d.ts.map
|