@blurrow/ui 0.2.0 → 0.2.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 +48 -36
- package/dist/Accordion.d.ts +17 -0
- package/dist/Accordion.js +138 -0
- package/dist/Accordion.js.map +1 -0
- package/dist/Alert.d.ts +13 -0
- package/dist/Alert.js +69 -0
- package/dist/Alert.js.map +1 -0
- package/dist/AuthorCard.d.ts +12 -0
- package/dist/AuthorCard.js +57 -0
- package/dist/AuthorCard.js.map +1 -0
- package/dist/Avatar.d.ts +21 -0
- package/dist/Avatar.js +96 -0
- package/dist/Avatar.js.map +1 -0
- package/dist/Badge.d.ts +12 -0
- package/dist/Badge.js +46 -0
- package/dist/Badge.js.map +1 -0
- package/dist/Banner.d.ts +15 -0
- package/dist/Banner.js +68 -0
- package/dist/Banner.js.map +1 -0
- package/dist/Breadcrumb.d.ts +13 -0
- package/dist/Breadcrumb.js +45 -0
- package/dist/Breadcrumb.js.map +1 -0
- package/dist/Button.d.ts +19 -0
- package/dist/Button.js +282 -0
- package/dist/Button.js.map +1 -0
- package/dist/ButtonGroup.d.ts +19 -0
- package/dist/ButtonGroup.js +62 -0
- package/dist/ButtonGroup.js.map +1 -0
- package/dist/Card.d.ts +40 -0
- package/dist/Card.js +175 -0
- package/dist/Card.js.map +1 -0
- package/dist/CardShell.d.ts +18 -0
- package/dist/CardShell.js +58 -0
- package/dist/CardShell.js.map +1 -0
- package/dist/Carousel.d.ts +22 -0
- package/dist/Carousel.js +247 -0
- package/dist/Carousel.js.map +1 -0
- package/dist/Checkbox.d.ts +15 -0
- package/dist/Checkbox.js +84 -0
- package/dist/Checkbox.js.map +1 -0
- package/dist/CloseButton.d.ts +12 -0
- package/dist/CloseButton.js +63 -0
- package/dist/CloseButton.js.map +1 -0
- package/dist/CodeBlock.d.ts +12 -0
- package/dist/CodeBlock.js +221 -0
- package/dist/CodeBlock.js.map +1 -0
- package/dist/ColorPicker.d.ts +24 -0
- package/dist/ColorPicker.js +366 -0
- package/dist/ColorPicker.js.map +1 -0
- package/dist/Combobox.d.ts +34 -0
- package/dist/Combobox.js +255 -0
- package/dist/Combobox.js.map +1 -0
- package/dist/CommandPalette.d.ts +25 -0
- package/dist/CommandPalette.js +304 -0
- package/dist/CommandPalette.js.map +1 -0
- package/dist/ContextMenu.d.ts +16 -0
- package/dist/ContextMenu.js +209 -0
- package/dist/ContextMenu.js.map +1 -0
- package/dist/DataList.d.ts +15 -0
- package/dist/DataList.js +42 -0
- package/dist/DataList.js.map +1 -0
- package/dist/DatePicker.d.ts +65 -0
- package/dist/DatePicker.js +391 -0
- package/dist/DatePicker.js.map +1 -0
- package/dist/DateRangePicker.d.ts +34 -0
- package/dist/DateRangePicker.js +180 -0
- package/dist/DateRangePicker.js.map +1 -0
- package/dist/Drawer.d.ts +17 -0
- package/dist/Drawer.js +132 -0
- package/dist/Drawer.js.map +1 -0
- package/dist/DropdownMenu.d.ts +23 -0
- package/dist/DropdownMenu.js +185 -0
- package/dist/DropdownMenu.js.map +1 -0
- package/dist/EmptyState.d.ts +13 -0
- package/dist/EmptyState.js +55 -0
- package/dist/EmptyState.js.map +1 -0
- package/dist/FieldMessage.d.ts +26 -0
- package/dist/FieldMessage.js +95 -0
- package/dist/FieldMessage.js.map +1 -0
- package/dist/FileUpload.d.ts +32 -0
- package/dist/FileUpload.js +253 -0
- package/dist/FileUpload.js.map +1 -0
- package/dist/Floating.d.ts +32 -0
- package/dist/Floating.js +95 -0
- package/dist/Floating.js.map +1 -0
- package/dist/HoverCard.d.ts +18 -0
- package/dist/HoverCard.js +90 -0
- package/dist/HoverCard.js.map +1 -0
- package/dist/Input.d.ts +17 -0
- package/dist/Input.js +96 -0
- package/dist/Input.js.map +1 -0
- package/dist/Kbd.d.ts +9 -0
- package/dist/Kbd.js +24 -0
- package/dist/Kbd.js.map +1 -0
- package/dist/Modal.d.ts +15 -0
- package/dist/Modal.js +109 -0
- package/dist/Modal.js.map +1 -0
- package/dist/NumberInput.d.ts +23 -0
- package/dist/NumberInput.js +162 -0
- package/dist/NumberInput.js.map +1 -0
- package/dist/OtpInput.d.ts +26 -0
- package/dist/OtpInput.js +173 -0
- package/dist/OtpInput.js.map +1 -0
- package/dist/Pagination.d.ts +11 -0
- package/dist/Pagination.js +148 -0
- package/dist/Pagination.js.map +1 -0
- package/dist/Popover.d.ts +13 -0
- package/dist/Popover.js +98 -0
- package/dist/Popover.js.map +1 -0
- package/dist/Progress.d.ts +12 -0
- package/dist/Progress.js +61 -0
- package/dist/Progress.js.map +1 -0
- package/dist/QuoteCard.d.ts +13 -0
- package/dist/QuoteCard.js +92 -0
- package/dist/QuoteCard.js.map +1 -0
- package/dist/Radio.d.ts +19 -0
- package/dist/Radio.js +161 -0
- package/dist/Radio.js.map +1 -0
- package/dist/RadioCards.d.ts +27 -0
- package/dist/RadioCards.js +205 -0
- package/dist/RadioCards.js.map +1 -0
- package/dist/ReactionBar.d.ts +15 -0
- package/dist/ReactionBar.js +92 -0
- package/dist/ReactionBar.js.map +1 -0
- package/dist/ReadingProgress.d.ts +8 -0
- package/dist/ReadingProgress.js +51 -0
- package/dist/ReadingProgress.js.map +1 -0
- package/dist/ReadingTime.d.ts +9 -0
- package/dist/ReadingTime.js +39 -0
- package/dist/ReadingTime.js.map +1 -0
- package/dist/Select.d.ts +20 -0
- package/dist/Select.js +244 -0
- package/dist/Select.js.map +1 -0
- package/dist/Separator.d.ts +14 -0
- package/dist/Separator.js +33 -0
- package/dist/Separator.js.map +1 -0
- package/dist/ShareButtons.d.ts +10 -0
- package/dist/ShareButtons.js +107 -0
- package/dist/ShareButtons.js.map +1 -0
- package/dist/Sidebar.d.ts +30 -0
- package/dist/Sidebar.js +140 -0
- package/dist/Sidebar.js.map +1 -0
- package/dist/Skeleton.d.ts +15 -0
- package/dist/Skeleton.js +67 -0
- package/dist/Skeleton.js.map +1 -0
- package/dist/Slider.d.ts +27 -0
- package/dist/Slider.js +182 -0
- package/dist/Slider.js.map +1 -0
- package/dist/Spinner.d.ts +12 -0
- package/dist/Spinner.js +24 -0
- package/dist/Spinner.js.map +1 -0
- package/dist/SpotlightCard.d.ts +17 -0
- package/dist/SpotlightCard.js +87 -0
- package/dist/SpotlightCard.js.map +1 -0
- package/dist/Stat.d.ts +19 -0
- package/dist/Stat.js +71 -0
- package/dist/Stat.js.map +1 -0
- package/dist/StatCard.d.ts +24 -0
- package/dist/StatCard.js +212 -0
- package/dist/StatCard.js.map +1 -0
- package/dist/Steps.d.ts +14 -0
- package/dist/Steps.js +94 -0
- package/dist/Steps.js.map +1 -0
- package/dist/Table.d.ts +21 -0
- package/dist/Table.js +93 -0
- package/dist/Table.js.map +1 -0
- package/dist/TableOfContents.d.ts +17 -0
- package/dist/TableOfContents.js +92 -0
- package/dist/TableOfContents.js.map +1 -0
- package/dist/Tabs.d.ts +17 -0
- package/dist/Tabs.js +123 -0
- package/dist/Tabs.js.map +1 -0
- package/dist/TagInput.d.ts +16 -0
- package/dist/TagInput.js +169 -0
- package/dist/TagInput.js.map +1 -0
- package/dist/Textarea.d.ts +13 -0
- package/dist/Textarea.js +74 -0
- package/dist/Textarea.js.map +1 -0
- package/dist/TimePicker.d.ts +28 -0
- package/dist/TimePicker.js +264 -0
- package/dist/TimePicker.js.map +1 -0
- package/dist/Timeline.d.ts +19 -0
- package/dist/Timeline.js +76 -0
- package/dist/Timeline.js.map +1 -0
- package/dist/Toast.d.ts +18 -0
- package/dist/Toast.js +318 -0
- package/dist/Toast.js.map +1 -0
- package/dist/Toggle.d.ts +13 -0
- package/dist/Toggle.js +84 -0
- package/dist/Toggle.js.map +1 -0
- package/dist/Tooltip.d.ts +13 -0
- package/dist/Tooltip.js +138 -0
- package/dist/Tooltip.js.map +1 -0
- package/dist/cli.js +172 -5
- package/dist/index.d.ts +68 -631
- package/dist/index.js +67 -5558
- package/dist/index.js.map +1 -1
- package/dist/registry/Accordion.tsx +6 -6
- package/dist/registry/Alert.tsx +13 -10
- package/dist/registry/AuthorCard.tsx +7 -7
- package/dist/registry/Avatar.tsx +5 -5
- package/dist/registry/Badge.tsx +12 -15
- package/dist/registry/Banner.tsx +15 -18
- package/dist/registry/Breadcrumb.tsx +3 -3
- package/dist/registry/Button.tsx +58 -69
- package/dist/registry/ButtonGroup.tsx +9 -9
- package/dist/registry/Card.tsx +15 -15
- package/dist/registry/CardShell.tsx +6 -6
- package/dist/registry/Carousel.tsx +19 -15
- package/dist/registry/Checkbox.tsx +14 -9
- package/dist/registry/CloseButton.tsx +2 -2
- package/dist/registry/CodeBlock.tsx +15 -15
- package/dist/registry/ColorPicker.tsx +404 -0
- package/dist/registry/Combobox.tsx +318 -0
- package/dist/registry/CommandPalette.tsx +31 -24
- package/dist/registry/ContextMenu.tsx +241 -0
- package/dist/registry/DataList.tsx +4 -4
- package/dist/registry/DatePicker.tsx +494 -0
- package/dist/registry/DateRangePicker.tsx +232 -0
- package/dist/registry/Drawer.tsx +17 -15
- package/dist/registry/DropdownMenu.tsx +39 -77
- package/dist/registry/EmptyState.tsx +8 -8
- package/dist/registry/FieldMessage.tsx +123 -0
- package/dist/registry/FileUpload.tsx +318 -0
- package/dist/registry/Floating.tsx +134 -0
- package/dist/registry/HoverCard.tsx +113 -0
- package/dist/registry/Input.tsx +54 -43
- package/dist/registry/Kbd.tsx +4 -5
- package/dist/registry/Modal.tsx +17 -15
- package/dist/registry/NumberInput.tsx +183 -0
- package/dist/registry/OtpInput.tsx +219 -0
- package/dist/registry/Pagination.tsx +15 -18
- package/dist/registry/Popover.tsx +40 -45
- package/dist/registry/Progress.tsx +4 -4
- package/dist/registry/QuoteCard.tsx +7 -7
- package/dist/registry/Radio.tsx +24 -21
- package/dist/registry/RadioCards.tsx +247 -0
- package/dist/registry/ReactionBar.tsx +5 -5
- package/dist/registry/ReadingProgress.tsx +2 -2
- package/dist/registry/ReadingTime.tsx +2 -2
- package/dist/registry/Select.tsx +28 -53
- package/dist/registry/Separator.tsx +52 -0
- package/dist/registry/ShareButtons.tsx +2 -2
- package/dist/registry/Sidebar.tsx +18 -18
- package/dist/registry/Skeleton.tsx +1 -1
- package/dist/registry/Slider.tsx +229 -0
- package/dist/registry/Spinner.tsx +32 -0
- package/dist/registry/SpotlightCard.tsx +6 -6
- package/dist/registry/Stat.tsx +4 -4
- package/dist/registry/StatCard.tsx +19 -19
- package/dist/registry/Steps.tsx +7 -7
- package/dist/registry/Table.tsx +12 -10
- package/dist/registry/TableOfContents.tsx +8 -8
- package/dist/registry/Tabs.tsx +6 -6
- package/dist/registry/TagInput.tsx +9 -9
- package/dist/registry/Textarea.tsx +5 -5
- package/dist/registry/TimePicker.tsx +315 -0
- package/dist/registry/Timeline.tsx +14 -14
- package/dist/registry/Toast.tsx +15 -12
- package/dist/registry/Toggle.tsx +7 -10
- package/dist/registry/Tooltip.tsx +21 -73
- package/dist/registry/styles.css +868 -62
- package/dist/registry/useFloatingPanel.ts +112 -0
- package/dist/styles.css +868 -62
- package/dist/useFloatingPanel.d.ts +24 -0
- package/dist/useFloatingPanel.js +89 -0
- package/dist/useFloatingPanel.js.map +1 -0
- package/dist/useModalBehavior.d.ts +15 -0
- package/dist/useModalBehavior.js +57 -0
- package/dist/useModalBehavior.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useId, useRef, useState } from "react";
|
|
4
|
+
import { ClearKey } from "./DatePicker";
|
|
5
|
+
import { Portal } from "./Floating";
|
|
6
|
+
import { FieldLabel, FieldMessage } from "./FieldMessage";
|
|
7
|
+
import { useFloatingPanel } from "./useFloatingPanel";
|
|
8
|
+
|
|
9
|
+
interface TimePickerProps {
|
|
10
|
+
/** 24-hour "HH:mm", like "14:30" */
|
|
11
|
+
value?: string | null;
|
|
12
|
+
defaultValue?: string | null;
|
|
13
|
+
onChange?: (time: string | null) => void;
|
|
14
|
+
/** Minutes between choices in the minute column */
|
|
15
|
+
step?: number;
|
|
16
|
+
/** Show 1–12 with am / pm instead of 0–23 */
|
|
17
|
+
hour12?: boolean;
|
|
18
|
+
/** Earliest and latest times, as "HH:mm" */
|
|
19
|
+
min?: string;
|
|
20
|
+
max?: string;
|
|
21
|
+
label?: string;
|
|
22
|
+
hint?: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
clearable?: boolean;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
fullWidth?: boolean;
|
|
28
|
+
name?: string;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
33
|
+
const toMinutes = (t?: string | null) => {
|
|
34
|
+
if (!t) return null;
|
|
35
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(t);
|
|
36
|
+
return m ? Number(m[1]) * 60 + Number(m[2]) : null;
|
|
37
|
+
};
|
|
38
|
+
const fromMinutes = (m: number) => `${pad(Math.floor(m / 60))}:${pad(m % 60)}`;
|
|
39
|
+
|
|
40
|
+
export function formatTime(t: string, hour12: boolean) {
|
|
41
|
+
const m = toMinutes(t);
|
|
42
|
+
if (m === null) return t;
|
|
43
|
+
const h = Math.floor(m / 60);
|
|
44
|
+
return hour12 ? `${h % 12 || 12}:${pad(m % 60)} ${h < 12 ? "am" : "pm"}` : fromMinutes(m);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type Column = "hour" | "minute" | "period";
|
|
48
|
+
|
|
49
|
+
/* A field that opens a panel of columns: hours, minutes and, for a
|
|
50
|
+
12-hour clock, am / pm. The chosen cell in each is a raised key in the
|
|
51
|
+
inverse material. Arrows move within a column and between columns;
|
|
52
|
+
Enter or picking a minute closes it. */
|
|
53
|
+
export function TimePicker({
|
|
54
|
+
value,
|
|
55
|
+
defaultValue = null,
|
|
56
|
+
onChange,
|
|
57
|
+
step = 15,
|
|
58
|
+
hour12 = false,
|
|
59
|
+
min,
|
|
60
|
+
max,
|
|
61
|
+
label,
|
|
62
|
+
hint,
|
|
63
|
+
error,
|
|
64
|
+
placeholder = "pick a time",
|
|
65
|
+
clearable = false,
|
|
66
|
+
disabled = false,
|
|
67
|
+
fullWidth = false,
|
|
68
|
+
name,
|
|
69
|
+
className = "",
|
|
70
|
+
}: TimePickerProps) {
|
|
71
|
+
const uid = useId();
|
|
72
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
73
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
74
|
+
const [internal, setInternal] = useState<string | null>(defaultValue);
|
|
75
|
+
const current = value !== undefined ? value : internal;
|
|
76
|
+
const total = toMinutes(current);
|
|
77
|
+
const { open, mounted, toggle, close, setFloating, panelStyle } = useFloatingPanel(ref, {
|
|
78
|
+
onEscape: () => buttonRef.current?.focus(),
|
|
79
|
+
});
|
|
80
|
+
const messageId = error || hint ? `${uid}-message` : undefined;
|
|
81
|
+
|
|
82
|
+
const lo = toMinutes(min) ?? 0;
|
|
83
|
+
const hi = toMinutes(max) ?? 24 * 60 - 1;
|
|
84
|
+
const hour = total === null ? null : Math.floor(total / 60);
|
|
85
|
+
const minute = total === null ? null : total % 60;
|
|
86
|
+
|
|
87
|
+
const set = (t: string | null) => {
|
|
88
|
+
setInternal(t);
|
|
89
|
+
onChange?.(t);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/* Picks a time, nudging the minutes into range when the hour changes */
|
|
93
|
+
const choose = (h: number, m: number) => {
|
|
94
|
+
let t = h * 60 + m;
|
|
95
|
+
if (t < lo) t = lo;
|
|
96
|
+
if (t > hi) t = hi;
|
|
97
|
+
set(fromMinutes(t));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const hours = hour12 ? Array.from({ length: 12 }, (_, i) => (i === 0 ? 12 : i)) : Array.from({ length: 24 }, (_, i) => i);
|
|
101
|
+
const minutes = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step).filter((m) => m < 60);
|
|
102
|
+
const isPm = hour !== null && hour >= 12;
|
|
103
|
+
const to24 = (h12: number, pm: boolean) => (h12 % 12) + (pm ? 12 : 0);
|
|
104
|
+
|
|
105
|
+
const hourOff = (h24: number) => h24 * 60 + 59 < lo || h24 * 60 > hi;
|
|
106
|
+
const minuteOff = (m: number) => hour !== null && (hour * 60 + m < lo || hour * 60 + m > hi);
|
|
107
|
+
|
|
108
|
+
type Cell = { key: string; label: string; selected: boolean; off: boolean; pick: () => void };
|
|
109
|
+
const columns: { id: Column; label: string; cells: Cell[] }[] = [
|
|
110
|
+
{
|
|
111
|
+
id: "hour",
|
|
112
|
+
label: "hour",
|
|
113
|
+
cells: hours.map((h) => {
|
|
114
|
+
const h24 = hour12 ? to24(h, isPm) : h;
|
|
115
|
+
return {
|
|
116
|
+
key: `h${h}`,
|
|
117
|
+
label: hour12 ? String(h) : pad(h),
|
|
118
|
+
selected: hour === h24,
|
|
119
|
+
off: hourOff(h24),
|
|
120
|
+
pick: () => choose(h24, minute ?? 0),
|
|
121
|
+
};
|
|
122
|
+
}),
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "minute",
|
|
126
|
+
label: "minute",
|
|
127
|
+
cells: minutes.map((m) => ({
|
|
128
|
+
key: `m${m}`,
|
|
129
|
+
label: pad(m),
|
|
130
|
+
selected: minute === m,
|
|
131
|
+
off: minuteOff(m),
|
|
132
|
+
pick: () => {
|
|
133
|
+
choose(hour ?? Math.floor(lo / 60), m);
|
|
134
|
+
close();
|
|
135
|
+
buttonRef.current?.focus();
|
|
136
|
+
},
|
|
137
|
+
})),
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
if (hour12) {
|
|
141
|
+
columns.push({
|
|
142
|
+
id: "period",
|
|
143
|
+
label: "am or pm",
|
|
144
|
+
cells: (["am", "pm"] as const).map((p) => {
|
|
145
|
+
const pm = p === "pm";
|
|
146
|
+
const h24 = to24(hour === null ? 12 : hour % 12 || 12, pm);
|
|
147
|
+
return {
|
|
148
|
+
key: p,
|
|
149
|
+
label: p,
|
|
150
|
+
selected: hour !== null && isPm === pm,
|
|
151
|
+
off: pm ? hi < 12 * 60 : lo >= 12 * 60,
|
|
152
|
+
pick: () => choose(h24, minute ?? 0),
|
|
153
|
+
};
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const colRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
159
|
+
|
|
160
|
+
/* Each column scrolls its chosen cell into the middle when it mounts */
|
|
161
|
+
const attachColumn = (id: Column) => (el: HTMLDivElement | null) => {
|
|
162
|
+
colRefs.current[id] = el;
|
|
163
|
+
const sel = el?.querySelector<HTMLElement>('[aria-selected="true"]');
|
|
164
|
+
if (!el || el.dataset.scrolled) return;
|
|
165
|
+
el.dataset.scrolled = "1";
|
|
166
|
+
if (sel) el.scrollTop = sel.offsetTop - el.clientHeight / 2 + sel.offsetHeight / 2;
|
|
167
|
+
// The panel lives at the end of <body>, so focus goes to it rather than waiting for Tab
|
|
168
|
+
if (id === "hour") el.focus({ preventScroll: true });
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const onColumnKey = (ci: number) => (e: React.KeyboardEvent) => {
|
|
172
|
+
const col = columns[ci];
|
|
173
|
+
const enabled = col.cells.map((c, i) => (c.off ? -1 : i)).filter((i) => i >= 0);
|
|
174
|
+
const at = col.cells.findIndex((c) => c.selected);
|
|
175
|
+
const pos = enabled.indexOf(at);
|
|
176
|
+
const focusCol = (n: number) =>
|
|
177
|
+
colRefs.current[columns[Math.max(0, Math.min(columns.length - 1, n))].id]?.focus();
|
|
178
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
const d = e.key === "ArrowDown" ? 1 : -1;
|
|
181
|
+
const next = enabled[pos < 0 ? 0 : (pos + d + enabled.length) % enabled.length];
|
|
182
|
+
// the minute column closes on pick; from the keyboard, just move
|
|
183
|
+
if (col.id === "minute") choose(hour ?? Math.floor(lo / 60), minutes[next]);
|
|
184
|
+
else col.cells[next]?.pick();
|
|
185
|
+
requestAnimationFrame(() =>
|
|
186
|
+
colRefs.current[col.id]?.querySelector('[aria-selected="true"]')?.scrollIntoView({ block: "nearest" }),
|
|
187
|
+
);
|
|
188
|
+
} else if (e.key === "ArrowRight") {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
focusCol(ci + 1);
|
|
191
|
+
} else if (e.key === "ArrowLeft") {
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
focusCol(ci - 1);
|
|
194
|
+
} else if (e.key === "Enter") {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
close();
|
|
197
|
+
buttonRef.current?.focus();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div className={["group/field flex flex-col gap-1.5", fullWidth ? "w-full" : ""].join(" ")}>
|
|
203
|
+
{label && <FieldLabel id={`${uid}-label`}>{label}</FieldLabel>}
|
|
204
|
+
<div ref={ref} className={["relative", fullWidth ? "w-full" : "inline-block w-44", className].join(" ")}>
|
|
205
|
+
{/* The field's error styling keys off aria-invalid, as Input's does */}
|
|
206
|
+
{/* eslint-disable-next-line jsx-a11y/role-supports-aria-props */}
|
|
207
|
+
<button
|
|
208
|
+
ref={buttonRef}
|
|
209
|
+
type="button"
|
|
210
|
+
disabled={disabled}
|
|
211
|
+
onClick={toggle}
|
|
212
|
+
aria-haspopup="dialog"
|
|
213
|
+
aria-expanded={open}
|
|
214
|
+
aria-labelledby={label ? `${uid}-label ${uid}-value` : undefined}
|
|
215
|
+
aria-invalid={error ? true : undefined}
|
|
216
|
+
aria-describedby={messageId}
|
|
217
|
+
data-open={open || undefined}
|
|
218
|
+
className={[
|
|
219
|
+
"field group/time relative isolate flex w-full cursor-pointer items-center gap-2.5 rounded-xl py-3 pl-3.5 text-left text-[13.5px]",
|
|
220
|
+
clearable && current ? "pr-10" : "pr-4",
|
|
221
|
+
"focus:outline-none disabled:opacity-40 disabled:pointer-events-none",
|
|
222
|
+
].join(" ")}
|
|
223
|
+
>
|
|
224
|
+
<svg
|
|
225
|
+
aria-hidden
|
|
226
|
+
width="15"
|
|
227
|
+
height="15"
|
|
228
|
+
fill="none"
|
|
229
|
+
stroke="currentColor"
|
|
230
|
+
strokeWidth="2"
|
|
231
|
+
strokeLinecap="round"
|
|
232
|
+
viewBox="0 0 24 24"
|
|
233
|
+
className={[
|
|
234
|
+
"shrink-0 transition-[color,transform] duration-300 ease-[var(--ease-spring)]",
|
|
235
|
+
open ? "scale-110 text-ink-soft" : "text-ink-faint group-hover/time:text-ink-body",
|
|
236
|
+
].join(" ")}
|
|
237
|
+
>
|
|
238
|
+
<circle cx="12" cy="12" r="9" />
|
|
239
|
+
<path d="M12 7v5l3 2" />
|
|
240
|
+
</svg>
|
|
241
|
+
<span id={`${uid}-value`} className={["tabular-nums", current ? "font-medium text-ink" : "text-ink-faint"].join(" ")}>
|
|
242
|
+
{current ? formatTime(current, hour12) : placeholder}
|
|
243
|
+
</span>
|
|
244
|
+
</button>
|
|
245
|
+
{clearable && current && !disabled && (
|
|
246
|
+
<ClearKey
|
|
247
|
+
label="clear time"
|
|
248
|
+
onClear={() => {
|
|
249
|
+
set(null);
|
|
250
|
+
buttonRef.current?.focus();
|
|
251
|
+
}}
|
|
252
|
+
/>
|
|
253
|
+
)}
|
|
254
|
+
{name && <input type="hidden" name={name} value={current ?? ""} />}
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{mounted && (
|
|
258
|
+
<Portal>
|
|
259
|
+
<div
|
|
260
|
+
ref={setFloating}
|
|
261
|
+
role="dialog"
|
|
262
|
+
aria-label={label ? `choose ${label}` : "choose a time"}
|
|
263
|
+
className="rounded-[16px] p-1.5"
|
|
264
|
+
style={panelStyle}
|
|
265
|
+
>
|
|
266
|
+
<div className="flex gap-1 rounded-[11px] bg-panel-face p-1.5" style={{ boxShadow: "var(--panel-face-shadow)" }}>
|
|
267
|
+
{columns.map((col, ci) => (
|
|
268
|
+
<div
|
|
269
|
+
key={col.id}
|
|
270
|
+
ref={attachColumn(col.id)}
|
|
271
|
+
role="listbox"
|
|
272
|
+
aria-label={col.label}
|
|
273
|
+
tabIndex={0}
|
|
274
|
+
onKeyDown={onColumnKey(ci)}
|
|
275
|
+
className="flex max-h-56 w-14 flex-col gap-0.5 overflow-y-auto rounded-[9px] p-0.5 scroll-thin outline-none focus-visible:ring-2 focus-visible:ring-ink"
|
|
276
|
+
style={{ scrollbarWidth: "none" }}
|
|
277
|
+
>
|
|
278
|
+
{col.cells.map((c) => (
|
|
279
|
+
<div
|
|
280
|
+
key={c.key}
|
|
281
|
+
role="option"
|
|
282
|
+
aria-selected={c.selected}
|
|
283
|
+
aria-disabled={c.off || undefined}
|
|
284
|
+
onClick={() => !c.off && c.pick()}
|
|
285
|
+
className={[
|
|
286
|
+
"grid h-8 shrink-0 place-items-center rounded-[8px] text-[13px] tabular-nums transition-colors duration-100",
|
|
287
|
+
c.off
|
|
288
|
+
? "cursor-not-allowed text-ink-ghost"
|
|
289
|
+
: c.selected
|
|
290
|
+
? "cursor-pointer font-semibold text-ink-inverse"
|
|
291
|
+
: "cursor-pointer text-ink-body hover:bg-surface-active hover:text-ink",
|
|
292
|
+
].join(" ")}
|
|
293
|
+
style={
|
|
294
|
+
c.selected && !c.off
|
|
295
|
+
? {
|
|
296
|
+
background: "var(--face-inverse)",
|
|
297
|
+
boxShadow: "var(--face-inverse-shadow)",
|
|
298
|
+
textShadow: "var(--emboss-inverse)",
|
|
299
|
+
}
|
|
300
|
+
: undefined
|
|
301
|
+
}
|
|
302
|
+
>
|
|
303
|
+
{c.label}
|
|
304
|
+
</div>
|
|
305
|
+
))}
|
|
306
|
+
</div>
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</Portal>
|
|
311
|
+
)}
|
|
312
|
+
<FieldMessage id={messageId} error={error} hint={hint} />
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
@@ -20,17 +20,17 @@ interface TimelineProps {
|
|
|
20
20
|
lamp; the variant only changes the lamp's colour. Nodes are joined by
|
|
21
21
|
one continuous groove, and titles sit on the node's centre line. */
|
|
22
22
|
const lamp: Record<TimelineVariant, string> = {
|
|
23
|
-
default: "
|
|
24
|
-
success: "
|
|
25
|
-
warning: "
|
|
26
|
-
accent: "
|
|
23
|
+
default: "var(--lamp-default)",
|
|
24
|
+
success: "var(--lamp-success)",
|
|
25
|
+
warning: "var(--lamp-warning)",
|
|
26
|
+
accent: "var(--lamp-accent)",
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const iconText: Record<TimelineVariant, string> = {
|
|
30
|
-
default: "text-
|
|
31
|
-
success: "text-
|
|
32
|
-
warning: "text-
|
|
33
|
-
accent: "text-
|
|
30
|
+
default: "text-ink-body",
|
|
31
|
+
success: "text-success-soft",
|
|
32
|
+
warning: "text-warning",
|
|
33
|
+
accent: "text-accent-soft",
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
export function Timeline({ items, className = "" }: TimelineProps) {
|
|
@@ -40,7 +40,7 @@ export function Timeline({ items, className = "" }: TimelineProps) {
|
|
|
40
40
|
style={{ background: "var(--panel-bezel)", boxShadow: "var(--panel-shadow)" }}
|
|
41
41
|
>
|
|
42
42
|
<ol
|
|
43
|
-
className="bg-
|
|
43
|
+
className="bg-panel-face rounded-[13px] px-5 py-5 flex flex-col"
|
|
44
44
|
style={{ boxShadow: "var(--panel-face-shadow)" }}
|
|
45
45
|
>
|
|
46
46
|
{items.map((item, index) => {
|
|
@@ -63,14 +63,14 @@ export function Timeline({ items, className = "" }: TimelineProps) {
|
|
|
63
63
|
"relative w-6 h-6 rounded-full flex items-center justify-center shrink-0",
|
|
64
64
|
iconText[variant],
|
|
65
65
|
].join(" ")}
|
|
66
|
-
style={{ background: "var(--face
|
|
66
|
+
style={{ background: "var(--face)", boxShadow: "var(--tile-shadow)" }}
|
|
67
67
|
>
|
|
68
68
|
{item.icon ?? (
|
|
69
69
|
<span
|
|
70
70
|
className="w-2 h-2 rounded-full"
|
|
71
71
|
style={{
|
|
72
72
|
background: lamp[variant],
|
|
73
|
-
boxShadow: `0 0 6px ${lamp[variant]}
|
|
73
|
+
boxShadow: `0 0 6px color-mix(in srgb, ${lamp[variant]} 40%, transparent), var(--lamp-lip)`,
|
|
74
74
|
}}
|
|
75
75
|
/>
|
|
76
76
|
)}
|
|
@@ -78,17 +78,17 @@ export function Timeline({ items, className = "" }: TimelineProps) {
|
|
|
78
78
|
|
|
79
79
|
<div className="flex-1 min-w-0">
|
|
80
80
|
<div className="flex items-baseline justify-between gap-3">
|
|
81
|
-
<p className="text-[13.5px] font-medium text-
|
|
81
|
+
<p className="text-[13.5px] font-medium text-ink leading-6 tracking-[-0.005em]">
|
|
82
82
|
{item.title}
|
|
83
83
|
</p>
|
|
84
84
|
{item.date && (
|
|
85
|
-
<time className="text-[12px] text-
|
|
85
|
+
<time className="text-[12px] text-ink-faint tabular-nums shrink-0">
|
|
86
86
|
{item.date}
|
|
87
87
|
</time>
|
|
88
88
|
)}
|
|
89
89
|
</div>
|
|
90
90
|
{item.description && (
|
|
91
|
-
<p className="text-[13px] text-
|
|
91
|
+
<p className="text-[13px] text-ink-muted leading-relaxed mt-0.5">
|
|
92
92
|
{item.description}
|
|
93
93
|
</p>
|
|
94
94
|
)}
|
package/dist/registry/Toast.tsx
CHANGED
|
@@ -41,7 +41,7 @@ const variantConfig: Record<
|
|
|
41
41
|
{ tile: string; icon: React.ReactNode }
|
|
42
42
|
> = {
|
|
43
43
|
default: {
|
|
44
|
-
tile: "var(--face-
|
|
44
|
+
tile: "var(--face-inverse)",
|
|
45
45
|
icon: (
|
|
46
46
|
<svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.75" viewBox="0 0 24 24">
|
|
47
47
|
<circle cx="12" cy="12" r="10" />
|
|
@@ -50,7 +50,7 @@ const variantConfig: Record<
|
|
|
50
50
|
),
|
|
51
51
|
},
|
|
52
52
|
success: {
|
|
53
|
-
tile: "
|
|
53
|
+
tile: "var(--tile-success)",
|
|
54
54
|
icon: (
|
|
55
55
|
<svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.75" viewBox="0 0 24 24">
|
|
56
56
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
@@ -58,7 +58,7 @@ const variantConfig: Record<
|
|
|
58
58
|
),
|
|
59
59
|
},
|
|
60
60
|
warning: {
|
|
61
|
-
tile: "
|
|
61
|
+
tile: "var(--tile-warning)",
|
|
62
62
|
icon: (
|
|
63
63
|
<svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.75" viewBox="0 0 24 24">
|
|
64
64
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
|
@@ -66,7 +66,7 @@ const variantConfig: Record<
|
|
|
66
66
|
),
|
|
67
67
|
},
|
|
68
68
|
error: {
|
|
69
|
-
tile: "
|
|
69
|
+
tile: "var(--tile-error)",
|
|
70
70
|
icon: (
|
|
71
71
|
<svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.75" viewBox="0 0 24 24">
|
|
72
72
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
@@ -199,26 +199,29 @@ function ToastCard({
|
|
|
199
199
|
style={{ background: "var(--panel-bezel)", boxShadow: "var(--overlay-shadow)" }}
|
|
200
200
|
>
|
|
201
201
|
<div
|
|
202
|
-
className="bg-
|
|
202
|
+
className="bg-panel-face rounded-[13px] pl-3 pr-2 py-3 flex items-start gap-3"
|
|
203
203
|
style={{ boxShadow: "var(--panel-face-shadow)" }}
|
|
204
204
|
>
|
|
205
205
|
<div
|
|
206
206
|
aria-hidden
|
|
207
|
-
className=
|
|
207
|
+
className={[
|
|
208
|
+
"shrink-0 w-7 h-7 rounded-[8px] flex items-center justify-center",
|
|
209
|
+
// the default tile is the inverse face; status tiles stay white-on-colour
|
|
210
|
+
tile === "var(--face-inverse)" ? "text-ink-inverse" : "text-white",
|
|
211
|
+
].join(" ")}
|
|
208
212
|
style={{
|
|
209
213
|
background: tile,
|
|
210
|
-
boxShadow:
|
|
211
|
-
"0 0 3px rgba(0,0,0,0.212), 0 1px 2px rgba(0,0,0,0.15), inset 0 2px 3px -2px rgba(255,255,255,0.188)",
|
|
214
|
+
boxShadow: "var(--tile-status-shadow)",
|
|
212
215
|
}}
|
|
213
216
|
>
|
|
214
217
|
{icon}
|
|
215
218
|
</div>
|
|
216
219
|
<div className="flex-1 min-w-0 pt-[5px]">
|
|
217
|
-
<p className="text-[13px] font-semibold text-
|
|
220
|
+
<p className="text-[13px] font-semibold text-ink leading-tight tracking-[-0.01em]">
|
|
218
221
|
{item.title}
|
|
219
222
|
</p>
|
|
220
223
|
{item.description && (
|
|
221
|
-
<p className="text-[12.5px] text-
|
|
224
|
+
<p className="text-[12.5px] text-ink-muted mt-1 leading-relaxed">
|
|
222
225
|
{item.description}
|
|
223
226
|
</p>
|
|
224
227
|
)}
|
|
@@ -371,8 +374,8 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|
|
371
374
|
}}
|
|
372
375
|
>
|
|
373
376
|
<span
|
|
374
|
-
className="text-[10.5px] font-medium text-
|
|
375
|
-
style={{ background: "var(--face
|
|
377
|
+
className="text-[10.5px] font-medium text-ink-muted px-2 py-0.5 rounded-full"
|
|
378
|
+
style={{ background: "var(--face)", boxShadow: "var(--face-shadow)" }}
|
|
376
379
|
>
|
|
377
380
|
+{showCount} more
|
|
378
381
|
</span>
|
package/dist/registry/Toggle.tsx
CHANGED
|
@@ -42,12 +42,12 @@ export function Toggle({
|
|
|
42
42
|
{(label || description) && (
|
|
43
43
|
<div className="flex flex-col gap-0.5 min-w-0">
|
|
44
44
|
{label && (
|
|
45
|
-
<span id={`${uid}-label`} className="text-[13px] font-medium text-
|
|
45
|
+
<span id={`${uid}-label`} className="text-[13px] font-medium text-ink leading-snug">
|
|
46
46
|
{label}
|
|
47
47
|
</span>
|
|
48
48
|
)}
|
|
49
49
|
{description && (
|
|
50
|
-
<span id={`${uid}-desc`} className="text-[12px] text-
|
|
50
|
+
<span id={`${uid}-desc`} className="text-[12px] text-ink-muted leading-relaxed">
|
|
51
51
|
{description}
|
|
52
52
|
</span>
|
|
53
53
|
)}
|
|
@@ -68,14 +68,12 @@ export function Toggle({
|
|
|
68
68
|
className={[
|
|
69
69
|
"relative shrink-0 w-[46px] h-[26px] rounded-[13px] cursor-pointer",
|
|
70
70
|
"transition-[background-color,box-shadow] duration-200 ease-out",
|
|
71
|
-
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-
|
|
72
|
-
isOn ? "bg-
|
|
71
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink",
|
|
72
|
+
isOn ? "bg-track-on" : "bg-track",
|
|
73
73
|
].join(" ")}
|
|
74
74
|
style={{
|
|
75
75
|
/* The track is a recessed channel; the knob is a raised face inside it */
|
|
76
|
-
boxShadow: isOn
|
|
77
|
-
? "inset 0 1px 3px rgba(0,0,0,0.5), inset 0 0 2px rgba(0,0,0,0.3), 0 1px 2px -1px rgba(255,255,255,0.48)"
|
|
78
|
-
: "inset 0 1px 2px rgba(17,17,17,0.12), inset 0 0 2px rgba(17,17,17,0.12), 0 1px 2px -1px rgba(255,255,255,0.64)",
|
|
76
|
+
boxShadow: isOn ? "var(--track-on-shadow)" : "var(--track-shadow)",
|
|
79
77
|
}}
|
|
80
78
|
>
|
|
81
79
|
<span
|
|
@@ -85,9 +83,8 @@ export function Toggle({
|
|
|
85
83
|
left: 3,
|
|
86
84
|
/* The knob stretches while held, like a finger dragging it */
|
|
87
85
|
width: pressed ? 24 : 20,
|
|
88
|
-
background: "var(--face
|
|
89
|
-
boxShadow:
|
|
90
|
-
"0 0 3px rgba(17,17,17,0.119), 0 1px 2px rgba(0,0,0,0.14), 0 3px 6px -1px rgba(0,0,0,0.12), inset 0 2px 3px -2px rgba(255,255,255,1), inset 0 -2px 3px -2px rgba(0,0,0,0.04)",
|
|
86
|
+
background: "var(--face)",
|
|
87
|
+
boxShadow: "var(--knob-shadow)",
|
|
91
88
|
transform: `translateX(${isOn ? (pressed ? 16 : 20) : 0}px) translateY(-50%)`,
|
|
92
89
|
transition:
|
|
93
90
|
"transform 260ms var(--ease-spring), width 200ms var(--ease-out-soft)",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useRef, useCallback, useEffect, useId } from "react";
|
|
4
|
+
import { ENTER_FROM, LAYER, ORIGIN, Portal, useAnchoredPosition } from "./Floating";
|
|
4
5
|
|
|
5
6
|
type TooltipPosition = "top" | "bottom" | "left" | "right";
|
|
6
7
|
|
|
@@ -11,20 +12,6 @@ interface TooltipProps {
|
|
|
11
12
|
className?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
const origin: Record<TooltipPosition, string> = {
|
|
15
|
-
top: "origin-bottom",
|
|
16
|
-
bottom: "origin-top",
|
|
17
|
-
left: "origin-right",
|
|
18
|
-
right: "origin-left",
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const enterTranslate: Record<TooltipPosition, string> = {
|
|
22
|
-
top: "translate-y-1",
|
|
23
|
-
bottom: "-translate-y-1",
|
|
24
|
-
left: "translate-x-1",
|
|
25
|
-
right: "-translate-x-1",
|
|
26
|
-
};
|
|
27
|
-
|
|
28
15
|
export function Tooltip({
|
|
29
16
|
content,
|
|
30
17
|
position = "top",
|
|
@@ -34,9 +21,7 @@ export function Tooltip({
|
|
|
34
21
|
const [open, setOpen] = useState(false);
|
|
35
22
|
const [visible, setVisible] = useState(false);
|
|
36
23
|
const [mounted, setMounted] = useState(false);
|
|
37
|
-
const [coords, setCoords] = useState({ top: 0, left: 0 });
|
|
38
24
|
const triggerRef = useRef<HTMLDivElement>(null);
|
|
39
|
-
const tooltipRef = useRef<HTMLDivElement>(null);
|
|
40
25
|
const enterTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
41
26
|
const exitTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
42
27
|
const autoHideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -65,54 +50,18 @@ export function Tooltip({
|
|
|
65
50
|
}
|
|
66
51
|
}, [hide]);
|
|
67
52
|
|
|
53
|
+
// Mounted during render; the effect only waits for it to paint
|
|
54
|
+
if (open && !mounted) setMounted(true);
|
|
55
|
+
|
|
68
56
|
useEffect(() => {
|
|
69
57
|
if (open) {
|
|
70
|
-
setMounted(true);
|
|
71
58
|
requestAnimationFrame(() => requestAnimationFrame(() => setVisible(true)));
|
|
72
59
|
}
|
|
73
60
|
}, [open]);
|
|
74
61
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const tp = tooltipRef.current.getBoundingClientRect();
|
|
79
|
-
const gap = 7;
|
|
80
|
-
|
|
81
|
-
let top = 0;
|
|
82
|
-
let left = 0;
|
|
83
|
-
|
|
84
|
-
switch (position) {
|
|
85
|
-
case "top":
|
|
86
|
-
top = -(tp.height + gap);
|
|
87
|
-
left = (tr.width - tp.width) / 2;
|
|
88
|
-
break;
|
|
89
|
-
case "bottom":
|
|
90
|
-
top = tr.height + gap;
|
|
91
|
-
left = (tr.width - tp.width) / 2;
|
|
92
|
-
break;
|
|
93
|
-
case "left":
|
|
94
|
-
top = (tr.height - tp.height) / 2;
|
|
95
|
-
left = -(tp.width + gap);
|
|
96
|
-
break;
|
|
97
|
-
case "right":
|
|
98
|
-
top = (tr.height - tp.height) / 2;
|
|
99
|
-
left = tr.width + gap;
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const viewportPadding = 8;
|
|
104
|
-
const absLeft = triggerRef.current.getBoundingClientRect().left + left;
|
|
105
|
-
const overflowRight = absLeft + tp.width - (window.innerWidth - viewportPadding);
|
|
106
|
-
const overflowLeft = viewportPadding - absLeft;
|
|
107
|
-
if (overflowRight > 0) left -= overflowRight;
|
|
108
|
-
if (overflowLeft > 0) left += overflowLeft;
|
|
109
|
-
|
|
110
|
-
setCoords({ top, left });
|
|
111
|
-
}, [position]);
|
|
112
|
-
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
if (mounted) measure();
|
|
115
|
-
}, [mounted, measure]);
|
|
62
|
+
/* Portalled to the body and placed before its first paint, flipping
|
|
63
|
+
to the other side when there's no room */
|
|
64
|
+
const { setFloating, side } = useAnchoredPosition(triggerRef, { open: mounted, side: position, align: "center", gap: 7 });
|
|
116
65
|
|
|
117
66
|
useEffect(() => {
|
|
118
67
|
if (!open) return;
|
|
@@ -183,29 +132,28 @@ export function Tooltip({
|
|
|
183
132
|
})
|
|
184
133
|
: children}
|
|
185
134
|
{mounted && (
|
|
135
|
+
<Portal>
|
|
186
136
|
<div
|
|
187
|
-
ref={
|
|
137
|
+
ref={setFloating}
|
|
188
138
|
id={`${uid}-tip`}
|
|
189
139
|
role="tooltip"
|
|
190
|
-
className=
|
|
191
|
-
"absolute z-50 px-2.5 py-1.5 rounded-[9px] text-white text-[11.5px] font-medium whitespace-nowrap pointer-events-none",
|
|
192
|
-
"transition-[opacity,transform,scale,translate] duration-150 ease-[var(--ease-spring)]",
|
|
193
|
-
origin[position],
|
|
194
|
-
visible
|
|
195
|
-
? "opacity-100 scale-100 translate-x-0 translate-y-0"
|
|
196
|
-
: ["opacity-0 scale-95", enterTranslate[position]].join(" "),
|
|
197
|
-
].join(" ")}
|
|
140
|
+
className="px-2.5 py-1.5 rounded-[9px] text-ink-inverse text-[11.5px] font-medium whitespace-nowrap pointer-events-none transition-[opacity,transform] duration-150 ease-[var(--ease-spring)]"
|
|
198
141
|
style={{
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
142
|
+
position: "fixed",
|
|
143
|
+
top: 0,
|
|
144
|
+
left: 0,
|
|
145
|
+
zIndex: LAYER.tooltip,
|
|
146
|
+
transformOrigin: ORIGIN[side],
|
|
147
|
+
opacity: visible ? 1 : 0,
|
|
148
|
+
transform: visible ? "none" : `${ENTER_FROM[side]} scale(0.95)`,
|
|
149
|
+
background: "var(--face-inverse)",
|
|
150
|
+
boxShadow: "var(--tooltip-shadow)",
|
|
151
|
+
textShadow: "var(--emboss-inverse)",
|
|
205
152
|
}}
|
|
206
153
|
>
|
|
207
154
|
{content}
|
|
208
155
|
</div>
|
|
156
|
+
</Portal>
|
|
209
157
|
)}
|
|
210
158
|
</div>
|
|
211
159
|
);
|