@seekalfred/primitives 0.6.0 → 0.7.1
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 +19 -11
- 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 +5 -1
- package/dist/alert/alert.js +68 -7
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert-dialog/alert-dialog.d.ts +45 -18
- package/dist/alert-dialog/alert-dialog.js +128 -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 +2 -2
- 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/card/card.js +1 -1
- 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 +56 -0
- package/dist/carousel/carousel.js +389 -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/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 +64 -0
- package/dist/donut-chart/donut-chart.js +576 -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 +21 -10
- package/dist/empty-state/empty-state.js +32 -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 +47 -26
- package/dist/filter-bar/filter-bar.js +272 -156
- 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 +49 -6
- package/dist/index.js +48 -8
- 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-surface.js +9 -8
- 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/line-chart/index.d.ts +2 -0
- package/dist/line-chart/index.js +2 -0
- package/dist/line-chart/line-chart.d.ts +76 -0
- package/dist/line-chart/line-chart.js +611 -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 +65 -0
- package/dist/money-input/money-input.js +607 -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 +71 -0
- package/dist/morph-select/morph-select.js +693 -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 +36 -0
- package/dist/multi-select/multi-select.js +152 -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.js +2 -2
- 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/popover.d.ts +12 -0
- package/dist/popover/popover.js +31 -7
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/progress.js +1 -1
- package/dist/radio-group/radio-group.js +1 -1
- 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 +208 -59
- 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 +2 -1
- package/dist/search-field/search-field.js +4 -4
- package/dist/search-field/search-field.js.map +1 -1
- package/dist/select/select.d.ts +3 -1
- package/dist/select/select.js +20 -8
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +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 +24 -4
- package/dist/stat-card/stat-card.js +114 -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 +56 -4
- 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.js +2 -2
- 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 +6 -0
- 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 +50 -0
- package/dist/usage-meter/usage-meter.js +440 -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,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { mergeRefs } from "../lib/merge-refs.js";
|
|
4
|
+
import { SwapText } from "../lib/swap-text.js";
|
|
5
|
+
import { useHeightFollow } from "../lib/height-follow.js";
|
|
6
|
+
import { useFlip } from "../lib/flip.js";
|
|
7
|
+
import { forwardRef, useMemo, useRef, useState } from "react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
//#region src/chip-group/chip-group.tsx
|
|
10
|
+
/** Roving-focus key of the overflow chip; the NUL keeps it from colliding with any option value. */
|
|
11
|
+
const MORE = "\0more";
|
|
12
|
+
/** Stagger between chips revealed by the overflow (ms), at most 300 in all. */
|
|
13
|
+
const STAGGER_MS = 35;
|
|
14
|
+
const ChipGroup = forwardRef(function ChipGroup({ options, value, onValueChange, label, multiple = true, maxVisible = Infinity, moreLabel = (hidden) => `+${hidden} more`, lessLabel = "Show less", className }, ref) {
|
|
15
|
+
const [expanded, setExpanded] = useState(false);
|
|
16
|
+
const [pinned, setPinned] = useState(value);
|
|
17
|
+
const [active, setActive] = useState(null);
|
|
18
|
+
const foldable = options.length > maxVisible;
|
|
19
|
+
const visible = !foldable || expanded ? options : options.filter((option, index) => index < maxVisible || pinned.includes(option.value) || value.includes(option.value));
|
|
20
|
+
const hidden = options.length - visible.length;
|
|
21
|
+
const showMore = foldable && (expanded || hidden > 0);
|
|
22
|
+
const keys = [...visible.map((option) => option.value), ...showMore ? [MORE] : []];
|
|
23
|
+
const tabStop = active !== null && keys.includes(active) ? active : visible.find((option) => value.includes(option.value))?.value ?? keys[0];
|
|
24
|
+
const group = useRef(null);
|
|
25
|
+
const refs = useMemo(() => mergeRefs(ref, group), [ref]);
|
|
26
|
+
const frame = useRef(null);
|
|
27
|
+
const morphKey = `${expanded}|${value.join(",")}`;
|
|
28
|
+
useFlip(group, morphKey, "var(--duration-morph)", "var(--ease-morph)");
|
|
29
|
+
useHeightFollow(frame, group, morphKey);
|
|
30
|
+
const seen = useRef(new Set(options.map((option) => option.value)));
|
|
31
|
+
const revealedAt = /* @__PURE__ */ new Map();
|
|
32
|
+
let rank = 0;
|
|
33
|
+
for (const option of visible) if (!seen.current.has(option.value)) revealedAt.set(option.value, Math.min(rank++ * STAGGER_MS, 300));
|
|
34
|
+
function toggle(next) {
|
|
35
|
+
const on = value.includes(next);
|
|
36
|
+
if (!multiple) {
|
|
37
|
+
onValueChange(on ? [] : [next]);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
onValueChange(options.filter((option) => option.value === next ? !on : value.includes(option.value)).map((option) => option.value));
|
|
41
|
+
}
|
|
42
|
+
function toggleMore() {
|
|
43
|
+
if (expanded) setPinned(value);
|
|
44
|
+
else seen.current = new Set(visible.map((option) => option.value));
|
|
45
|
+
setExpanded(!expanded);
|
|
46
|
+
}
|
|
47
|
+
function onKeyDown(event) {
|
|
48
|
+
const buttons = Array.from(event.currentTarget.querySelectorAll("button[data-chip]"));
|
|
49
|
+
const index = buttons.indexOf(document.activeElement);
|
|
50
|
+
if (index < 0) return;
|
|
51
|
+
const last = buttons.length - 1;
|
|
52
|
+
const moves = {
|
|
53
|
+
ArrowRight: index === last ? 0 : index + 1,
|
|
54
|
+
ArrowDown: index === last ? 0 : index + 1,
|
|
55
|
+
ArrowLeft: index === 0 ? last : index - 1,
|
|
56
|
+
ArrowUp: index === 0 ? last : index - 1,
|
|
57
|
+
Home: 0,
|
|
58
|
+
End: last
|
|
59
|
+
};
|
|
60
|
+
if (!(event.key in moves)) return;
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
buttons[moves[event.key]].focus();
|
|
63
|
+
}
|
|
64
|
+
const moreText = expanded ? lessLabel : moreLabel(hidden);
|
|
65
|
+
return /* @__PURE__ */ jsx("div", {
|
|
66
|
+
ref: frame,
|
|
67
|
+
"data-slot": "chip-group-frame",
|
|
68
|
+
className: "height-follow min-w-0",
|
|
69
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
70
|
+
ref: refs,
|
|
71
|
+
role: "group",
|
|
72
|
+
"aria-label": label,
|
|
73
|
+
"data-slot": "chip-group",
|
|
74
|
+
onKeyDown,
|
|
75
|
+
className: cn("relative isolate flex flex-wrap gap-2 box-border font-sans", className),
|
|
76
|
+
children: [visible.map((option) => {
|
|
77
|
+
const selected = value.includes(option.value);
|
|
78
|
+
const delay = revealedAt.get(option.value);
|
|
79
|
+
return /* @__PURE__ */ jsx("button", {
|
|
80
|
+
type: "button",
|
|
81
|
+
"data-flip": "",
|
|
82
|
+
"data-chip": option.value,
|
|
83
|
+
"data-slot": "chip",
|
|
84
|
+
"aria-pressed": selected,
|
|
85
|
+
tabIndex: tabStop === option.value ? 0 : -1,
|
|
86
|
+
onClick: () => toggle(option.value),
|
|
87
|
+
onFocus: () => setActive(option.value),
|
|
88
|
+
style: delay === void 0 ? void 0 : { animationDelay: `${delay}ms` },
|
|
89
|
+
className: cn("chip", delay === void 0 ? "" : "motion-safe:animate-chip-reveal"),
|
|
90
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
91
|
+
"data-selected": selected,
|
|
92
|
+
className: "chip-body",
|
|
93
|
+
children: [
|
|
94
|
+
/* @__PURE__ */ jsx("span", {
|
|
95
|
+
"aria-hidden": "true",
|
|
96
|
+
className: "chip-check",
|
|
97
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
98
|
+
width: 14,
|
|
99
|
+
height: 14,
|
|
100
|
+
viewBox: "0 0 24 24",
|
|
101
|
+
fill: "none",
|
|
102
|
+
stroke: "currentColor",
|
|
103
|
+
strokeWidth: 2.5,
|
|
104
|
+
strokeLinecap: "round",
|
|
105
|
+
strokeLinejoin: "round",
|
|
106
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
107
|
+
d: "M4 12.5 9.5 18 20 6.5",
|
|
108
|
+
pathLength: 1
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
}),
|
|
112
|
+
/* @__PURE__ */ jsx("span", {
|
|
113
|
+
"aria-hidden": "true",
|
|
114
|
+
className: "chip-slot"
|
|
115
|
+
}),
|
|
116
|
+
/* @__PURE__ */ jsx("span", {
|
|
117
|
+
className: "truncate",
|
|
118
|
+
children: option.label
|
|
119
|
+
})
|
|
120
|
+
]
|
|
121
|
+
})
|
|
122
|
+
}, option.value);
|
|
123
|
+
}), showMore ? /* @__PURE__ */ jsx("button", {
|
|
124
|
+
type: "button",
|
|
125
|
+
"data-flip": "",
|
|
126
|
+
"data-chip": MORE,
|
|
127
|
+
"data-more": "",
|
|
128
|
+
"aria-expanded": expanded,
|
|
129
|
+
tabIndex: tabStop === MORE ? 0 : -1,
|
|
130
|
+
onClick: toggleMore,
|
|
131
|
+
onFocus: () => setActive(MORE),
|
|
132
|
+
className: "chip chip-more",
|
|
133
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
134
|
+
className: "chip-body tabular-nums",
|
|
135
|
+
children: /* @__PURE__ */ jsx(SwapText, {
|
|
136
|
+
swapKey: moreText,
|
|
137
|
+
motion: "rise",
|
|
138
|
+
children: moreText
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
}, MORE) : null]
|
|
142
|
+
})
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
//#endregion
|
|
146
|
+
export { ChipGroup };
|
|
147
|
+
|
|
148
|
+
//# sourceMappingURL=chip-group.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chip-group.js","names":[],"sources":["../../src/chip-group/chip-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useMemo, useRef, useState, type KeyboardEvent } from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useFlip } from \"../lib/flip.js\";\nimport { useHeightFollow } from \"../lib/height-follow.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * ChipGroup: selectable filter chips for narrowing a list by facets people\n * toggle often, such as topics, statuses or tags.\n *\n * Selecting a chip grows a check in at its start (it draws itself as it\n * scales up from a soft blur), the label slides over, the chip widens on the\n * morph spring, and its neighbours glide to their new places, even across\n * lines. The row follows its own height. Long sets fold behind a \"+N more\"\n * chip; opening it grows the hidden chips in one after another. Arrow keys move\n * between chips, Space or Enter toggles, and each chip is a pressed or\n * unpressed button. Selected reads as a tint plus the check, never colour alone.\n */\nexport interface ChipOption {\n value: string;\n label: string;\n}\n\nexport interface ChipGroupProps {\n options: ChipOption[];\n value: string[];\n onValueChange: (value: string[]) => void;\n /** Accessible name of the group, such as \"Topics\". */\n label: string;\n /** Allow several chips at once. In single mode the selected chip can still be cleared. @default true */\n multiple?: boolean;\n /** Chips shown before the rest fold behind a \"+N more\" chip. Chips selected when it folds stay in view. */\n maxVisible?: number;\n /** Text of the overflow chip. @default (hidden) => `+${hidden} more` */\n moreLabel?: (hidden: number) => string;\n /** @default \"Show less\" */\n lessLabel?: string;\n /** Extra classes for the group. Merged last. */\n className?: string;\n}\n\n/** Roving-focus key of the overflow chip; the NUL keeps it from colliding with any option value. */\nconst MORE = \"\\u0000more\";\n/** Stagger between chips revealed by the overflow (ms), at most 300 in all. */\nconst STAGGER_MS = 35;\n\nexport const ChipGroup = forwardRef<HTMLDivElement, ChipGroupProps>(function ChipGroup(\n {\n options,\n value,\n onValueChange,\n label,\n multiple = true,\n maxVisible = Infinity,\n moreLabel = (hidden) => `+${hidden} more`,\n lessLabel = \"Show less\",\n className,\n },\n ref,\n) {\n const [expanded, setExpanded] = useState(false);\n // Chips selected when the overflow folds stay in view, so a selection is never hidden.\n const [pinned, setPinned] = useState<string[]>(value);\n const [active, setActive] = useState<string | null>(null);\n const foldable = options.length > maxVisible;\n const visible =\n !foldable || expanded\n ? options\n : options.filter(\n (option, index) => index < maxVisible || pinned.includes(option.value) || value.includes(option.value),\n );\n const hidden = options.length - visible.length;\n const showMore = foldable && (expanded || hidden > 0);\n const keys = [...visible.map((option) => option.value), ...(showMore ? [MORE] : [])];\n // One chip holds the tab stop: the last one focused, else the first selected, else the first.\n const tabStop =\n active !== null && keys.includes(active)\n ? active\n : (visible.find((option) => value.includes(option.value))?.value ?? keys[0]);\n\n const group = useRef<HTMLDivElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, group), [ref]);\n const frame = useRef<HTMLDivElement>(null);\n const morphKey = `${expanded}|${value.join(\",\")}`;\n // Neighbours glide to their new places when a chip widens, even across lines; the row follows its height.\n useFlip(group, morphKey, \"var(--duration-morph)\", \"var(--ease-morph)\");\n useHeightFollow(frame, group, morphKey);\n // Chips that appear after the first render grow in; the ones there from the start never animate.\n const seen = useRef(new Set(options.map((option) => option.value)));\n const revealedAt = new Map<string, number>();\n let rank = 0;\n for (const option of visible) {\n if (!seen.current.has(option.value)) revealedAt.set(option.value, Math.min(rank++ * STAGGER_MS, 300));\n }\n\n function toggle(next: string) {\n const on = value.includes(next);\n if (!multiple) {\n onValueChange(on ? [] : [next]);\n return;\n }\n onValueChange(\n options.filter((option) => (option.value === next ? !on : value.includes(option.value))).map((option) => option.value),\n );\n }\n\n function toggleMore() {\n if (expanded) setPinned(value);\n // Only the chips this opening reveals grow in; the ones already on screen stay put.\n else seen.current = new Set(visible.map((option) => option.value));\n setExpanded(!expanded);\n }\n\n function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n const buttons = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>(\"button[data-chip]\"));\n const index = buttons.indexOf(document.activeElement as HTMLButtonElement);\n if (index < 0) return;\n const last = buttons.length - 1;\n const moves: Record<string, number> = {\n ArrowRight: index === last ? 0 : index + 1,\n ArrowDown: index === last ? 0 : index + 1,\n ArrowLeft: index === 0 ? last : index - 1,\n ArrowUp: index === 0 ? last : index - 1,\n Home: 0,\n End: last,\n };\n if (!(event.key in moves)) return;\n event.preventDefault();\n buttons[moves[event.key]!]!.focus();\n }\n\n const moreText = expanded ? lessLabel : moreLabel(hidden);\n\n return (\n <div ref={frame} data-slot=\"chip-group-frame\" className=\"height-follow min-w-0\">\n <div\n ref={refs}\n role=\"group\"\n aria-label={label}\n data-slot=\"chip-group\"\n onKeyDown={onKeyDown}\n className={cn(\"relative isolate flex flex-wrap gap-2 box-border font-sans\", className)}\n >\n {visible.map((option) => {\n const selected = value.includes(option.value);\n const delay = revealedAt.get(option.value);\n return (\n <button\n key={option.value}\n type=\"button\"\n data-flip=\"\"\n data-chip={option.value}\n data-slot=\"chip\"\n aria-pressed={selected}\n tabIndex={tabStop === option.value ? 0 : -1}\n onClick={() => toggle(option.value)}\n onFocus={() => setActive(option.value)}\n style={delay === undefined ? undefined : ({ animationDelay: `${delay}ms` })}\n className={cn(\"chip\", delay === undefined ? \"\" : \"motion-safe:animate-chip-reveal\")}\n >\n <span data-selected={selected} className=\"chip-body\">\n <span aria-hidden=\"true\" className=\"chip-check\">\n <svg width={14} height={14} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M4 12.5 9.5 18 20 6.5\" pathLength={1} />\n </svg>\n </span>\n <span aria-hidden=\"true\" className=\"chip-slot\" />\n <span className=\"truncate\">{option.label}</span>\n </span>\n </button>\n );\n })}\n {showMore ? (\n <button\n key={MORE}\n type=\"button\"\n data-flip=\"\"\n data-chip={MORE}\n data-more=\"\"\n aria-expanded={expanded}\n tabIndex={tabStop === MORE ? 0 : -1}\n onClick={toggleMore}\n onFocus={() => setActive(MORE)}\n className=\"chip chip-more\"\n >\n <span className=\"chip-body tabular-nums\">\n <SwapText swapKey={moreText} motion=\"rise\">\n {moreText}\n </SwapText>\n </span>\n </button>\n ) : null}\n </div>\n </div>\n );\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/countdown/countdown.d.ts
|
|
3
|
+
export type CountdownUnit = "days" | "hours" | "minutes" | "seconds";
|
|
4
|
+
/**
|
|
5
|
+
* Countdown: the time left until a fixed moment people wait for, such as a
|
|
6
|
+
* launch, a live event or a deadline. Each digit is a wheel that takes the short
|
|
7
|
+
* way round, so a tick turns downward and 00 to 59 is a single step; the first
|
|
8
|
+
* reading rolls up from zero, one digit after another. When the moment passes
|
|
9
|
+
* the digits give way to `completeLabel` and the box follows the new size.
|
|
10
|
+
*
|
|
11
|
+
* The clock runs only while the countdown is on screen and the tab is visible,
|
|
12
|
+
* and reads the clock again on return, so it never drifts. Completion is armed
|
|
13
|
+
* separately, so `onComplete` fires on time even while the ticking is paused.
|
|
14
|
+
*
|
|
15
|
+
* The digits are a picture. The timer is read as plain text ("1 day, 2 hours,
|
|
16
|
+
* 5 minutes") that is not announced every second, and finishing is announced
|
|
17
|
+
* once, politely. Reduced motion jumps.
|
|
18
|
+
*/
|
|
19
|
+
export interface CountdownProps {
|
|
20
|
+
/**
|
|
21
|
+
* 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
|
|
22
|
+
* 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
|
|
23
|
+
* no zone is given, so the result never depends on the visitor's own time zone.
|
|
24
|
+
*/
|
|
25
|
+
target: Date | number | string;
|
|
26
|
+
/** IANA zone for a target string without an offset, such as "America/Los_Angeles". Daylight saving is resolved for the target date. */
|
|
27
|
+
timeZone?: string;
|
|
28
|
+
/** 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"] */
|
|
29
|
+
units?: CountdownUnit[];
|
|
30
|
+
/** Large digits with unit names, or one compact line for banners and table cells. @default "large" */
|
|
31
|
+
variant?: "large" | "compact";
|
|
32
|
+
/** Shown once the target passes. The digits swap into it. @default "Live now" */
|
|
33
|
+
completeLabel?: ReactNode;
|
|
34
|
+
/** Fires once, the moment the countdown reaches zero while mounted. It fires on time even when the tab is hidden. */
|
|
35
|
+
onComplete?: () => void;
|
|
36
|
+
/** Accessible name, such as "Keynote starts in". @default "Time remaining" */
|
|
37
|
+
label?: string;
|
|
38
|
+
/** Clock source in epoch milliseconds. Pass a server-synced clock when the visitor's device time cannot be trusted. @default Date.now */
|
|
39
|
+
clock?: () => number;
|
|
40
|
+
/** Extra classes for the countdown. Merged last. */
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
export declare const Countdown: import("react").ForwardRefExoticComponent<CountdownProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
44
|
+
//#endregion
|
|
45
|
+
//# sourceMappingURL=countdown.d.ts.map
|
|
@@ -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":";;;;;;;;;AA8DA,MAAM,gBAAiC;CAAC;CAAQ;CAAS;CAAW;AAAS;AAC7E,MAAM,UAAyC;CAC7C,MAAM;CACN,OAAO;CACP,SAAS;CACT,SAAS;AACX;;AAEA,MAAM,QAAuC;CAC3C,MAAM;CACN,OAAO;CACP,SAAS;CACT,SAAS;AACX;;AAEA,MAAM,QAAyD;CAC7D,MAAM;EAAC;EAAQ;EAAK;CAAK;CACzB,OAAO;EAAC;EAAS;EAAK;CAAM;CAC5B,SAAS;EAAC;EAAW;EAAK;CAAQ;CAClC,SAAS;EAAC;EAAW;EAAK;CAAQ;AACpC;;AAEA,MAAM,cAAc;;AAEpB,MAAM,iBAAiB;;AAEvB,MAAM,oBAAoB;AAC1B,MAAM,sBAAsB;;AAE5B,MAAM,mBAAmB;AAEzB,MAAM,oBAAoB,KAAK,IAAI;;AAGnC,SAAS,WAAW,OAAe,UAAkB;CACnD,MAAM,QAAQ,IAAI,KAAK,eAAe,SAAS;EAC7C;EACA,WAAW;EACX,MAAM;EACN,OAAO;EACP,KAAK;EACL,MAAM;EACN,QAAQ;EACR,QAAQ;CACV,CAAC,CAAC,CAAC,cAAc,IAAI,KAAK,KAAK,CAAC;CAChC,MAAM,OAAO,SAAiB,OAAO,MAAM,MAAM,SAAS,KAAK,SAAS,IAAI,CAAC,EAAE,SAAS,CAAC;CACzF,OACE,KAAK,IAAI,IAAI,MAAM,GAAG,IAAI,OAAO,IAAI,GAAG,IAAI,KAAK,GAAG,IAAI,MAAM,GAAG,IAAI,QAAQ,GAAG,IAAI,QAAQ,CAAC,IAC7F,KAAK,MAAM,QAAQ,GAAI,IAAI;AAE/B;;AAGA,SAAS,QAAQ,QAAgC,UAAmB;CAClE,IAAI,OAAO,WAAW,UAAU,OAAO;CACvC,IAAI,kBAAkB,MAAM,OAAO,OAAO,QAAQ;CAClD,MAAM,OAAO,2EAA2E,KACtF,OAAO,KAAK,CACd;CACA,IAAI,CAAC,MAAM,OAAO,KAAK,MAAM,MAAM;CACnC,MAAM,MAAM,KAAK,IACf,CAAC,KAAK,IACN,CAAC,KAAK,KAAM,GACZ,CAAC,KAAK,IACN,EAAE,KAAK,MAAM,IACb,EAAE,KAAK,MAAM,IACb,EAAE,KAAK,MAAM,EACf;CACA,IAAI,CAAC,UAAU,OAAO;CAGtB,OAAO,MAAM,WADC,MAAM,WAAW,KAAK,QAAQ,GACb,QAAQ;AACzC;;AAGA,SAAS,SAAS,SAAiB,OAAwB;CACzD,OAAO,MAAM,KAAK,MAAM,WAAW;EACjC;EACA,OACE,UAAU,IACN,KAAK,MAAM,UAAU,QAAQ,KAAK,IAClC,KAAK,MAAM,UAAU,QAAQ,KAAK,IAAI,MAAM;CACpD,EAAE;AACJ;;AAGA,SAAS,SAAS,MAAqB,SAAkB,QAAgB;CACvE,OAAO,MAAM,KAAK,EAAE,OAAO,IAAI,GAAG,UAAU;EAE1C,OADa,UAAU,SAAS,KAAK,WAAW,KAAK,CAAC,UACvC,SAAS,UAAU,IAAI,IAAK;CAC7C,CAAC;AACH;AAEA,MAAM,uBAAuB,WAAuB;CAClD,SAAS,iBAAiB,oBAAoB,MAAM;CACpD,aAAa,SAAS,oBAAoB,oBAAoB,MAAM;AACtE;AACA,MAAM,sBAAsB,SAAS,oBAAoB;AACzD,MAAM,sBAAsB;;AAG5B,SAAS,YAAY,MAAuC;CAC1D,MAAM,CAAC,UAAU,eAAe,SAAS,OAAO,yBAAyB,WAAW;CACpF,gBAAgB;EACd,MAAM,SAAS,KAAK;EACpB,IAAI,CAAC,UAAU,OAAO,yBAAyB,aAAa;EAC5D,MAAM,WAAW,IAAI,sBAAsB,CAAC,WAAW;GACrD,IAAI,OAAO,YAAY,MAAM,cAAc;EAC7C,CAAC;EACD,SAAS,QAAQ,MAAM;EACvB,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,IAAI,CAAC;CACT,OAAO;AACT;;;;;AAQA,SAAS,MAAM,EACb,OACA,MACA,OACA,SAMC;CACD,MAAM,CAAC,OAAO,YAAY,SAAS;EAAE,OAAO;EAAG,OAAO;CAAE,CAAC;CACzD,IAAI,EAAE,UAAU;CAChB,IAAI,MAAM,UAAU,OAAO;EACzB,MAAM,MAAM,QAAQ,MAAM,QAAQ,QAAQ;EAC1C,MAAM,OAAO,OAAO;EACpB,QAAQ,MAAM,SAAS,QAAQ,KAAK,CAAC,OAAO;EAC5C,SAAS;GAAE;GAAO;EAAM,CAAC;CAC3B;CACA,MAAM,QAAyD;EAC7D,WAAW;EACX,UAAU;CACZ;CAEA,IAAI,UAAU,aAAa;EACzB,MAAM,kBAAkB,GAAG,MAAM;EACjC,MAAM,qBAAqB;EAC3B,MAAM,2BAA2B;CACnC;CACA,OACE,qBAAC,QAAD;EAAa;EAAO,WAAU;EAA9B,UAAA,CACE,oBAAC,QAAD;GAAM,WAAU;GAAgB,UAAA;EAAO,CAAA,GACtC,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,SAChC,oBAAC,QAAD;GAAiB,OAAO,EAAE,UAAU,KAAK;GAAoB,WAAU;GACpE,UAAA;EACG,GAFK,IAEL,CACP,CACG;;AAEV;AAEA,MAAa,YAAY,WAA2C,SAAS,UAC3E,EACE,QACA,UACA,QAAQ,eACR,UAAU,SACV,gBAAgB,YAChB,YACA,QAAQ,kBACR,QAAQ,aACR,aAEF,KACA;CACA,MAAM,OAAO,OAA8B,IAAI;CAC/C,MAAM,OAAO,cAAc,UAAU,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC;CACtD,MAAM,MAAM,QAAQ,QAAQ,QAAQ;CACpC,MAAM,QAAQ,OAAO,SAAS,GAAG;CACjC,MAAM,aAAa,MAAM,SACrB,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,GAAG,MAAM,QAAQ,KAAK,QAAQ,EAAE,IACjD;CAGJ,MAAM,cAAc,qBAAqB,qBAAqB,eAAe,aAAa;CAC1F,MAAM,WAAW,YAAY,IAAI;CACjC,MAAM,UAAU,eAAe,YAAY;CAC3C,MAAM,CAAC,KAAK,UAAU,SAAwB,IAAI;CAClD,MAAM,WAAW,OAAO,KAAK;CAC7B,sBAAsB;EACpB,SAAS,UAAU;CACrB,CAAC;CACD,gBAAgB;EACd,IAAI,CAAC,SAAS;EACd,IAAI;EACJ,MAAM,aAAa;GACjB,MAAM,OAAO,SAAS,QAAQ;GAC9B,OAAO,IAAI;GACX,MAAM,OAAO,MAAM;GAEnB,IAAI,OAAO,GAAG,QAAQ,WAAW,OAAO,OAAO,OAAQ,OAAQ,cAAc;EAC/E;EACA,QAAQ,WAAW,MAAM,CAAC;EAC1B,aAAa,aAAa,KAAK;CACjC,GAAG,CAAC,KAAK,OAAO,CAAC;CAGjB,gBAAgB;EACd,IAAI,CAAC,OAAO;EACZ,MAAM,OAAO,MAAM,SAAS,QAAQ;EACpC,IAAI,QAAQ,GAAG;EACf,MAAM,QAAQ,iBACN,OAAO,SAAS,QAAQ,CAAC,GAC/B,KAAK,IAAI,aAAa,OAAO,cAAc,CAC7C;EACA,aAAa,aAAa,KAAK;CACjC,GAAG,CAAC,KAAK,KAAK,CAAC;CAEf,MAAM,OAAO,QAAQ,QAAQ,CAAC,QAAQ,OAAO,KAAK,IAAI,GAAG,MAAM,GAAG;CAClE,MAAM,WAAW,SAAS;CAC1B,MAAM,UAAU,SAAS;CACzB,MAAM,gBAAgB,OAAO,UAAU;CACvC,sBAAsB;EACpB,cAAc,UAAU;CAC1B,CAAC;CACD,MAAM,aAAa,OAAsB,IAAI;CAC7C,gBAAgB;EACd,IAAI,SAAS,MAAM;EACnB,IAAI,OAAO,GAAG;GACZ,WAAW,UAAU;GACrB;EACF;EACA,IAAI,WAAW,YAAY,KAAK;GAC9B,WAAW,UAAU;GACrB,cAAc,UAAU;EAC1B;CACF,GAAG,CAAC,KAAK,IAAI,CAAC;CAEd,MAAM,CAAC,OAAO,YAAY,SAAgB,SAAS;CACnD,IAAI,UAAU,aAAa,CAAC,SAAS,SAAS,WAAW;CACzD,gBAAgB;EACd,IAAI,UAAU,aAAa;EAC3B,MAAM,QAAQ,iBAAiB,SAAS,SAAS,GAAG,gBAAgB;EACpE,aAAa,aAAa,KAAK;CACjC,GAAG,CAAC,KAAK,CAAC;CAEV,MAAM,UAAU,SAAS,OAAO,IAAI,KAAK,KAAK,OAAO,GAAI;CACzD,MAAM,QAAQ,SAAS,SAAS,UAAU;CAC1C,MAAM,UAAU,YAAY;CAG5B,MAAM,QAAQ,OAAuB,IAAI;CACzC,MAAM,UAAU,OAAuB,IAAI;CAC3C,MAAM,QAAQ,WAAW,SAAS;CAClC,cAAc,SAAS,KAAK;CAC5B,gBAAgB,OAAO,SAAS,KAAK;CAErC,MAAM,eAAe,OAAO,kBAAkB,WAAW,gBAAgB;CACzE,MAAM,SAAS,WACX,eACA,UACE,KACA,MACG,QAAQ,MAAM,UAAU,KAAK,QAAQ,KAAK,UAAU,MAAM,SAAS,CAAC,CAAC,CACrE,QAAQ,SAAS,UAAU,MAAM,KAAK,SAAS,SAAS,CAAC,CACzD,KAAK,SAAS,GAAG,KAAK,MAAM,GAAG,MAAM,KAAK,KAAK,CAAC,KAAK,KAAK,UAAU,IAAI,KAAK,KAAK,CAAC,CACnF,KAAK,IAAI;CAElB,OACE,qBAAC,OAAD;EACE,KAAK;EACL,aAAU;EACV,gBAAc;EACd,iBAAe,WAAW,KAAK,KAAA;EAC/B,gBAAc,UAAU,KAAK,KAAA;EAC7B,WAAW,GAAG,aAAa,SAAS;EANtC,UAAA,CAQE,qBAAC,OAAD;GAAK,KAAK;GAAO,MAAK;GAAQ,cAAY;GAAO,WAAU;GAA3D,UAAA,CACE,oBAAC,OAAD;IAAK,WAAU;IACb,UAAA,oBAAC,OAAD;KAAK,KAAK;KAAS,WAAU;KAAoB,eAAY;KAC3D,UAAA,oBAAC,UAAD;MAAU,SAAS;MAAO,QAAO;MAC9B,UAAA,WACC,qBAAC,QAAD;OAAM,WAAU;OAAhB,UAAA,CACE,oBAAC,QAAD,EAAM,WAAU,kBAAmB,CAAA,GAClC,aACG;MAEN,CAAA,IAAA,oBAAC,QAAD;OAAM,WAAU;OACb,UAAA,MAAM,KAAK,MAAM,UAAU;QAC1B,MAAM,OAAO,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,GAAG,GAAG;QAC/C,MAAM,QAAQ,SAAS,KAAK,MAAM,UAAU,GAAG,KAAK,MAAM;QAC1D,OACE,qBAAC,QAAD;SAAsB,WAAU;SAAhC,UAAA,CACG,CAAC,WAAW,QAAQ,IAAI,oBAAC,QAAD;UAAM,WAAU;UAAkB,UAAA;SAAO,CAAA,IAAI,MACtE,qBAAC,QAAD;UAAM,WAAU;UAAhB,UAAA,CACE,oBAAC,QAAD;WAAM,WAAU;WACb,UAAA,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,MAAM,UACpB,oBAAC,OAAD;YAEE,OAAO,OAAO,IAAI;YAClB,MAAM,MAAM;YACL;YACP,OAAO,KAAK,KACT,QAAQ,IAAI,SAAS,mBACtB,mBACF;WACD,GARM,GAAG,KAAK,OAAO,KAAK,SAAS,OAQnC,CACF;UACG,CAAA,GACN,oBAAC,QAAD;WAAM,WAAU;WACb,UAAA,MAAM,KAAK,KAAK,CAAC,UAAU,IAAI;UAC5B,CAAA,CACF;SACF,CAAA,CAAA;QArBK,GAAA,KAAK,IAqBV;OAEV,CAAC;MACG,CAAA;KAEA,CAAA;IACP,CAAA;GACF,CAAA,GACL,oBAAC,QAAD;IAAM,WAAU;IAAW,UAAA;GAAa,CAAA,CACrC;EAEL,CAAA,GAAA,oBAAC,QAAD;GAAM,WAAU;GAAU,MAAK;GAC5B,UAAA,WAAW,eAAe;EACvB,CAAA,CACH;;AAET,CAAC"}
|
|
@@ -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"}
|