@seekalfred/primitives 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +664 -9
- package/dist/accordion/accordion.d.ts +5 -5
- package/dist/accordion/accordion.js +22 -11
- package/dist/accordion/accordion.js.map +1 -1
- package/dist/action-button/action-button.d.ts +50 -0
- package/dist/action-button/action-button.js +159 -0
- package/dist/action-button/action-button.js.map +1 -0
- package/dist/action-button/index.d.ts +2 -0
- package/dist/action-button/index.js +2 -0
- package/dist/alert/alert.d.ts +16 -2
- package/dist/alert/alert.js +118 -20
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert-dialog/alert-dialog.d.ts +1 -1
- package/dist/alert-dialog/alert-dialog.js +4 -2
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/avatar/avatar.d.ts +7 -1
- package/dist/avatar/avatar.js +51 -18
- package/dist/avatar/avatar.js.map +1 -1
- package/dist/badge/badge.d.ts +10 -3
- package/dist/badge/badge.js +37 -5
- package/dist/badge/badge.js.map +1 -1
- package/dist/breadcrumb/breadcrumb.d.ts +3 -30
- package/dist/breadcrumb/breadcrumb.js +25 -8
- package/dist/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/button/button.d.ts +36 -23
- package/dist/button/button.js +178 -33
- package/dist/button/button.js.map +1 -1
- package/dist/button-group/button-group.d.ts +76 -0
- package/dist/button-group/button-group.js +403 -0
- package/dist/button-group/button-group.js.map +1 -0
- package/dist/button-group/index.d.ts +2 -0
- package/dist/button-group/index.js +2 -0
- package/dist/calendar/calendar.d.ts +3 -3
- package/dist/calendar/calendar.js +263 -34
- package/dist/calendar/calendar.js.map +1 -1
- package/dist/card/card.d.ts +14 -7
- package/dist/card/card.js +32 -14
- package/dist/card/card.js.map +1 -1
- package/dist/checkbox/checkbox.d.ts +2 -9
- package/dist/checkbox/checkbox.js +28 -29
- package/dist/checkbox/checkbox.js.map +1 -1
- package/dist/confirm-morph/confirm-morph.d.ts +85 -0
- package/dist/confirm-morph/confirm-morph.js +652 -0
- package/dist/confirm-morph/confirm-morph.js.map +1 -0
- package/dist/confirm-morph/index.d.ts +2 -0
- package/dist/confirm-morph/index.js +2 -0
- package/dist/copy-button/copy-button.d.ts +32 -0
- package/dist/copy-button/copy-button.js +134 -0
- package/dist/copy-button/copy-button.js.map +1 -0
- package/dist/copy-button/index.d.ts +2 -0
- package/dist/copy-button/index.js +2 -0
- package/dist/date-picker/date-picker.d.ts +51 -0
- package/dist/date-picker/date-picker.js +241 -0
- package/dist/date-picker/date-picker.js.map +1 -0
- package/dist/date-picker/date-range-picker.d.ts +78 -0
- package/dist/date-picker/date-range-picker.js +1015 -0
- package/dist/date-picker/date-range-picker.js.map +1 -0
- package/dist/date-picker/index.d.ts +4 -0
- package/dist/date-picker/index.js +4 -0
- package/dist/date-picker/picker-parts.d.ts +46 -0
- package/dist/date-picker/picker-parts.js +135 -0
- package/dist/date-picker/picker-parts.js.map +1 -0
- package/dist/dialog/dialog.d.ts +12 -16
- package/dist/dialog/dialog.js +45 -14
- package/dist/dialog/dialog.js.map +1 -1
- package/dist/empty-state/empty-state.d.ts +44 -0
- package/dist/empty-state/empty-state.js +83 -0
- package/dist/empty-state/empty-state.js.map +1 -0
- package/dist/empty-state/index.d.ts +2 -0
- package/dist/empty-state/index.js +2 -0
- package/dist/expanding-button-group/expanding-button-group.d.ts +54 -0
- package/dist/expanding-button-group/expanding-button-group.js +350 -0
- package/dist/expanding-button-group/expanding-button-group.js.map +1 -0
- package/dist/expanding-button-group/index.d.ts +2 -0
- package/dist/expanding-button-group/index.js +2 -0
- package/dist/file-drop-zone/file-drop-zone.d.ts +71 -0
- package/dist/file-drop-zone/file-drop-zone.js +116 -0
- package/dist/file-drop-zone/file-drop-zone.js.map +1 -0
- package/dist/file-drop-zone/index.d.ts +2 -0
- package/dist/file-drop-zone/index.js +2 -0
- package/dist/filter-bar/filter-bar.d.ts +121 -0
- package/dist/filter-bar/filter-bar.js +300 -0
- package/dist/filter-bar/filter-bar.js.map +1 -0
- package/dist/filter-bar/index.d.ts +2 -0
- package/dist/filter-bar/index.js +2 -0
- package/dist/floating-button-group/floating-button-group.d.ts +61 -0
- package/dist/floating-button-group/floating-button-group.js +340 -0
- package/dist/floating-button-group/floating-button-group.js.map +1 -0
- package/dist/floating-button-group/index.d.ts +2 -0
- package/dist/floating-button-group/index.js +2 -0
- package/dist/icon-morph/icon-morph.d.ts +55 -0
- package/dist/icon-morph/icon-morph.js +331 -0
- package/dist/icon-morph/icon-morph.js.map +1 -0
- package/dist/icon-morph/index.d.ts +2 -0
- package/dist/icon-morph/index.js +2 -0
- package/dist/index.d.ts +24 -2
- package/dist/index.js +29 -7
- package/dist/input/input.d.ts +23 -1
- package/dist/input/input.js +37 -13
- package/dist/input/input.js.map +1 -1
- package/dist/label/label.d.ts +1 -1
- package/dist/label/label.js +4 -2
- package/dist/label/label.js.map +1 -1
- package/dist/lib/count-to.js +113 -0
- package/dist/lib/count-to.js.map +1 -0
- package/dist/lib/eye-morph.js +62 -0
- package/dist/lib/eye-morph.js.map +1 -0
- package/dist/lib/field-message.js +336 -0
- package/dist/lib/field-message.js.map +1 -0
- package/dist/lib/field-shell.js +81 -0
- package/dist/lib/field-shell.js.map +1 -0
- package/dist/lib/field-surface.js +4 -2
- package/dist/lib/field-surface.js.map +1 -1
- package/dist/lib/flip.js +80 -0
- package/dist/lib/flip.js.map +1 -0
- package/dist/lib/height-follow.js +59 -0
- package/dist/lib/height-follow.js.map +1 -0
- package/dist/lib/icon-swap.js +47 -0
- package/dist/lib/icon-swap.js.map +1 -0
- package/dist/lib/leaving-list.js +57 -0
- package/dist/lib/leaving-list.js.map +1 -0
- package/dist/lib/merge-refs.js +22 -0
- package/dist/lib/merge-refs.js.map +1 -0
- package/dist/lib/morph-text.js +128 -0
- package/dist/lib/morph-text.js.map +1 -0
- package/dist/lib/motion.js +162 -0
- package/dist/lib/motion.js.map +1 -0
- package/dist/lib/pausable-timeout.js +67 -0
- package/dist/lib/pausable-timeout.js.map +1 -0
- package/dist/lib/rolling-value.js +31 -0
- package/dist/lib/rolling-value.js.map +1 -0
- package/dist/lib/spring-progress.js +114 -0
- package/dist/lib/spring-progress.js.map +1 -0
- package/dist/lib/svg-id.js +10 -0
- package/dist/lib/svg-id.js.map +1 -0
- package/dist/lib/swap-text.js +77 -0
- package/dist/lib/swap-text.js.map +1 -0
- package/dist/lib/use-copy-feedback.js +36 -0
- package/dist/lib/use-copy-feedback.js.map +1 -0
- package/dist/menu/menu.d.ts +5 -5
- package/dist/menu/menu.js +10 -6
- package/dist/menu/menu.js.map +1 -1
- package/dist/page-header/index.d.ts +2 -0
- package/dist/page-header/index.js +2 -0
- package/dist/page-header/page-header.d.ts +75 -0
- package/dist/page-header/page-header.js +83 -0
- package/dist/page-header/page-header.js.map +1 -0
- package/dist/password-field/index.d.ts +2 -0
- package/dist/password-field/index.js +2 -0
- package/dist/password-field/password-field.d.ts +37 -0
- package/dist/password-field/password-field.js +101 -0
- package/dist/password-field/password-field.js.map +1 -0
- package/dist/password-strength/index.d.ts +2 -0
- package/dist/password-strength/index.js +2 -0
- package/dist/password-strength/password-strength.d.ts +127 -0
- package/dist/password-strength/password-strength.js +455 -0
- package/dist/password-strength/password-strength.js.map +1 -0
- package/dist/popover/popover.d.ts +10 -2
- package/dist/popover/popover.js +6 -4
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/progress.d.ts +2 -2
- package/dist/progress/progress.js +50 -7
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/radio-group.d.ts +3 -3
- package/dist/radio-group/radio-group.js +14 -10
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/scroll-area/scroll-area.d.ts +17 -6
- package/dist/scroll-area/scroll-area.js +30 -6
- package/dist/scroll-area/scroll-area.js.map +1 -1
- package/dist/search-field/index.d.ts +2 -0
- package/dist/search-field/index.js +2 -0
- package/dist/search-field/search-field.d.ts +26 -0
- package/dist/search-field/search-field.js +155 -0
- package/dist/search-field/search-field.js.map +1 -0
- package/dist/select/select.d.ts +3 -3
- package/dist/select/select.js +85 -24
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +1 -1
- package/dist/separator/separator.js +4 -2
- package/dist/separator/separator.js.map +1 -1
- package/dist/sheet/sheet.d.ts +1 -1
- package/dist/sheet/sheet.js +6 -3
- package/dist/sheet/sheet.js.map +1 -1
- package/dist/side-nav/index.d.ts +2 -0
- package/dist/side-nav/index.js +2 -0
- package/dist/side-nav/side-nav.d.ts +72 -0
- package/dist/side-nav/side-nav.js +142 -0
- package/dist/side-nav/side-nav.js.map +1 -0
- package/dist/skeleton/skeleton.d.ts +8 -2
- package/dist/skeleton/skeleton.js +9 -3
- package/dist/skeleton/skeleton.js.map +1 -1
- package/dist/slider/slider.d.ts +2 -2
- package/dist/slider/slider.js +64 -8
- package/dist/slider/slider.js.map +1 -1
- package/dist/spinner/spinner.d.ts +1 -1
- package/dist/spinner/spinner.js +4 -2
- package/dist/spinner/spinner.js.map +1 -1
- package/dist/split-button/index.d.ts +2 -0
- package/dist/split-button/index.js +2 -0
- package/dist/split-button/split-button.d.ts +46 -0
- package/dist/split-button/split-button.js +122 -0
- package/dist/split-button/split-button.js.map +1 -0
- package/dist/stat-card/index.d.ts +2 -0
- package/dist/stat-card/index.js +2 -0
- package/dist/stat-card/stat-card.d.ts +81 -0
- package/dist/stat-card/stat-card.js +152 -0
- package/dist/stat-card/stat-card.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/switch/switch.d.ts +2 -2
- package/dist/switch/switch.js +11 -8
- 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 +38 -7
- package/dist/table/table.js +114 -12
- package/dist/table/table.js.map +1 -1
- package/dist/tabs/tabs.d.ts +4 -4
- package/dist/tabs/tabs.js +153 -12
- package/dist/tabs/tabs.js.map +1 -1
- package/dist/textarea/textarea.d.ts +24 -1
- package/dist/textarea/textarea.js +27 -3
- package/dist/textarea/textarea.js.map +1 -1
- package/dist/theme-switch/index.d.ts +2 -0
- package/dist/theme-switch/index.js +2 -0
- package/dist/theme-switch/theme-switch.d.ts +37 -0
- package/dist/theme-switch/theme-switch.js +123 -0
- package/dist/theme-switch/theme-switch.js.map +1 -0
- package/dist/time-picker/index.d.ts +2 -0
- package/dist/time-picker/index.js +2 -0
- package/dist/time-picker/time-picker.d.ts +54 -0
- package/dist/time-picker/time-picker.js +201 -0
- package/dist/time-picker/time-picker.js.map +1 -0
- package/dist/toast/toast.d.ts +2 -2
- package/dist/toast/toast.js +61 -39
- package/dist/toast/toast.js.map +1 -1
- package/dist/tooltip/tooltip.d.ts +3 -3
- package/dist/tooltip/tooltip.js +66 -15
- package/dist/tooltip/tooltip.js.map +1 -1
- package/package.json +83 -3
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn, cnAll } from "../lib/cn.js";
|
|
3
|
+
import { useIsomorphicLayoutEffect } from "../lib/motion.js";
|
|
4
|
+
import { labelVariants } from "../label/label.js";
|
|
5
|
+
import { RollingValue } from "../lib/rolling-value.js";
|
|
6
|
+
import { selectTriggerVariants, selectVariants } from "../select/select.js";
|
|
7
|
+
import { cva } from "class-variance-authority";
|
|
8
|
+
import { forwardRef, useMemo, useRef, useState } from "react";
|
|
9
|
+
import { Button, Label, ListBox, ListBoxItem, Popover, Select, SelectValue, Text, useLocale } from "react-aria-components";
|
|
10
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { Time } from "@internationalized/date";
|
|
12
|
+
//#region src/time-picker/time-picker.tsx
|
|
13
|
+
/**
|
|
14
|
+
* TimePicker, a field that shows a time and opens a list of times to pick from.
|
|
15
|
+
*
|
|
16
|
+
* Values are `TimeValue` objects from `@internationalized/date`, as Calendar and
|
|
17
|
+
* DatePicker use `DateValue` (ADR 0006), not "HH:MM" strings: a time of day has
|
|
18
|
+
* no time zone or format of its own. Unlike those two, this one builds a `Time`
|
|
19
|
+
* at runtime to hand back, so the package is a real (peer) dependency here and
|
|
20
|
+
* not only a type.
|
|
21
|
+
*
|
|
22
|
+
* The control is a React Aria `Select`, so the list is a listbox with typeahead,
|
|
23
|
+
* arrow keys and a roving highlight; the motion (the value rolling like a clock
|
|
24
|
+
* face, the chevron, the list growing from the field) sits on top of it.
|
|
25
|
+
*/
|
|
26
|
+
/** The list: it grows from the field, and the highlight follows the pointer and the arrows. */
|
|
27
|
+
const timePickerListVariants = cva([
|
|
28
|
+
"picker-pop w-(--trigger-width) origin-top overflow-hidden",
|
|
29
|
+
"[--pop-in-y:-6px] [--pop-in-scale:0.97] [--pop-out-y:-4px] [--pop-out-scale:0.98] [--pop-out-duration:120ms]",
|
|
30
|
+
"rounded-md border-(length:--border-width) border-border p-1",
|
|
31
|
+
"bg-card text-card-foreground shadow-floating",
|
|
32
|
+
"box-border font-sans text-sm leading-(--leading-normal)"
|
|
33
|
+
]);
|
|
34
|
+
/** A time. The highlight changes almost at once (the arrows move it often); a dot marks the chosen one. */
|
|
35
|
+
const timePickerItemVariants = cva([
|
|
36
|
+
"relative flex min-h-9 cursor-pointer select-none items-center justify-between gap-2",
|
|
37
|
+
"rounded-2xs px-3 py-1.5 tabular-nums outline-none",
|
|
38
|
+
"text-sm leading-(--leading-normal) text-card-foreground",
|
|
39
|
+
"data-[focused]:bg-muted",
|
|
40
|
+
"motion-safe:transition-colors motion-safe:duration-80 motion-safe:ease-settle",
|
|
41
|
+
"data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width)",
|
|
42
|
+
"data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring)",
|
|
43
|
+
"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)"
|
|
44
|
+
]);
|
|
45
|
+
const descriptionClass = "text-sm leading-(--leading-normal) text-muted-foreground";
|
|
46
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
47
|
+
const keyOf = (minutes) => `${pad(Math.floor(minutes / 60))}:${pad(minutes % 60)}`;
|
|
48
|
+
const minutesOf = (value) => value.hour * 60 + value.minute;
|
|
49
|
+
function ClockGlyph() {
|
|
50
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
51
|
+
"aria-hidden": "true",
|
|
52
|
+
viewBox: "0 0 16 16",
|
|
53
|
+
fill: "none",
|
|
54
|
+
stroke: "currentColor",
|
|
55
|
+
strokeWidth: "1.5",
|
|
56
|
+
strokeLinecap: "round",
|
|
57
|
+
strokeLinejoin: "round",
|
|
58
|
+
className: "text-muted-foreground",
|
|
59
|
+
children: [/* @__PURE__ */ jsx("circle", {
|
|
60
|
+
cx: "8",
|
|
61
|
+
cy: "8",
|
|
62
|
+
r: "5.5"
|
|
63
|
+
}), /* @__PURE__ */ jsx("path", { d: "M8 5v3l2 1.5" })]
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** The chevron turns half a turn while the list is open, on the gentle spring. */
|
|
67
|
+
function Chevron() {
|
|
68
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
69
|
+
"aria-hidden": "true",
|
|
70
|
+
viewBox: "0 0 16 16",
|
|
71
|
+
fill: "none",
|
|
72
|
+
stroke: "currentColor",
|
|
73
|
+
strokeWidth: "1.75",
|
|
74
|
+
strokeLinecap: "round",
|
|
75
|
+
strokeLinejoin: "round",
|
|
76
|
+
className: "text-muted-foreground motion-safe:transition-[rotate] motion-safe:duration-(--duration-gentle) motion-safe:ease-gentle group-aria-expanded/trigger:rotate-180",
|
|
77
|
+
children: /* @__PURE__ */ jsx("path", { d: "m4 6 4 4 4-4" })
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The list of times. It opens centred on the chosen time (or at the top with
|
|
82
|
+
* none), before the first frame, so there is no scroll to watch; the arrow keys
|
|
83
|
+
* then keep the highlight in view, which React Aria does.
|
|
84
|
+
*/
|
|
85
|
+
function CenteredListBox({ children }) {
|
|
86
|
+
const ref = useRef(null);
|
|
87
|
+
useIsomorphicLayoutEffect(() => {
|
|
88
|
+
const list = ref.current;
|
|
89
|
+
const chosen = list?.querySelector("[role=\"option\"][aria-selected=\"true\"]");
|
|
90
|
+
if (!list || !chosen) return;
|
|
91
|
+
list.scrollTop = chosen.offsetTop - (list.clientHeight - chosen.offsetHeight) / 2;
|
|
92
|
+
}, []);
|
|
93
|
+
return /* @__PURE__ */ jsx(ListBox, {
|
|
94
|
+
ref,
|
|
95
|
+
"data-slot": "time-picker-list",
|
|
96
|
+
className: "relative max-h-62.5 overflow-y-auto overscroll-contain outline-none",
|
|
97
|
+
children
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A time picker.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* <TimePicker label="Start time" value={time} onChange={setTime} minuteStep={30} />
|
|
105
|
+
*/
|
|
106
|
+
const TimePicker = forwardRef(function TimePicker({ label, labelHidden, description, placeholder = "Select a time", value, defaultValue = null, onChange, minuteStep = 15, hourCycle = 12, size = "md", isDisabled, className }, ref) {
|
|
107
|
+
const { locale } = useLocale();
|
|
108
|
+
const [inner, setInner] = useState(defaultValue);
|
|
109
|
+
const current = value === void 0 ? inner : value;
|
|
110
|
+
const minutes = current ? minutesOf(current) : null;
|
|
111
|
+
const key = minutes === null ? null : keyOf(minutes);
|
|
112
|
+
const display = useMemo(() => {
|
|
113
|
+
const format = new Intl.DateTimeFormat(locale, {
|
|
114
|
+
hour: "numeric",
|
|
115
|
+
minute: "2-digit",
|
|
116
|
+
hourCycle: hourCycle === 24 ? "h23" : "h12",
|
|
117
|
+
timeZone: "UTC"
|
|
118
|
+
});
|
|
119
|
+
return (total) => format.format(new Date(Date.UTC(2e3, 0, 1, Math.floor(total / 60), total % 60))).replace(/\u202f/g, " ");
|
|
120
|
+
}, [locale, hourCycle]);
|
|
121
|
+
const options = useMemo(() => {
|
|
122
|
+
const all = Array.from({ length: Math.ceil(1440 / minuteStep) }, (_, i) => i * minuteStep);
|
|
123
|
+
if (minutes !== null && !all.includes(minutes)) all.push(minutes);
|
|
124
|
+
return all.sort((a, b) => a - b);
|
|
125
|
+
}, [minuteStep, minutes]);
|
|
126
|
+
const [previous, setPrevious] = useState(minutes);
|
|
127
|
+
const [dir, setDir] = useState(1);
|
|
128
|
+
if (previous !== minutes) {
|
|
129
|
+
setPrevious(minutes);
|
|
130
|
+
setDir(minutes !== null && previous !== null && minutes < previous ? -1 : 1);
|
|
131
|
+
}
|
|
132
|
+
return /* @__PURE__ */ jsxs(Select, {
|
|
133
|
+
"data-slot": "time-picker",
|
|
134
|
+
selectedKey: key,
|
|
135
|
+
onSelectionChange: (next) => {
|
|
136
|
+
if (next == null) return;
|
|
137
|
+
const [hour = 0, minute = 0] = String(next).split(":").map(Number);
|
|
138
|
+
const time = new Time(hour, minute);
|
|
139
|
+
if (value === void 0) setInner(time);
|
|
140
|
+
onChange?.(time);
|
|
141
|
+
},
|
|
142
|
+
isDisabled,
|
|
143
|
+
placeholder,
|
|
144
|
+
className: cn(selectVariants(), className),
|
|
145
|
+
children: [
|
|
146
|
+
/* @__PURE__ */ jsx(Label, {
|
|
147
|
+
"data-slot": "time-picker-label",
|
|
148
|
+
className: cnAll(labelVariants(), labelHidden && "sr-only"),
|
|
149
|
+
children: label
|
|
150
|
+
}),
|
|
151
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
152
|
+
ref,
|
|
153
|
+
"data-slot": "time-picker-trigger",
|
|
154
|
+
className: selectTriggerVariants({ size }),
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ jsx(ClockGlyph, {}),
|
|
157
|
+
/* @__PURE__ */ jsx(RollingValue, {
|
|
158
|
+
srValue: /* @__PURE__ */ jsx(SelectValue, {
|
|
159
|
+
"data-slot": "time-picker-value",
|
|
160
|
+
className: "sr-only",
|
|
161
|
+
children: ({ isPlaceholder, selectedText }) => isPlaceholder ? placeholder : selectedText
|
|
162
|
+
}),
|
|
163
|
+
swapKey: key ?? "placeholder",
|
|
164
|
+
dir: key === null ? -1 : dir,
|
|
165
|
+
children: minutes === null ? /* @__PURE__ */ jsx("span", {
|
|
166
|
+
"data-slot": "time-picker-placeholder",
|
|
167
|
+
className: "text-muted-foreground",
|
|
168
|
+
children: placeholder
|
|
169
|
+
}) : display(minutes)
|
|
170
|
+
}),
|
|
171
|
+
/* @__PURE__ */ jsx(Chevron, {})
|
|
172
|
+
]
|
|
173
|
+
}),
|
|
174
|
+
description ? /* @__PURE__ */ jsx(Text, {
|
|
175
|
+
slot: "description",
|
|
176
|
+
"data-slot": "time-picker-description",
|
|
177
|
+
className: descriptionClass,
|
|
178
|
+
children: description
|
|
179
|
+
}) : null,
|
|
180
|
+
/* @__PURE__ */ jsx(Popover, {
|
|
181
|
+
"data-slot": "time-picker-popover",
|
|
182
|
+
className: timePickerListVariants(),
|
|
183
|
+
children: /* @__PURE__ */ jsx(CenteredListBox, { children: options.map((total) => /* @__PURE__ */ jsx(ListBoxItem, {
|
|
184
|
+
id: keyOf(total),
|
|
185
|
+
textValue: display(total),
|
|
186
|
+
"data-slot": "time-picker-item",
|
|
187
|
+
className: timePickerItemVariants(),
|
|
188
|
+
children: ({ isSelected }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [display(total), isSelected ? /* @__PURE__ */ jsx("span", {
|
|
189
|
+
"aria-hidden": "true",
|
|
190
|
+
"data-slot": "time-picker-dot",
|
|
191
|
+
className: "size-1.5 rounded-full bg-foreground"
|
|
192
|
+
}) : null] })
|
|
193
|
+
}, keyOf(total))) })
|
|
194
|
+
})
|
|
195
|
+
]
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
//#endregion
|
|
199
|
+
export { TimePicker, timePickerItemVariants, timePickerListVariants };
|
|
200
|
+
|
|
201
|
+
//# sourceMappingURL=time-picker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-picker.js","names":["AriaListBox","AriaSelect","AriaLabel","AriaButton","AriaSelectValue","AriaText","AriaPopover","AriaListBoxItem"],"sources":["../../src/time-picker/time-picker.tsx"],"sourcesContent":["\"use client\";\n\nimport { Time } from \"@internationalized/date\";\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useMemo, useRef, useState } from \"react\";\nimport {\n Button as AriaButton,\n Label as AriaLabel,\n ListBox as AriaListBox,\n ListBoxItem as AriaListBoxItem,\n Popover as AriaPopover,\n Select as AriaSelect,\n SelectValue as AriaSelectValue,\n Text as AriaText,\n useLocale,\n type TimeValue,\n} from \"react-aria-components\";\n\nimport { labelVariants } from \"../label/label.js\";\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { useIsomorphicLayoutEffect } from \"../lib/motion.js\";\nimport { RollingValue } from \"../lib/rolling-value.js\";\nimport { selectTriggerVariants, selectVariants } from \"../select/select.js\";\n\n/**\n * TimePicker, a field that shows a time and opens a list of times to pick from.\n *\n * Values are `TimeValue` objects from `@internationalized/date`, as Calendar and\n * DatePicker use `DateValue` (ADR 0006), not \"HH:MM\" strings: a time of day has\n * no time zone or format of its own. Unlike those two, this one builds a `Time`\n * at runtime to hand back, so the package is a real (peer) dependency here and\n * not only a type.\n *\n * The control is a React Aria `Select`, so the list is a listbox with typeahead,\n * arrow keys and a roving highlight; the motion (the value rolling like a clock\n * face, the chevron, the list growing from the field) sits on top of it.\n */\n\n/** The list: it grows from the field, and the highlight follows the pointer and the arrows. */\nexport const timePickerListVariants = cva([\n \"picker-pop w-(--trigger-width) origin-top overflow-hidden\",\n // A little less offset than a date picker: this list sits close under its field.\n \"[--pop-in-y:-6px] [--pop-in-scale:0.97] [--pop-out-y:-4px] [--pop-out-scale:0.98] [--pop-out-duration:120ms]\",\n \"rounded-md border-(length:--border-width) border-border p-1\",\n \"bg-card text-card-foreground shadow-floating\",\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n]);\n\n/** A time. The highlight changes almost at once (the arrows move it often); a dot marks the chosen one. */\nexport const timePickerItemVariants = cva([\n \"relative flex min-h-9 cursor-pointer select-none items-center justify-between gap-2\",\n \"rounded-2xs px-3 py-1.5 tabular-nums outline-none\",\n \"text-sm leading-(--leading-normal) text-card-foreground\",\n \"data-[focused]:bg-muted\",\n \"motion-safe:transition-colors motion-safe:duration-80 motion-safe:ease-settle\",\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\nconst descriptionClass = \"text-sm leading-(--leading-normal) text-muted-foreground\";\n\nconst pad = (value: number) => String(value).padStart(2, \"0\");\nconst keyOf = (minutes: number) => `${pad(Math.floor(minutes / 60))}:${pad(minutes % 60)}`;\nconst minutesOf = (value: TimeValue) => value.hour * 60 + value.minute;\n\nfunction ClockGlyph() {\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 <circle cx=\"8\" cy=\"8\" r=\"5.5\" />\n <path d=\"M8 5v3l2 1.5\" />\n </svg>\n );\n}\n\n/** The chevron turns half a turn while the list is open, on the gentle spring. */\nfunction 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\n/**\n * The list of times. It opens centred on the chosen time (or at the top with\n * none), before the first frame, so there is no scroll to watch; the arrow keys\n * then keep the highlight in view, which React Aria does.\n */\nfunction CenteredListBox({ children }: { children: React.ReactNode }) {\n const ref = useRef<HTMLDivElement>(null);\n useIsomorphicLayoutEffect(() => {\n const list = ref.current;\n const chosen = list?.querySelector<HTMLElement>('[role=\"option\"][aria-selected=\"true\"]');\n if (!list || !chosen) return;\n list.scrollTop = chosen.offsetTop - (list.clientHeight - chosen.offsetHeight) / 2;\n }, []);\n return (\n <AriaListBox\n ref={ref}\n data-slot=\"time-picker-list\"\n className=\"relative max-h-62.5 overflow-y-auto overscroll-contain outline-none\"\n >\n {children}\n </AriaListBox>\n );\n}\n\nexport interface TimePickerProps {\n /** What is being chosen, and the control's accessible name. Required, as on Select. */\n label: string;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /** Help text under the control, named in `aria-describedby`. */\n description?: string;\n /** Shown while no time is chosen. @default \"Select a time\" */\n placeholder?: string;\n /** The chosen time. Controlled: pass `null` for none. */\n value?: TimeValue | null;\n /** The starting time when uncontrolled. @default null */\n defaultValue?: TimeValue | null;\n /** Called with the chosen time, as a `Time`. */\n onChange?: (value: TimeValue) => void;\n /** Minutes between the times in the list. @default 15 */\n minuteStep?: 1 | 5 | 10 | 15 | 30;\n /** 12-hour (9:00 AM) or 24-hour (09:00) times. @default 12 */\n hourCycle?: 12 | 24;\n /** Control scale of the trigger. @default \"md\" */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Disable the control. */\n isDisabled?: boolean;\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\n/**\n * A time picker.\n *\n * @example\n * <TimePicker label=\"Start time\" value={time} onChange={setTime} minuteStep={30} />\n */\nexport const TimePicker = forwardRef<HTMLButtonElement, TimePickerProps>(function TimePicker(\n {\n label,\n labelHidden,\n description,\n placeholder = \"Select a time\",\n value,\n defaultValue = null,\n onChange,\n minuteStep = 15,\n hourCycle = 12,\n size = \"md\",\n isDisabled,\n className,\n },\n ref,\n) {\n const { locale } = useLocale();\n const [inner, setInner] = useState<TimeValue | null>(defaultValue);\n const current = value === undefined ? inner : value;\n const minutes = current ? minutesOf(current) : null;\n const key = minutes === null ? null : keyOf(minutes);\n\n const display = useMemo(() => {\n const format = new Intl.DateTimeFormat(locale, {\n hour: \"numeric\",\n minute: \"2-digit\",\n hourCycle: hourCycle === 24 ? \"h23\" : \"h12\",\n timeZone: \"UTC\",\n });\n return (total: number) =>\n format\n .format(new Date(Date.UTC(2000, 0, 1, Math.floor(total / 60), total % 60)))\n .replace(/\\u202f/g, \" \");\n }, [locale, hourCycle]);\n\n // The list: every step of the day, plus the current time if it falls between steps.\n const options = useMemo(() => {\n const all = Array.from({ length: Math.ceil(1440 / minuteStep) }, (_, i) => i * minuteStep);\n if (minutes !== null && !all.includes(minutes)) all.push(minutes);\n return all.sort((a, b) => a - b);\n }, [minuteStep, minutes]);\n\n // Which way the time moved, so the shown value rolls like a clock face.\n const [previous, setPrevious] = useState(minutes);\n const [dir, setDir] = useState(1);\n if (previous !== minutes) {\n setPrevious(minutes);\n setDir(minutes !== null && previous !== null && minutes < previous ? -1 : 1);\n }\n\n return (\n <AriaSelect\n data-slot=\"time-picker\"\n selectedKey={key}\n onSelectionChange={(next) => {\n if (next == null) return;\n const [hour = 0, minute = 0] = String(next).split(\":\").map(Number);\n const time = new Time(hour, minute);\n if (value === undefined) setInner(time);\n onChange?.(time);\n }}\n isDisabled={isDisabled}\n placeholder={placeholder}\n className={cn(selectVariants(), className)}\n >\n <AriaLabel\n data-slot=\"time-picker-label\"\n className={cnAll(labelVariants(), labelHidden && \"sr-only\")}\n >\n {label}\n </AriaLabel>\n <AriaButton ref={ref} data-slot=\"time-picker-trigger\" className={selectTriggerVariants({ size })}>\n <ClockGlyph />\n <RollingValue\n srValue={\n <AriaSelectValue data-slot=\"time-picker-value\" className=\"sr-only\">\n {({ isPlaceholder, selectedText }) => (isPlaceholder ? placeholder : selectedText)}\n </AriaSelectValue>\n }\n swapKey={key ?? \"placeholder\"}\n dir={key === null ? -1 : dir}\n >\n {minutes === null ? (\n <span data-slot=\"time-picker-placeholder\" className=\"text-muted-foreground\">\n {placeholder}\n </span>\n ) : (\n display(minutes)\n )}\n </RollingValue>\n <Chevron />\n </AriaButton>\n {description ? (\n <AriaText slot=\"description\" data-slot=\"time-picker-description\" className={descriptionClass}>\n {description}\n </AriaText>\n ) : null}\n <AriaPopover data-slot=\"time-picker-popover\" className={timePickerListVariants()}>\n <CenteredListBox>\n {options.map((total) => (\n <AriaListBoxItem\n key={keyOf(total)}\n id={keyOf(total)}\n textValue={display(total)}\n data-slot=\"time-picker-item\"\n className={timePickerItemVariants()}\n >\n {({ isSelected }) => (\n <>\n {display(total)}\n {isSelected ? (\n <span\n aria-hidden=\"true\"\n data-slot=\"time-picker-dot\"\n className=\"size-1.5 rounded-full bg-foreground\"\n />\n ) : null}\n </>\n )}\n </AriaListBoxItem>\n ))}\n </CenteredListBox>\n </AriaPopover>\n </AriaSelect>\n );\n});\n"],"mappings":"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"}
|
package/dist/toast/toast.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { VariantProps } from "class-variance-authority";
|
|
2
|
-
import { UNSTABLE_ToastQueue } from "react-aria-components";
|
|
3
2
|
import { ReactNode } from "react";
|
|
3
|
+
import { UNSTABLE_ToastQueue } from "react-aria-components";
|
|
4
4
|
//#region src/toast/toast.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Toast: a transient message, announced without stealing focus.
|
|
@@ -88,6 +88,6 @@ export interface ToastRegionProps {
|
|
|
88
88
|
* React Aria only renders the landmark while something is queued, so this
|
|
89
89
|
* costs nothing on a quiet screen.
|
|
90
90
|
*/
|
|
91
|
-
export declare
|
|
91
|
+
export declare const ToastRegion: import("react").ForwardRefExoticComponent<ToastRegionProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
92
92
|
//#endregion
|
|
93
93
|
//# sourceMappingURL=toast.d.ts.map
|
package/dist/toast/toast.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { useExitBackstop } from "../lib/motion.js";
|
|
3
4
|
import { cva } from "class-variance-authority";
|
|
5
|
+
import { forwardRef, useCallback, useState } from "react";
|
|
4
6
|
import { Button, Text, UNSTABLE_Toast, UNSTABLE_ToastContent, UNSTABLE_ToastQueue, UNSTABLE_ToastRegion } from "react-aria-components";
|
|
5
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
8
|
//#region src/toast/toast.tsx
|
|
@@ -44,8 +46,8 @@ const toastVariants = cva([
|
|
|
44
46
|
"flex items-start gap-3 box-border w-full",
|
|
45
47
|
"rounded-md border-(length:--border-width) p-(--density-bar-pad-x)",
|
|
46
48
|
"font-sans shadow-surface",
|
|
47
|
-
"motion-safe:
|
|
48
|
-
"data-[
|
|
49
|
+
"motion-safe:animate-toast-in motion-reduce:animate-fade-in",
|
|
50
|
+
"motion-safe:data-[closing]:animate-toast-out motion-reduce:data-[closing]:animate-fade-out"
|
|
49
51
|
], {
|
|
50
52
|
variants: { tone: {
|
|
51
53
|
neutral: "bg-background border-separator text-foreground",
|
|
@@ -59,6 +61,7 @@ const toastVariants = cva([
|
|
|
59
61
|
const toastCloseVariants = cva([
|
|
60
62
|
"ms-auto shrink-0 rounded-sm cursor-pointer",
|
|
61
63
|
"text-current opacity-70 data-[hovered]:opacity-100",
|
|
64
|
+
"toast-close",
|
|
62
65
|
"outline-none",
|
|
63
66
|
"data-[focus-visible]:outline-solid",
|
|
64
67
|
"data-[focus-visible]:outline-(length:--focus-ring-width)",
|
|
@@ -90,55 +93,74 @@ function toast(content, timeout = 5e3) {
|
|
|
90
93
|
return toastQueue.add(content, timeout === 0 ? void 0 : { timeout });
|
|
91
94
|
}
|
|
92
95
|
/**
|
|
96
|
+
* One toast. Pressing its close control plays the exit first, then closes it in the
|
|
97
|
+
* queue; React Aria unmounts a closed toast at once and has no exit of its own, so a
|
|
98
|
+
* toast closed any other way (the timeout, `toastQueue.close`) leaves without one.
|
|
99
|
+
*/
|
|
100
|
+
function ToastItem({ queued, closeLabel }) {
|
|
101
|
+
const [closing, setClosing] = useState(false);
|
|
102
|
+
const finish = useCallback(() => toastQueue.close(queued.key), [queued.key]);
|
|
103
|
+
useExitBackstop(closing, finish);
|
|
104
|
+
return /* @__PURE__ */ jsxs(UNSTABLE_Toast, {
|
|
105
|
+
toast: queued,
|
|
106
|
+
"data-slot": "toast",
|
|
107
|
+
"data-tone": queued.content.tone ?? "neutral",
|
|
108
|
+
"data-closing": closing || void 0,
|
|
109
|
+
onAnimationEnd: (event) => {
|
|
110
|
+
if (closing && event.target === event.currentTarget) finish();
|
|
111
|
+
},
|
|
112
|
+
className: toastVariants({ tone: queued.content.tone }),
|
|
113
|
+
children: [/* @__PURE__ */ jsxs(UNSTABLE_ToastContent, {
|
|
114
|
+
"data-slot": "toast-content",
|
|
115
|
+
className: "flex min-w-0 flex-col gap-1",
|
|
116
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
117
|
+
slot: "title",
|
|
118
|
+
"data-slot": "toast-title",
|
|
119
|
+
className: "text-sm leading-(--leading-snug) font-medium",
|
|
120
|
+
children: queued.content.title
|
|
121
|
+
}), queued.content.description == null ? null : /* @__PURE__ */ jsx(Text, {
|
|
122
|
+
slot: "description",
|
|
123
|
+
"data-slot": "toast-description",
|
|
124
|
+
className: "text-sm leading-(--leading-normal) opacity-90",
|
|
125
|
+
children: queued.content.description
|
|
126
|
+
})]
|
|
127
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
128
|
+
"aria-label": closeLabel,
|
|
129
|
+
"data-slot": "toast-close",
|
|
130
|
+
className: toastCloseVariants(),
|
|
131
|
+
onPress: () => setClosing(true),
|
|
132
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
133
|
+
"aria-hidden": "true",
|
|
134
|
+
viewBox: "0 0 16 16",
|
|
135
|
+
fill: "none",
|
|
136
|
+
stroke: "currentColor",
|
|
137
|
+
strokeWidth: "1.75",
|
|
138
|
+
strokeLinecap: "round",
|
|
139
|
+
className: "size-(--icon-sm)",
|
|
140
|
+
children: /* @__PURE__ */ jsx("path", { d: "m4 4 8 8M12 4l-8 8" })
|
|
141
|
+
})
|
|
142
|
+
})]
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
93
146
|
* Renders the queue. Mount ONCE, near the root.
|
|
94
147
|
*
|
|
95
148
|
* React Aria only renders the landmark while something is queued, so this
|
|
96
149
|
* costs nothing on a quiet screen.
|
|
97
150
|
*/
|
|
98
|
-
function ToastRegion({ label, closeLabel = "Dismiss", className }) {
|
|
151
|
+
const ToastRegion = forwardRef(function ToastRegion({ label, closeLabel = "Dismiss", className }, ref) {
|
|
99
152
|
return /* @__PURE__ */ jsx(UNSTABLE_ToastRegion, {
|
|
153
|
+
ref,
|
|
100
154
|
queue: toastQueue,
|
|
101
155
|
"aria-label": label,
|
|
102
156
|
"data-slot": "toast-region",
|
|
103
157
|
className: cn(toastRegionVariants(), className),
|
|
104
|
-
children: ({ toast: queued }) => /* @__PURE__ */
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"data-tone": queued.content.tone ?? "neutral",
|
|
108
|
-
className: toastVariants({ tone: queued.content.tone }),
|
|
109
|
-
children: [/* @__PURE__ */ jsxs(UNSTABLE_ToastContent, {
|
|
110
|
-
"data-slot": "toast-content",
|
|
111
|
-
className: "flex min-w-0 flex-col gap-1",
|
|
112
|
-
children: [/* @__PURE__ */ jsx(Text, {
|
|
113
|
-
slot: "title",
|
|
114
|
-
"data-slot": "toast-title",
|
|
115
|
-
className: "text-sm leading-(--leading-snug) font-medium",
|
|
116
|
-
children: queued.content.title
|
|
117
|
-
}), queued.content.description == null ? null : /* @__PURE__ */ jsx(Text, {
|
|
118
|
-
slot: "description",
|
|
119
|
-
"data-slot": "toast-description",
|
|
120
|
-
className: "text-sm leading-(--leading-normal) opacity-90",
|
|
121
|
-
children: queued.content.description
|
|
122
|
-
})]
|
|
123
|
-
}), /* @__PURE__ */ jsx(Button, {
|
|
124
|
-
slot: "close",
|
|
125
|
-
"aria-label": closeLabel,
|
|
126
|
-
"data-slot": "toast-close",
|
|
127
|
-
className: toastCloseVariants(),
|
|
128
|
-
children: /* @__PURE__ */ jsx("svg", {
|
|
129
|
-
"aria-hidden": "true",
|
|
130
|
-
viewBox: "0 0 16 16",
|
|
131
|
-
fill: "none",
|
|
132
|
-
stroke: "currentColor",
|
|
133
|
-
strokeWidth: "1.75",
|
|
134
|
-
strokeLinecap: "round",
|
|
135
|
-
className: "size-(--icon-sm)",
|
|
136
|
-
children: /* @__PURE__ */ jsx("path", { d: "m4 4 8 8M12 4l-8 8" })
|
|
137
|
-
})
|
|
138
|
-
})]
|
|
158
|
+
children: ({ toast: queued }) => /* @__PURE__ */ jsx(ToastItem, {
|
|
159
|
+
queued,
|
|
160
|
+
closeLabel
|
|
139
161
|
})
|
|
140
162
|
});
|
|
141
|
-
}
|
|
163
|
+
});
|
|
142
164
|
//#endregion
|
|
143
165
|
export { ToastRegion, toast, toastCloseVariants, toastQueue, toastRegionVariants, toastVariants };
|
|
144
166
|
|
package/dist/toast/toast.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toast.js","names":["AriaToastQueue","AriaToastRegion","AriaToast","AriaToastContent","AriaText","AriaButton"],"sources":["../../src/toast/toast.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Text as AriaText,\n UNSTABLE_Toast as AriaToast,\n UNSTABLE_ToastContent as AriaToastContent,\n UNSTABLE_ToastQueue as AriaToastQueue,\n UNSTABLE_ToastRegion as AriaToastRegion,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Toast: a transient message, announced without stealing focus.\n *\n * Maturity: **experimental**, and the reason is upstream rather than here.\n * React Aria ships this API as `UNSTABLE_Toast*` in 1.21.1, so a RAC minor\n * bump can rename or reshape it. The alternative was hand-rolling the live\n * region, the queue, the timers and the focus behaviour, which is how every\n * toast implementation acquires its accessibility bugs -- so this takes the\n * upstream one and labels the risk instead of hiding it.\n *\n * THE PRODUCT HAS 30 FILES CALLING `useToast` AND 67 `toast(...)` CALLS, and\n * its implementation is the shadcn reducer with both of its known defects\n * still in place:\n *\n * TOAST_LIMIT = 1 -- a second toast REPLACES the first, so a\n * burst of results shows only the last one.\n * TOAST_REMOVE_DELAY = 1e6 -- sixteen minutes. Nothing auto-dismisses;\n * every toast waits to be closed by hand.\n *\n * Neither is ported. `maxVisibleToasts` defaults to 3 and `timeout` to 5s,\n * which is the WCAG 2.2.1 floor React Aria enforces -- it REFUSES a timeout\n * under 5000ms, because a message that vanishes faster cannot be read by\n * someone using a screen magnifier.\n *\n * The queue is a module singleton on purpose. The product's `toast()` is\n * importable from anywhere and called from API error handlers and effects\n * that have no component to hang a hook off, and a context-based queue would\n * have made those 67 call sites into 67 refactors.\n */\n\n/** Where the region sits. Fixed, above everything, out of the way of content. */\nexport const toastRegionVariants = cva([\n \"fixed z-50 flex flex-col gap-2 box-border\",\n // Logical insets so the stack follows the writing direction.\n \"bottom-(--density-bar-pad-y) end-(--density-bar-pad-x)\",\n \"w-[min(24rem,calc(100vw-2rem))]\",\n \"outline-none\",\n]);\n\nexport const toastVariants = cva(\n [\n \"flex items-start gap-3 box-border w-full\",\n \"rounded-md border-(length:--border-width) p-(--density-bar-pad-x)\",\n \"font-sans shadow-surface\",\n \"motion-safe:transition-opacity motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n // Not colour alone: an exiting toast fades AND the region reflows.\n \"data-[animation=exiting]:opacity-0\",\n ],\n {\n variants: {\n /*\n * The same vocabulary as Badge, deliberately. The product's toast had\n * `default` and `destructive` only, so 38 of its 67 calls pass a variant\n * that cannot say \"this worked\" -- and a save confirmation styled\n * identically to a failure is the most common thing a toast gets wrong.\n */\n tone: {\n neutral: \"bg-background border-separator text-foreground\",\n success: \"bg-success-subtle border-success-strong text-success-foreground\",\n warning: \"bg-warning-subtle border-warning-strong text-warning-foreground\",\n danger:\n \"bg-destructive-subtle border-destructive-strong text-destructive-subtle-foreground\",\n info: \"bg-info-subtle border-info-strong text-info-foreground\",\n },\n },\n defaultVariants: { tone: \"neutral\" },\n },\n);\n\nexport const toastCloseVariants = cva([\n \"ms-auto shrink-0 rounded-sm cursor-pointer\",\n \"text-current opacity-70 data-[hovered]:opacity-100\",\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]);\n\n/** What a toast carries. `title` is required: an unnamed toast announces nothing. */\nexport interface ToastContent extends VariantProps<typeof toastVariants> {\n /** The message. Required -- a toast with only a description announces as loose text. */\n title: ReactNode;\n /** Secondary detail, beneath the title. */\n description?: ReactNode;\n}\n\n/**\n * The queue every `toast()` call feeds.\n *\n * Exported so a consumer can read `visibleToasts` in a test, or dismiss them\n * on a route change -- there is no `clear()` upstream, so that is\n * `[...toastQueue.visibleToasts].forEach((t) => toastQueue.close(t.key))`.\n * Not something to touch in normal use.\n */\nexport const toastQueue = new AriaToastQueue<ToastContent>({\n /*\n * Three, against the product's one. Its TOAST_LIMIT of 1 means a second\n * toast REPLACES the first -- so a page that reports three failed uploads\n * shows one and silently drops the rest.\n */\n maxVisibleToasts: 3,\n});\n\n/**\n * Show a toast.\n *\n * @example\n * toast({ title: \"Report saved\" });\n * toast({ title: \"Upload failed\", description: err.message, tone: \"danger\" });\n *\n * @param timeout - ms before it dismisses itself. React Aria REFUSES anything\n * under 5000 and anything at all while the region is focused, because a\n * message that disappears before it can be read fails WCAG 2.2.1. Pass 0 to\n * require a manual dismiss, which is right for an error a user must act on.\n */\nexport function toast(content: ToastContent, timeout = 5000) {\n return toastQueue.add(content, timeout === 0 ? undefined : { timeout });\n}\n\nexport interface ToastRegionProps {\n /**\n * Names the region for assistive tech.\n *\n * **Required.** React Aria renders the region as a labelled landmark, and\n * an unnamed one is announced as \"region\" with no indication of what just\n * arrived. It is a prop rather than a default string because\n * ARCHITECTURE section 7 rule 9 routes user-visible copy through the\n * consumer until `useMessages()` exists.\n */\n label: string;\n /** Accessible name for each toast's dismiss control. */\n closeLabel?: string;\n /** Extra classes for the region. Merged last. */\n className?: string;\n}\n\n/**\n * Renders the queue. Mount ONCE, near the root.\n *\n * React Aria only renders the landmark while something is queued, so this\n * costs nothing on a quiet screen.\n */\nexport function ToastRegion({ label, closeLabel = \"Dismiss\", className }: ToastRegionProps) {\n return (\n <AriaToastRegion\n queue={toastQueue}\n aria-label={label}\n data-slot=\"toast-region\"\n className={cn(toastRegionVariants(), className)}\n >\n {({ toast: queued }) => (\n <AriaToast\n toast={queued}\n data-slot=\"toast\"\n data-tone={queued.content.tone ?? \"neutral\"}\n className={toastVariants({ tone: queued.content.tone })}\n >\n <AriaToastContent data-slot=\"toast-content\" className=\"flex min-w-0 flex-col gap-1\">\n {/*\n slot=\"title\" is what React Aria announces. A plain div here\n renders identically and leaves the toast unnamed.\n */}\n <AriaText\n slot=\"title\"\n data-slot=\"toast-title\"\n className=\"text-sm leading-(--leading-snug) font-medium\"\n >\n {queued.content.title}\n </AriaText>\n {queued.content.description == null ? null : (\n <AriaText\n slot=\"description\"\n data-slot=\"toast-description\"\n className=\"text-sm leading-(--leading-normal) opacity-90\"\n >\n {queued.content.description}\n </AriaText>\n )}\n </AriaToastContent>\n <AriaButton\n slot=\"close\"\n aria-label={closeLabel}\n data-slot=\"toast-close\"\n className={toastCloseVariants()}\n >\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 className=\"size-(--icon-sm)\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n </AriaToast>\n )}\n </AriaToastRegion>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,MAAa,sBAAsB,IAAI;CACrC;CAEA;CACA;CACA;AACF,CAAC;AAED,MAAa,gBAAgB,IAC3B;CACE;CACA;CACA;CACA;CAEA;AACF,GACA;CACE,UAAU,EAOR,MAAM;EACJ,SAAS;EACT,SAAS;EACT,SAAS;EACT,QACE;EACF,MAAM;CACR,EACF;CACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CACF;AAEA,MAAa,qBAAqB,IAAI;CACpC;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;AAkBD,MAAa,aAAa,IAAIA,oBAA6B,EAMzD,kBAAkB,EACpB,CAAC;;;;;;;;;;;;;AAcD,SAAgB,MAAM,SAAuB,UAAU,KAAM;CAC3D,OAAO,WAAW,IAAI,SAAS,YAAY,IAAI,KAAA,IAAY,EAAE,QAAQ,CAAC;AACxE;;;;;;;AAyBA,SAAgB,YAAY,EAAE,OAAO,aAAa,WAAW,aAA+B;CAC1F,OACE,oBAACC,sBAAD;EACE,OAAO;EACP,cAAY;EACZ,aAAU;EACV,WAAW,GAAG,oBAAoB,GAAG,SAAS;EAE5C,WAAA,EAAE,OAAO,aACT,qBAACC,gBAAD;GACE,OAAO;GACP,aAAU;GACV,aAAW,OAAO,QAAQ,QAAQ;GAClC,WAAW,cAAc,EAAE,MAAM,OAAO,QAAQ,KAAK,CAAC;GAJxD,UAAA,CAME,qBAACC,uBAAD;IAAkB,aAAU;IAAgB,WAAU;IAAtD,UAAA,CAKE,oBAACC,MAAD;KACE,MAAK;KACL,aAAU;KACV,WAAU;KAET,UAAA,OAAO,QAAQ;IACR,CAAA,GACT,OAAO,QAAQ,eAAe,OAAO,OACpC,oBAACA,MAAD;KACE,MAAK;KACL,aAAU;KACV,WAAU;KAET,UAAA,OAAO,QAAQ;IACR,CAAA,CAEI;GAClB,CAAA,GAAA,oBAACC,QAAD;IACE,MAAK;IACL,cAAY;IACZ,aAAU;IACV,WAAW,mBAAmB;IAE9B,UAAA,oBAAC,OAAD;KACE,eAAY;KACZ,SAAQ;KACR,MAAK;KACL,QAAO;KACP,aAAY;KACZ,eAAc;KACd,WAAU;KAEV,UAAA,oBAAC,QAAD,EAAM,GAAE,qBAAsB,CAAA;IAC3B,CAAA;GACK,CAAA,CACH;;CAEE,CAAA;AAErB"}
|
|
1
|
+
{"version":3,"file":"toast.js","names":["AriaToastQueue","AriaToast","AriaToastContent","AriaText","AriaButton","AriaToastRegion"],"sources":["../../src/toast/toast.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { forwardRef, useCallback, useState, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Text as AriaText,\n UNSTABLE_Toast as AriaToast,\n UNSTABLE_ToastContent as AriaToastContent,\n UNSTABLE_ToastQueue as AriaToastQueue,\n UNSTABLE_ToastRegion as AriaToastRegion,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useExitBackstop } from \"../lib/motion.js\";\n\n/**\n * Toast: a transient message, announced without stealing focus.\n *\n * Maturity: **experimental**, and the reason is upstream rather than here.\n * React Aria ships this API as `UNSTABLE_Toast*` in 1.21.1, so a RAC minor\n * bump can rename or reshape it. The alternative was hand-rolling the live\n * region, the queue, the timers and the focus behaviour, which is how every\n * toast implementation acquires its accessibility bugs -- so this takes the\n * upstream one and labels the risk instead of hiding it.\n *\n * THE PRODUCT HAS 30 FILES CALLING `useToast` AND 67 `toast(...)` CALLS, and\n * its implementation is the shadcn reducer with both of its known defects\n * still in place:\n *\n * TOAST_LIMIT = 1 -- a second toast REPLACES the first, so a\n * burst of results shows only the last one.\n * TOAST_REMOVE_DELAY = 1e6 -- sixteen minutes. Nothing auto-dismisses;\n * every toast waits to be closed by hand.\n *\n * Neither is ported. `maxVisibleToasts` defaults to 3 and `timeout` to 5s,\n * which is the WCAG 2.2.1 floor React Aria enforces -- it REFUSES a timeout\n * under 5000ms, because a message that vanishes faster cannot be read by\n * someone using a screen magnifier.\n *\n * The queue is a module singleton on purpose. The product's `toast()` is\n * importable from anywhere and called from API error handlers and effects\n * that have no component to hang a hook off, and a context-based queue would\n * have made those 67 call sites into 67 refactors.\n */\n\n/** Where the region sits. Fixed, above everything, out of the way of content. */\nexport const toastRegionVariants = cva([\n \"fixed z-50 flex flex-col gap-2 box-border\",\n // Logical insets so the stack follows the writing direction.\n \"bottom-(--density-bar-pad-y) end-(--density-bar-pad-x)\",\n \"w-[min(24rem,calc(100vw-2rem))]\",\n \"outline-none\",\n]);\n\nexport const toastVariants = cva(\n [\n \"flex items-start gap-3 box-border w-full\",\n \"rounded-md border-(length:--border-width) p-(--density-bar-pad-x)\",\n \"font-sans shadow-surface\",\n // Rises in when it mounts. When its own close control is pressed it sinks out\n // first (data-closing) and the queue is told when that ends. Reduced motion:\n // opacity only. Not colour alone: the region reflows too.\n \"motion-safe:animate-toast-in motion-reduce:animate-fade-in\",\n \"motion-safe:data-[closing]:animate-toast-out motion-reduce:data-[closing]:animate-fade-out\",\n ],\n {\n variants: {\n /*\n * The same vocabulary as Badge, deliberately. The product's toast had\n * `default` and `destructive` only, so 38 of its 67 calls pass a variant\n * that cannot say \"this worked\" -- and a save confirmation styled\n * identically to a failure is the most common thing a toast gets wrong.\n */\n tone: {\n neutral: \"bg-background border-separator text-foreground\",\n success: \"bg-success-subtle border-success-strong text-success-foreground\",\n warning: \"bg-warning-subtle border-warning-strong text-warning-foreground\",\n danger:\n \"bg-destructive-subtle border-destructive-strong text-destructive-subtle-foreground\",\n info: \"bg-info-subtle border-info-strong text-info-foreground\",\n },\n },\n defaultVariants: { tone: \"neutral\" },\n },\n);\n\nexport const toastCloseVariants = cva([\n \"ms-auto shrink-0 rounded-sm cursor-pointer\",\n \"text-current opacity-70 data-[hovered]:opacity-100\",\n // Quick press, spring release (styles.css).\n \"toast-close\",\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]);\n\n/** What a toast carries. `title` is required: an unnamed toast announces nothing. */\nexport interface ToastContent extends VariantProps<typeof toastVariants> {\n /** The message. Required -- a toast with only a description announces as loose text. */\n title: ReactNode;\n /** Secondary detail, beneath the title. */\n description?: ReactNode;\n}\n\n/**\n * The queue every `toast()` call feeds.\n *\n * Exported so a consumer can read `visibleToasts` in a test, or dismiss them\n * on a route change -- there is no `clear()` upstream, so that is\n * `[...toastQueue.visibleToasts].forEach((t) => toastQueue.close(t.key))`.\n * Not something to touch in normal use.\n */\nexport const toastQueue = new AriaToastQueue<ToastContent>({\n /*\n * Three, against the product's one. Its TOAST_LIMIT of 1 means a second\n * toast REPLACES the first -- so a page that reports three failed uploads\n * shows one and silently drops the rest.\n */\n maxVisibleToasts: 3,\n});\n\n/**\n * Show a toast.\n *\n * @example\n * toast({ title: \"Report saved\" });\n * toast({ title: \"Upload failed\", description: err.message, tone: \"danger\" });\n *\n * @param timeout - ms before it dismisses itself. React Aria REFUSES anything\n * under 5000 and anything at all while the region is focused, because a\n * message that disappears before it can be read fails WCAG 2.2.1. Pass 0 to\n * require a manual dismiss, which is right for an error a user must act on.\n */\nexport function toast(content: ToastContent, timeout = 5000) {\n return toastQueue.add(content, timeout === 0 ? undefined : { timeout });\n}\n\ntype Queued = (typeof toastQueue.visibleToasts)[number];\n\n/**\n * One toast. Pressing its close control plays the exit first, then closes it in the\n * queue; React Aria unmounts a closed toast at once and has no exit of its own, so a\n * toast closed any other way (the timeout, `toastQueue.close`) leaves without one.\n */\nfunction ToastItem({ queued, closeLabel }: { queued: Queued; closeLabel: string }) {\n const [closing, setClosing] = useState(false);\n const finish = useCallback(() => toastQueue.close(queued.key), [queued.key]);\n // If the animation never reports (reduced motion, a hidden tab), close anyway.\n useExitBackstop(closing, finish);\n return (\n <AriaToast\n toast={queued}\n data-slot=\"toast\"\n data-tone={queued.content.tone ?? \"neutral\"}\n data-closing={closing || undefined}\n onAnimationEnd={(event) => {\n if (closing && event.target === event.currentTarget) finish();\n }}\n className={toastVariants({ tone: queued.content.tone })}\n >\n <AriaToastContent data-slot=\"toast-content\" className=\"flex min-w-0 flex-col gap-1\">\n {/*\n slot=\"title\" is what React Aria announces. A plain div here\n renders identically and leaves the toast unnamed.\n */}\n <AriaText\n slot=\"title\"\n data-slot=\"toast-title\"\n className=\"text-sm leading-(--leading-snug) font-medium\"\n >\n {queued.content.title}\n </AriaText>\n {queued.content.description == null ? null : (\n <AriaText\n slot=\"description\"\n data-slot=\"toast-description\"\n className=\"text-sm leading-(--leading-normal) opacity-90\"\n >\n {queued.content.description}\n </AriaText>\n )}\n </AriaToastContent>\n <AriaButton\n aria-label={closeLabel}\n data-slot=\"toast-close\"\n className={toastCloseVariants()}\n onPress={() => setClosing(true)}\n >\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 className=\"size-(--icon-sm)\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n </AriaToast>\n );\n}\n\nexport interface ToastRegionProps {\n /**\n * Names the region for assistive tech.\n *\n * **Required.** React Aria renders the region as a labelled landmark, and\n * an unnamed one is announced as \"region\" with no indication of what just\n * arrived. It is a prop rather than a default string because\n * ARCHITECTURE section 7 rule 9 routes user-visible copy through the\n * consumer until `useMessages()` exists.\n */\n label: string;\n /** Accessible name for each toast's dismiss control. */\n closeLabel?: string;\n /** Extra classes for the region. Merged last. */\n className?: string;\n}\n\n/**\n * Renders the queue. Mount ONCE, near the root.\n *\n * React Aria only renders the landmark while something is queued, so this\n * costs nothing on a quiet screen.\n */\nexport const ToastRegion = forwardRef<HTMLDivElement, ToastRegionProps>(function ToastRegion(\n { label, closeLabel = \"Dismiss\", className },\n ref,\n) {\n return (\n <AriaToastRegion\n ref={ref}\n queue={toastQueue}\n aria-label={label}\n data-slot=\"toast-region\"\n className={cn(toastRegionVariants(), className)}\n >\n {({ toast: queued }) => <ToastItem queued={queued} closeLabel={closeLabel} />}\n </AriaToastRegion>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,MAAa,sBAAsB,IAAI;CACrC;CAEA;CACA;CACA;AACF,CAAC;AAED,MAAa,gBAAgB,IAC3B;CACE;CACA;CACA;CAIA;CACA;AACF,GACA;CACE,UAAU,EAOR,MAAM;EACJ,SAAS;EACT,SAAS;EACT,SAAS;EACT,QACE;EACF,MAAM;CACR,EACF;CACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CACF;AAEA,MAAa,qBAAqB,IAAI;CACpC;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;AAkBD,MAAa,aAAa,IAAIA,oBAA6B,EAMzD,kBAAkB,EACpB,CAAC;;;;;;;;;;;;;AAcD,SAAgB,MAAM,SAAuB,UAAU,KAAM;CAC3D,OAAO,WAAW,IAAI,SAAS,YAAY,IAAI,KAAA,IAAY,EAAE,QAAQ,CAAC;AACxE;;;;;;AASA,SAAS,UAAU,EAAE,QAAQ,cAAsD;CACjF,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,MAAM,SAAS,kBAAkB,WAAW,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,GAAG,CAAC;CAE3E,gBAAgB,SAAS,MAAM;CAC/B,OACE,qBAACC,gBAAD;EACE,OAAO;EACP,aAAU;EACV,aAAW,OAAO,QAAQ,QAAQ;EAClC,gBAAc,WAAW,KAAA;EACzB,iBAAiB,UAAU;GACzB,IAAI,WAAW,MAAM,WAAW,MAAM,eAAe,OAAO;EAC9D;EACA,WAAW,cAAc,EAAE,MAAM,OAAO,QAAQ,KAAK,CAAC;EARxD,UAAA,CAUE,qBAACC,uBAAD;GAAkB,aAAU;GAAgB,WAAU;GAAtD,UAAA,CAKE,oBAACC,MAAD;IACE,MAAK;IACL,aAAU;IACV,WAAU;IAET,UAAA,OAAO,QAAQ;GACR,CAAA,GACT,OAAO,QAAQ,eAAe,OAAO,OACpC,oBAACA,MAAD;IACE,MAAK;IACL,aAAU;IACV,WAAU;IAET,UAAA,OAAO,QAAQ;GACR,CAAA,CAEI;EAClB,CAAA,GAAA,oBAACC,QAAD;GACE,cAAY;GACZ,aAAU;GACV,WAAW,mBAAmB;GAC9B,eAAe,WAAW,IAAI;GAE9B,UAAA,oBAAC,OAAD;IACE,eAAY;IACZ,SAAQ;IACR,MAAK;IACL,QAAO;IACP,aAAY;IACZ,eAAc;IACd,WAAU;IAEV,UAAA,oBAAC,QAAD,EAAM,GAAE,qBAAsB,CAAA;GAC3B,CAAA;EACK,CAAA,CACH;;AAEf;;;;;;;AAyBA,MAAa,cAAc,WAA6C,SAAS,YAC/E,EAAE,OAAO,aAAa,WAAW,aACjC,KACA;CACA,OACE,oBAACC,sBAAD;EACO;EACL,OAAO;EACP,cAAY;EACZ,aAAU;EACV,WAAW,GAAG,oBAAoB,GAAG,SAAS;EAE5C,WAAA,EAAE,OAAO,aAAa,oBAAC,WAAD;GAAmB;GAAoB;EAAa,CAAA;CAC7D,CAAA;AAErB,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { TooltipProps, TooltipTriggerComponentProps } from "react-aria-components";
|
|
2
1
|
import { ReactNode } from "react";
|
|
2
|
+
import { TooltipProps, TooltipTriggerComponentProps } from "react-aria-components";
|
|
3
3
|
//#region src/tooltip/tooltip.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Tooltip, ported from the legacy shadcn/Radix component.
|
|
@@ -46,7 +46,7 @@ interface TooltipProps$1 extends Omit<TooltipProps, "className" | "style" | "chi
|
|
|
46
46
|
*/
|
|
47
47
|
placement?: TooltipProps["placement"];
|
|
48
48
|
}
|
|
49
|
-
declare
|
|
49
|
+
declare const Tooltip$1: import("react").ForwardRefExoticComponent<TooltipProps$1 & import("react").RefAttributes<HTMLDivElement>>;
|
|
50
50
|
/**
|
|
51
51
|
* Pairs a trigger with a tooltip.
|
|
52
52
|
*
|
|
@@ -64,7 +64,7 @@ export interface TooltipTriggerProps extends TooltipTriggerComponentProps {
|
|
|
64
64
|
/** Hover cool-down in ms. @default 0 */
|
|
65
65
|
closeDelay?: number;
|
|
66
66
|
}
|
|
67
|
-
declare function TooltipTrigger$1({ delay, closeDelay, ...props }: TooltipTriggerProps): import("react").JSX.Element;
|
|
67
|
+
declare function TooltipTrigger$1({ delay, closeDelay, onOpenChange, ...props }: TooltipTriggerProps): import("react").JSX.Element;
|
|
68
68
|
//#endregion
|
|
69
69
|
export { Tooltip$1 as Tooltip, TooltipProps$1 as TooltipProps, TooltipTrigger$1 as TooltipTrigger };
|
|
70
70
|
//# sourceMappingURL=tooltip.d.ts.map
|
package/dist/tooltip/tooltip.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { useIsomorphicLayoutEffect } from "../lib/motion.js";
|
|
3
4
|
import { cva } from "class-variance-authority";
|
|
5
|
+
import { forwardRef, useEffect, useRef } from "react";
|
|
4
6
|
import { OverlayArrow, Tooltip, TooltipTrigger } from "react-aria-components";
|
|
5
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
8
|
//#region src/tooltip/tooltip.tsx
|
|
@@ -31,32 +33,81 @@ const tooltipVariants = cva([
|
|
|
31
33
|
"w-fit rounded-md px-3 py-1.5",
|
|
32
34
|
"bg-primary text-primary-foreground",
|
|
33
35
|
"box-border font-sans text-xs leading-(--leading-snug) text-balance",
|
|
34
|
-
"outline-none"
|
|
36
|
+
"outline-none",
|
|
37
|
+
"tooltip-motion"
|
|
35
38
|
]);
|
|
36
|
-
|
|
39
|
+
const WARM_MS = 300;
|
|
40
|
+
let openCount = 0;
|
|
41
|
+
let coolTimer;
|
|
42
|
+
let warm = false;
|
|
43
|
+
let openedWarm = false;
|
|
44
|
+
function noteOpen(open) {
|
|
45
|
+
if (open) {
|
|
46
|
+
openedWarm = warm;
|
|
47
|
+
openCount += 1;
|
|
48
|
+
clearTimeout(coolTimer);
|
|
49
|
+
warm = true;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
openCount = Math.max(0, openCount - 1);
|
|
53
|
+
if (openCount) return;
|
|
54
|
+
clearTimeout(coolTimer);
|
|
55
|
+
coolTimer = setTimeout(() => {
|
|
56
|
+
warm = false;
|
|
57
|
+
}, WARM_MS);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Mounts with the open tooltip (the Tooltip function itself runs while closed too) and
|
|
61
|
+
* marks it `data-instant` if its trigger opened it inside the warm window, before paint.
|
|
62
|
+
*/
|
|
63
|
+
function WarmMark() {
|
|
64
|
+
const ref = useRef(null);
|
|
65
|
+
useIsomorphicLayoutEffect(() => {
|
|
66
|
+
if (openedWarm) ref.current?.parentElement?.setAttribute("data-instant", "");
|
|
67
|
+
}, []);
|
|
68
|
+
return /* @__PURE__ */ jsx("span", {
|
|
69
|
+
ref,
|
|
70
|
+
hidden: true
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const Tooltip$1 = forwardRef(function Tooltip$2({ children, className, placement = "top", ...props }, ref) {
|
|
37
74
|
return /* @__PURE__ */ jsxs(Tooltip, {
|
|
75
|
+
ref,
|
|
38
76
|
...props,
|
|
39
77
|
placement,
|
|
40
78
|
offset: 8,
|
|
41
79
|
"data-slot": "tooltip",
|
|
42
80
|
className: cn(tooltipVariants(), className),
|
|
43
|
-
children: [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ jsx(OverlayArrow, {
|
|
83
|
+
"data-slot": "tooltip-arrow",
|
|
84
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
85
|
+
width: "10",
|
|
86
|
+
height: "5",
|
|
87
|
+
viewBox: "0 0 10 5",
|
|
88
|
+
className: "fill-primary",
|
|
89
|
+
children: /* @__PURE__ */ jsx("path", { d: "M0 5 5 0l5 5z" })
|
|
90
|
+
})
|
|
91
|
+
}),
|
|
92
|
+
/* @__PURE__ */ jsx(WarmMark, {}),
|
|
93
|
+
children
|
|
94
|
+
]
|
|
53
95
|
});
|
|
54
|
-
}
|
|
55
|
-
function TooltipTrigger$1({ delay = 0, closeDelay = 0, ...props }) {
|
|
96
|
+
});
|
|
97
|
+
function TooltipTrigger$1({ delay = 0, closeDelay = 0, onOpenChange, ...props }) {
|
|
98
|
+
const isOpen = useRef(false);
|
|
99
|
+
useEffect(() => () => {
|
|
100
|
+
if (isOpen.current) noteOpen(false);
|
|
101
|
+
}, []);
|
|
56
102
|
return /* @__PURE__ */ jsx(TooltipTrigger, {
|
|
57
103
|
...props,
|
|
58
104
|
delay,
|
|
59
|
-
closeDelay
|
|
105
|
+
closeDelay,
|
|
106
|
+
onOpenChange: (open) => {
|
|
107
|
+
isOpen.current = open;
|
|
108
|
+
noteOpen(open);
|
|
109
|
+
onOpenChange?.(open);
|
|
110
|
+
}
|
|
60
111
|
});
|
|
61
112
|
}
|
|
62
113
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tooltip.js","names":["Tooltip","AriaTooltip","AriaOverlayArrow","TooltipTrigger","AriaTooltipTrigger"],"sources":["../../src/tooltip/tooltip.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport type
|
|
1
|
+
{"version":3,"file":"tooltip.js","names":["Tooltip","AriaTooltip","AriaOverlayArrow","TooltipTrigger","AriaTooltipTrigger"],"sources":["../../src/tooltip/tooltip.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useEffect, useRef, type ReactNode } from \"react\";\nimport {\n OverlayArrow as AriaOverlayArrow,\n Tooltip as AriaTooltip,\n TooltipTrigger as AriaTooltipTrigger,\n type TooltipProps as AriaTooltipProps,\n type TooltipTriggerComponentProps as AriaTooltipTriggerProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useIsomorphicLayoutEffect } from \"../lib/motion.js\";\n\n/**\n * Tooltip, ported from the legacy shadcn/Radix component.\n *\n * Census: ten files import it but only SEVEN render anything -- three\n * import the parts and use none. Across those seven: `Tooltip` 34,\n * `TooltipTrigger` 34, `TooltipContent` 34, `TooltipProvider` 23. `side`\n * is passed 12 times: `\"top\"` 11, `\"bottom\"` once.\n *\n * `TooltipProvider` IS NOT PORTED, and that removes 23 call sites rather\n * than renaming them. The legacy `Tooltip` already wraps itself in a\n * provider, so every explicit `<TooltipProvider>` in app code nested a\n * second one inside the first -- harmless, but it also meant the\n * `skipDelayDuration` grouping the provider exists for never spanned two\n * tooltips. React Aria has no provider at all; that grouping is\n * module-global.\n *\n * THE DELAY IS THE ONE THING THAT WOULD HAVE CHANGED SILENTLY. The legacy\n * provider hardcodes `delayDuration = 0`, so all 34 tooltips appear\n * instantly on hover. React Aria's default is 1500ms. Porting without\n * noticing would have put a 1.5-second warm-up on every tooltip in the\n * app, and neither the type system nor the test suite would have said a\n * word -- so `delay` defaults to 0 here, and a story pins the default.\n */\nexport const tooltipVariants = cva([\n \"w-fit rounded-md px-3 py-1.5\",\n // The legacy palette, kept: primary-foreground on primary is 8.35:1 and\n // is already in the contrast contract. An inverse-surface tooltip is the\n // more common choice, but changing it would be a design decision with no\n // defect behind it.\n \"bg-primary text-primary-foreground\",\n \"box-border font-sans text-xs leading-(--leading-snug) text-balance\",\n \"outline-none\",\n // Rises from its trigger and fades; only fades in the warm window (styles.css).\n \"tooltip-motion\",\n]);\n\n/*\n * The warm window: while any tooltip is open, and for 300ms after the last one\n * closes, the next opens with no travel. TooltipTrigger writes it; the tooltip\n * that mounts next reads whether it was warm when its trigger opened it.\n */\nconst WARM_MS = 300;\nlet openCount = 0;\nlet coolTimer: ReturnType<typeof setTimeout> | undefined;\nlet warm = false;\nlet openedWarm = false;\n\nfunction noteOpen(open: boolean) {\n if (open) {\n openedWarm = warm;\n openCount += 1;\n clearTimeout(coolTimer);\n warm = true;\n return;\n }\n openCount = Math.max(0, openCount - 1);\n if (openCount) return;\n clearTimeout(coolTimer);\n coolTimer = setTimeout(() => {\n warm = false;\n }, WARM_MS);\n}\n\n/**\n * Mounts with the open tooltip (the Tooltip function itself runs while closed too) and\n * marks it `data-instant` if its trigger opened it inside the warm window, before paint.\n */\nfunction WarmMark() {\n const ref = useRef<HTMLSpanElement>(null);\n useIsomorphicLayoutEffect(() => {\n if (openedWarm) ref.current?.parentElement?.setAttribute(\"data-instant\", \"\");\n }, []);\n return <span ref={ref} hidden />;\n}\n\n/**\n * A hover and focus hint.\n *\n * @example\n * <TooltipTrigger>\n * <Button aria-label=\"Mute\"><MicOff /></Button>\n * <Tooltip>Mute</Tooltip>\n * </TooltipTrigger>\n */\nexport interface TooltipProps\n extends Omit<AriaTooltipProps, \"className\" | \"style\" | \"children\"> {\n children?: ReactNode;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Where it opens. The legacy `side` prop: `\"top\"` 11 times, `\"bottom\"`\n * once, and the default for the other 22.\n *\n * @default \"top\"\n */\n placement?: AriaTooltipProps[\"placement\"];\n}\n\nexport const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(function Tooltip(\n { children, className, placement = \"top\", ...props },\n ref,\n) {\n return (\n <AriaTooltip\n // The ref goes on the tooltip itself, not the OverlayArrow child below\n // -- the arrow is a decorative sub-element, not the tooltip's own DOM\n // node.\n ref={ref}\n {...props}\n placement={placement}\n offset={8}\n data-slot=\"tooltip\"\n className={cn(tooltipVariants(), className)}\n >\n {/*\n An SVG arrow, not the legacy rotated square. That used\n `translate-y-[calc(-50%_-_2px)]` and `rounded-[2px]` -- two bracket\n values check:hardcoding rejects -- and a rotated box has to be\n re-placed for every side. React Aria rotates the whole OverlayArrow\n per placement, so one upward triangle serves all four.\n */}\n <AriaOverlayArrow data-slot=\"tooltip-arrow\">\n <svg width=\"10\" height=\"5\" viewBox=\"0 0 10 5\" className=\"fill-primary\">\n <path d=\"M0 5 5 0l5 5z\" />\n </svg>\n </AriaOverlayArrow>\n <WarmMark />\n {children}\n </AriaTooltip>\n );\n});\n\n/**\n * Pairs a trigger with a tooltip.\n *\n * React Aria's `TooltipTrigger`, EXCEPT that `delay` and `closeDelay`\n * default to 0 to match the legacy provider. React Aria ships 1500ms and\n * 500ms; taking those defaults would have changed all 34 call sites\n * without a word.\n *\n * Focus opens it immediately whatever `delay` says -- React Aria passes\n * `immediate` on focus -- which is the behaviour a keyboard user needs.\n */\nexport interface TooltipTriggerProps extends AriaTooltipTriggerProps {\n /** Hover warm-up in ms. @default 0 */\n delay?: number;\n /** Hover cool-down in ms. @default 0 */\n closeDelay?: number;\n}\n\nexport function TooltipTrigger({\n delay = 0,\n closeDelay = 0,\n onOpenChange,\n ...props\n}: TooltipTriggerProps) {\n const isOpen = useRef(false);\n // A trigger that unmounts while open never reports closing; count it out.\n useEffect(\n () => () => {\n if (isOpen.current) noteOpen(false);\n },\n [],\n );\n return (\n <AriaTooltipTrigger\n {...props}\n delay={delay}\n closeDelay={closeDelay}\n onOpenChange={(open) => {\n isOpen.current = open;\n noteOpen(open);\n onOpenChange?.(open);\n }}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,MAAa,kBAAkB,IAAI;CACjC;CAKA;CACA;CACA;CAEA;AACF,CAAC;AAOD,MAAM,UAAU;AAChB,IAAI,YAAY;AAChB,IAAI;AACJ,IAAI,OAAO;AACX,IAAI,aAAa;AAEjB,SAAS,SAAS,MAAe;CAC/B,IAAI,MAAM;EACR,aAAa;EACb,aAAa;EACb,aAAa,SAAS;EACtB,OAAO;EACP;CACF;CACA,YAAY,KAAK,IAAI,GAAG,YAAY,CAAC;CACrC,IAAI,WAAW;CACf,aAAa,SAAS;CACtB,YAAY,iBAAiB;EAC3B,OAAO;CACT,GAAG,OAAO;AACZ;;;;;AAMA,SAAS,WAAW;CAClB,MAAM,MAAM,OAAwB,IAAI;CACxC,gCAAgC;EAC9B,IAAI,YAAY,IAAI,SAAS,eAAe,aAAa,gBAAgB,EAAE;CAC7E,GAAG,CAAC,CAAC;CACL,OAAO,oBAAC,QAAD;EAAW;EAAK,QAAA;CAAQ,CAAA;AACjC;AAyBA,MAAaA,YAAU,WAAyC,SAASA,UACvE,EAAE,UAAU,WAAW,YAAY,OAAO,GAAG,SAC7C,KACA;CACA,OACE,qBAACC,SAAD;EAIO;EACL,GAAI;EACO;EACX,QAAQ;EACR,aAAU;EACV,WAAW,GAAG,gBAAgB,GAAG,SAAS;EAT5C,UAAA;GAkBE,oBAACC,cAAD;IAAkB,aAAU;IAC1B,UAAA,oBAAC,OAAD;KAAK,OAAM;KAAK,QAAO;KAAI,SAAQ;KAAW,WAAU;KACtD,UAAA,oBAAC,QAAD,EAAM,GAAE,gBAAiB,CAAA;IACtB,CAAA;GACW,CAAA;GAClB,oBAAC,UAAD,CAAW,CAAA;GACV;EACU;;AAEjB,CAAC;AAoBD,SAAgBC,iBAAe,EAC7B,QAAQ,GACR,aAAa,GACb,cACA,GAAG,SACmB;CACtB,MAAM,SAAS,OAAO,KAAK;CAE3B,sBACc;EACV,IAAI,OAAO,SAAS,SAAS,KAAK;CACpC,GACA,CAAC,CACH;CACA,OACE,oBAACC,gBAAD;EACE,GAAI;EACG;EACK;EACZ,eAAe,SAAS;GACtB,OAAO,UAAU;GACjB,SAAS,IAAI;GACb,eAAe,IAAI;EACrB;CACD,CAAA;AAEL"}
|