@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,232 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useId, useRef, useState } from "react";
|
|
4
|
+
import { Calendar, CalendarIcon, ClearKey, DateRange, sameDay, startOfDay, toISODate } from "./DatePicker";
|
|
5
|
+
import { Portal } from "./Floating";
|
|
6
|
+
import { FieldLabel, FieldMessage } from "./FieldMessage";
|
|
7
|
+
import { useFloatingPanel } from "./useFloatingPanel";
|
|
8
|
+
|
|
9
|
+
export type { DateRange };
|
|
10
|
+
|
|
11
|
+
export interface DateRangePreset {
|
|
12
|
+
label: string;
|
|
13
|
+
range: () => DateRange;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface DateRangePickerProps {
|
|
17
|
+
value?: DateRange;
|
|
18
|
+
defaultValue?: DateRange;
|
|
19
|
+
onChange?: (range: DateRange) => void;
|
|
20
|
+
label?: string;
|
|
21
|
+
hint?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
min?: Date;
|
|
25
|
+
max?: Date;
|
|
26
|
+
isDateDisabled?: (date: Date) => boolean;
|
|
27
|
+
weekStartsOn?: 0 | 1;
|
|
28
|
+
locale?: string;
|
|
29
|
+
/** Shortcuts down the side, like "last 7 days" */
|
|
30
|
+
presets?: DateRangePreset[];
|
|
31
|
+
/** One month instead of two side by side */
|
|
32
|
+
singleMonth?: boolean;
|
|
33
|
+
clearable?: boolean;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
fullWidth?: boolean;
|
|
36
|
+
/** Submits name[from] and name[to] as yyyy-mm-dd */
|
|
37
|
+
name?: string;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const EMPTY: DateRange = { from: null, to: null };
|
|
42
|
+
|
|
43
|
+
/* The first click sets where the range starts, the second where it ends
|
|
44
|
+
(either way round), and the days between sink into a well as you hover.
|
|
45
|
+
Two months sit side by side, wrapping to one above the other on a
|
|
46
|
+
narrow screen. */
|
|
47
|
+
export function DateRangePicker({
|
|
48
|
+
value,
|
|
49
|
+
defaultValue = EMPTY,
|
|
50
|
+
onChange,
|
|
51
|
+
label,
|
|
52
|
+
hint,
|
|
53
|
+
error,
|
|
54
|
+
placeholder = "pick dates",
|
|
55
|
+
min,
|
|
56
|
+
max,
|
|
57
|
+
isDateDisabled,
|
|
58
|
+
weekStartsOn,
|
|
59
|
+
locale = "en-US",
|
|
60
|
+
presets,
|
|
61
|
+
singleMonth = false,
|
|
62
|
+
clearable = false,
|
|
63
|
+
disabled = false,
|
|
64
|
+
fullWidth = false,
|
|
65
|
+
name,
|
|
66
|
+
className = "",
|
|
67
|
+
}: DateRangePickerProps) {
|
|
68
|
+
const uid = useId();
|
|
69
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
70
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
71
|
+
const [internal, setInternal] = useState<DateRange>(defaultValue);
|
|
72
|
+
const range = value ?? internal;
|
|
73
|
+
// Mid-selection: the start is down, the end follows the pointer
|
|
74
|
+
const [draft, setDraft] = useState<Date | null>(null);
|
|
75
|
+
const [hover, setHover] = useState<Date | null>(null);
|
|
76
|
+
const { open, mounted, toggle, close, setFloating, panelStyle } = useFloatingPanel(ref, {
|
|
77
|
+
onEscape: () => buttonRef.current?.focus(),
|
|
78
|
+
});
|
|
79
|
+
const messageId = error || hint ? `${uid}-message` : undefined;
|
|
80
|
+
|
|
81
|
+
// Reset a half-made selection whenever the panel closes
|
|
82
|
+
const [wasOpen, setWasOpen] = useState(open);
|
|
83
|
+
if (open !== wasOpen) {
|
|
84
|
+
setWasOpen(open);
|
|
85
|
+
if (!open) {
|
|
86
|
+
setDraft(null);
|
|
87
|
+
setHover(null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const set = (r: DateRange) => {
|
|
92
|
+
setInternal(r);
|
|
93
|
+
onChange?.(r);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const pickDay = (d: Date) => {
|
|
97
|
+
if (!draft) {
|
|
98
|
+
setDraft(d);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const [from, to] = d < draft ? [d, draft] : [draft, d];
|
|
102
|
+
set({ from, to });
|
|
103
|
+
setDraft(null);
|
|
104
|
+
close();
|
|
105
|
+
buttonRef.current?.focus();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// What the calendar shades: the finished range, or the one being drawn
|
|
109
|
+
const shown: DateRange = draft
|
|
110
|
+
? hover && !sameDay(hover, draft)
|
|
111
|
+
? hover < draft
|
|
112
|
+
? { from: hover, to: draft }
|
|
113
|
+
: { from: draft, to: hover }
|
|
114
|
+
: { from: draft, to: draft }
|
|
115
|
+
: range;
|
|
116
|
+
|
|
117
|
+
const fmt = new Intl.DateTimeFormat(locale, { month: "short", day: "numeric", year: "numeric" });
|
|
118
|
+
const text =
|
|
119
|
+
range.from && range.to
|
|
120
|
+
? // formatRange drops the repeated month and year: "Aug 3 – 12, 2028"
|
|
121
|
+
fmt.formatRange(range.from, range.to)
|
|
122
|
+
: range.from
|
|
123
|
+
? `${fmt.format(range.from)} –`
|
|
124
|
+
: null;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div className={["group/field flex flex-col gap-1.5", fullWidth ? "w-full" : ""].join(" ")}>
|
|
128
|
+
{label && <FieldLabel id={`${uid}-label`}>{label}</FieldLabel>}
|
|
129
|
+
<div ref={ref} className={["relative", fullWidth ? "w-full" : "inline-block w-72", className].join(" ")}>
|
|
130
|
+
{/* The field's error styling keys off aria-invalid, as Input's does */}
|
|
131
|
+
{/* eslint-disable-next-line jsx-a11y/role-supports-aria-props */}
|
|
132
|
+
<button
|
|
133
|
+
ref={buttonRef}
|
|
134
|
+
type="button"
|
|
135
|
+
disabled={disabled}
|
|
136
|
+
onClick={toggle}
|
|
137
|
+
aria-haspopup="dialog"
|
|
138
|
+
aria-expanded={open}
|
|
139
|
+
aria-labelledby={label ? `${uid}-label ${uid}-value` : undefined}
|
|
140
|
+
aria-invalid={error ? true : undefined}
|
|
141
|
+
aria-describedby={messageId}
|
|
142
|
+
data-open={open || undefined}
|
|
143
|
+
className={[
|
|
144
|
+
"field group/date relative isolate flex w-full cursor-pointer items-center gap-2.5 rounded-xl py-3 pl-3.5 text-left text-[13.5px]",
|
|
145
|
+
clearable && range.from ? "pr-10" : "pr-4",
|
|
146
|
+
"focus:outline-none disabled:opacity-40 disabled:pointer-events-none",
|
|
147
|
+
].join(" ")}
|
|
148
|
+
>
|
|
149
|
+
<CalendarIcon
|
|
150
|
+
className={[
|
|
151
|
+
"shrink-0 transition-[color,transform] duration-300 ease-[var(--ease-spring)]",
|
|
152
|
+
open ? "scale-110 text-ink-soft" : "text-ink-faint group-hover/date:text-ink-body",
|
|
153
|
+
].join(" ")}
|
|
154
|
+
/>
|
|
155
|
+
<span id={`${uid}-value`} className={["truncate tabular-nums", text ? "font-medium text-ink" : "text-ink-faint"].join(" ")}>
|
|
156
|
+
{text ?? placeholder}
|
|
157
|
+
</span>
|
|
158
|
+
</button>
|
|
159
|
+
{clearable && range.from && !disabled && (
|
|
160
|
+
<ClearKey
|
|
161
|
+
label="clear dates"
|
|
162
|
+
onClear={() => {
|
|
163
|
+
set(EMPTY);
|
|
164
|
+
buttonRef.current?.focus();
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
{name && (
|
|
169
|
+
<>
|
|
170
|
+
<input type="hidden" name={`${name}[from]`} value={range.from ? toISODate(range.from) : ""} />
|
|
171
|
+
<input type="hidden" name={`${name}[to]`} value={range.to ? toISODate(range.to) : ""} />
|
|
172
|
+
</>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{mounted && (
|
|
177
|
+
<Portal>
|
|
178
|
+
<div
|
|
179
|
+
ref={setFloating}
|
|
180
|
+
role="dialog"
|
|
181
|
+
aria-label={label ? `choose ${label}` : "choose dates"}
|
|
182
|
+
className="max-w-[calc(100vw-16px)] rounded-[16px] p-1.5"
|
|
183
|
+
style={panelStyle}
|
|
184
|
+
>
|
|
185
|
+
<div
|
|
186
|
+
className="flex flex-col gap-3 rounded-[11px] bg-panel-face p-2.5 sm:flex-row"
|
|
187
|
+
style={{ boxShadow: "var(--panel-face-shadow)" }}
|
|
188
|
+
>
|
|
189
|
+
{presets && presets.length > 0 && (
|
|
190
|
+
<div className="flex shrink-0 flex-row flex-wrap gap-1 border-line-strong pb-2 sm:w-32 sm:flex-col sm:border-r sm:pb-0 sm:pr-2">
|
|
191
|
+
{presets.map((p) => (
|
|
192
|
+
<button
|
|
193
|
+
key={p.label}
|
|
194
|
+
type="button"
|
|
195
|
+
onClick={() => {
|
|
196
|
+
const r = p.range();
|
|
197
|
+
set({ from: r.from && startOfDay(r.from), to: r.to && startOfDay(r.to) });
|
|
198
|
+
close();
|
|
199
|
+
buttonRef.current?.focus();
|
|
200
|
+
}}
|
|
201
|
+
className="cursor-pointer rounded-[8px] px-2.5 py-1.5 text-left text-[12.5px] text-ink-body transition-colors hover:bg-surface-active hover:text-ink focus-visible:outline-2 focus-visible:outline-ink"
|
|
202
|
+
>
|
|
203
|
+
{p.label}
|
|
204
|
+
</button>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
<div className="flex flex-col gap-2">
|
|
209
|
+
<Calendar
|
|
210
|
+
range={shown}
|
|
211
|
+
onChange={pickDay}
|
|
212
|
+
onDayHover={setHover}
|
|
213
|
+
months={singleMonth ? 1 : 2}
|
|
214
|
+
min={min}
|
|
215
|
+
max={max}
|
|
216
|
+
isDateDisabled={isDateDisabled}
|
|
217
|
+
weekStartsOn={weekStartsOn}
|
|
218
|
+
locale={locale}
|
|
219
|
+
autoFocus
|
|
220
|
+
/>
|
|
221
|
+
<p aria-live="polite" className="px-1 text-[11.5px] text-ink-faint">
|
|
222
|
+
{draft ? "now pick the last day" : "pick the first day"}
|
|
223
|
+
</p>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</Portal>
|
|
228
|
+
)}
|
|
229
|
+
<FieldMessage id={messageId} error={error} hint={hint} />
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
package/dist/registry/Drawer.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { Portal } from "./Floating";
|
|
4
5
|
import { CloseButton } from "./CloseButton";
|
|
5
6
|
import { useModalBehavior } from "./useModalBehavior";
|
|
6
7
|
|
|
@@ -42,16 +43,15 @@ export function Drawer({
|
|
|
42
43
|
const uid = useId();
|
|
43
44
|
|
|
44
45
|
/* ── mount / unmount ────────────────────────────────── */
|
|
46
|
+
/* Mounting on open and sliding out on close happen during render, so
|
|
47
|
+
neither waits for an extra commit; only the unmount is delayed */
|
|
48
|
+
if (open && !mounted) setMounted(true);
|
|
49
|
+
if (!open && visible) setVisible(false);
|
|
50
|
+
|
|
45
51
|
useEffect(() => {
|
|
46
|
-
if (open)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
if (mounted) {
|
|
51
|
-
setVisible(false);
|
|
52
|
-
const t = setTimeout(() => setMounted(false), EXIT_MS);
|
|
53
|
-
return () => clearTimeout(t);
|
|
54
|
-
}
|
|
52
|
+
if (open || !mounted) return;
|
|
53
|
+
const t = setTimeout(() => setMounted(false), EXIT_MS);
|
|
54
|
+
return () => clearTimeout(t);
|
|
55
55
|
}, [open, mounted]);
|
|
56
56
|
|
|
57
57
|
/* ── trigger enter animation after mount ────────────── */
|
|
@@ -70,6 +70,7 @@ export function Drawer({
|
|
|
70
70
|
const isRight = side === "right";
|
|
71
71
|
|
|
72
72
|
return (
|
|
73
|
+
<Portal>
|
|
73
74
|
<div className="fixed inset-0 z-90">
|
|
74
75
|
<div
|
|
75
76
|
aria-hidden
|
|
@@ -117,7 +118,7 @@ export function Drawer({
|
|
|
117
118
|
}}
|
|
118
119
|
>
|
|
119
120
|
<div
|
|
120
|
-
className="flex flex-col h-full bg-
|
|
121
|
+
className="flex flex-col h-full bg-panel-face rounded-[16px] overflow-hidden"
|
|
121
122
|
style={{ boxShadow: "var(--panel-face-shadow)" }}
|
|
122
123
|
>
|
|
123
124
|
<div className="pl-6 pr-4 pt-5 pb-4 flex items-start gap-4 shrink-0">
|
|
@@ -125,13 +126,13 @@ export function Drawer({
|
|
|
125
126
|
{title && (
|
|
126
127
|
<h2
|
|
127
128
|
id={`${uid}-title`}
|
|
128
|
-
className="text-[16px] font-semibold text-
|
|
129
|
+
className="text-[16px] font-semibold text-ink tracking-[-0.015em] leading-snug"
|
|
129
130
|
>
|
|
130
131
|
{title}
|
|
131
132
|
</h2>
|
|
132
133
|
)}
|
|
133
134
|
{description && (
|
|
134
|
-
<p id={`${uid}-desc`} className="text-[13px] text-
|
|
135
|
+
<p id={`${uid}-desc`} className="text-[13px] text-ink-muted leading-relaxed">
|
|
135
136
|
{description}
|
|
136
137
|
</p>
|
|
137
138
|
)}
|
|
@@ -140,7 +141,7 @@ export function Drawer({
|
|
|
140
141
|
</div>
|
|
141
142
|
|
|
142
143
|
{children && (
|
|
143
|
-
<div className="flex-1 overflow-y-auto scroll-thin px-6 pb-5 text-[13.5px] text-
|
|
144
|
+
<div className="flex-1 overflow-y-auto scroll-thin px-6 pb-5 text-[13.5px] text-ink-body leading-relaxed">
|
|
144
145
|
{children}
|
|
145
146
|
</div>
|
|
146
147
|
)}
|
|
@@ -149,8 +150,8 @@ export function Drawer({
|
|
|
149
150
|
<div
|
|
150
151
|
className="mt-auto px-5 py-3.5 flex items-center justify-end gap-2 shrink-0"
|
|
151
152
|
style={{
|
|
152
|
-
background: "
|
|
153
|
-
boxShadow: "inset 0 1px 0
|
|
153
|
+
background: "var(--panel-tray)",
|
|
154
|
+
boxShadow: "inset 0 1px 0 var(--hairline)",
|
|
154
155
|
}}
|
|
155
156
|
>
|
|
156
157
|
{actions}
|
|
@@ -160,5 +161,6 @@ export function Drawer({
|
|
|
160
161
|
</div>
|
|
161
162
|
</div>
|
|
162
163
|
</div>
|
|
164
|
+
</Portal>
|
|
163
165
|
);
|
|
164
166
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useRef, useEffect, useId, useCallback, ReactNode } from "react";
|
|
4
4
|
import { Kbd } from "./Kbd";
|
|
5
|
+
import { ENTER_FROM, LAYER, Portal, useAnchoredPosition } from "./Floating";
|
|
5
6
|
|
|
6
7
|
type DropdownMenuPosition =
|
|
7
8
|
| "bottom-start"
|
|
@@ -27,20 +28,6 @@ interface DropdownMenuProps {
|
|
|
27
28
|
className?: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const positionStyles: Record<DropdownMenuPosition, string> = {
|
|
31
|
-
"bottom-start": "top-full left-0 mt-2 origin-top-left",
|
|
32
|
-
"bottom-end": "top-full right-0 mt-2 origin-top-right",
|
|
33
|
-
"top-start": "bottom-full left-0 mb-2 origin-bottom-left",
|
|
34
|
-
"top-end": "bottom-full right-0 mb-2 origin-bottom-right",
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const enterFrom: Record<DropdownMenuPosition, string> = {
|
|
38
|
-
"bottom-start": "-translate-y-1.5",
|
|
39
|
-
"bottom-end": "-translate-y-1.5",
|
|
40
|
-
"top-start": "translate-y-1.5",
|
|
41
|
-
"top-end": "translate-y-1.5",
|
|
42
|
-
};
|
|
43
|
-
|
|
44
31
|
export function DropdownMenu({
|
|
45
32
|
trigger,
|
|
46
33
|
items,
|
|
@@ -51,15 +38,21 @@ export function DropdownMenu({
|
|
|
51
38
|
const [open, setOpen] = useState(false);
|
|
52
39
|
const [visible, setVisible] = useState(false);
|
|
53
40
|
const [mounted, setMounted] = useState(false);
|
|
54
|
-
// Where the menu actually opens: the requested side unless that runs off-screen
|
|
55
|
-
const [placement, setPlacement] = useState(position);
|
|
56
|
-
const [shift, setShift] = useState(0);
|
|
57
41
|
const ref = useRef<HTMLDivElement>(null);
|
|
58
|
-
const menuRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
const menuRef = useRef<HTMLDivElement | null>(null);
|
|
59
43
|
const uid = useId();
|
|
44
|
+
const [v, h] = position.split("-") as ["top" | "bottom", "start" | "end"];
|
|
45
|
+
// Portalled and pinned to the trigger; flips up or over when it runs out of room
|
|
46
|
+
const { setFloating, side } = useAnchoredPosition(ref, { open: mounted, side: v, align: h });
|
|
47
|
+
const attachMenu = useCallback(
|
|
48
|
+
(el: HTMLDivElement | null) => {
|
|
49
|
+
menuRef.current = el;
|
|
50
|
+
setFloating(el);
|
|
51
|
+
},
|
|
52
|
+
[setFloating],
|
|
53
|
+
);
|
|
60
54
|
|
|
61
|
-
const enabledItems = () =>
|
|
62
|
-
Array.from(menuRef.current?.querySelectorAll<HTMLElement>('[role="menuitem"]:not([disabled])') ?? []);
|
|
55
|
+
const enabledItems = () => Array.from(menuRef.current?.querySelectorAll<HTMLElement>('[role="menuitem"]:not([disabled])') ?? []);
|
|
63
56
|
|
|
64
57
|
const focusItem = (index: number) => {
|
|
65
58
|
const list = enabledItems();
|
|
@@ -67,51 +60,20 @@ export function DropdownMenu({
|
|
|
67
60
|
list[(index + list.length) % list.length].focus();
|
|
68
61
|
};
|
|
69
62
|
|
|
70
|
-
/*
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const attachMenu = useCallback((menu: HTMLDivElement | null) => {
|
|
74
|
-
menuRef.current = menu;
|
|
75
|
-
const anchor = ref.current;
|
|
76
|
-
if (!menu || !anchor) return;
|
|
77
|
-
const gap = 8;
|
|
78
|
-
const a = anchor.getBoundingClientRect();
|
|
79
|
-
const w = menu.offsetWidth;
|
|
80
|
-
const h = menu.offsetHeight;
|
|
81
|
-
const vw = document.documentElement.clientWidth;
|
|
82
|
-
const vh = window.innerHeight;
|
|
83
|
-
|
|
84
|
-
let [v, side] = position.split("-") as ["top" | "bottom", "start" | "end"];
|
|
85
|
-
const fitsStart = a.left + w <= vw - gap;
|
|
86
|
-
const fitsEnd = a.right - w >= gap;
|
|
87
|
-
if (side === "end" && !fitsEnd && fitsStart) side = "start";
|
|
88
|
-
else if (side === "start" && !fitsStart && fitsEnd) side = "end";
|
|
89
|
-
|
|
90
|
-
const fitsBelow = a.bottom + gap + h <= vh - gap;
|
|
91
|
-
const fitsAbove = a.top - gap - h >= gap;
|
|
92
|
-
if (v === "bottom" && !fitsBelow && fitsAbove) v = "top";
|
|
93
|
-
else if (v === "top" && !fitsAbove && fitsBelow) v = "bottom";
|
|
94
|
-
|
|
95
|
-
// Neither side fits (narrow screens): slide it back inside the viewport
|
|
96
|
-
const left = side === "start" ? a.left : a.right - w;
|
|
97
|
-
const nudge = Math.max(gap - left, 0) - Math.max(left + w - (vw - gap), 0);
|
|
98
|
-
|
|
99
|
-
setPlacement(`${v}-${side}` as DropdownMenuPosition);
|
|
100
|
-
setShift(nudge);
|
|
101
|
-
}, [position]);
|
|
63
|
+
/* Mounted and faded out during render; the effect only times the rest */
|
|
64
|
+
if (open && !mounted) setMounted(true);
|
|
65
|
+
if (!open && visible) setVisible(false);
|
|
102
66
|
|
|
103
67
|
useEffect(() => {
|
|
104
68
|
if (open) {
|
|
105
|
-
setMounted(true);
|
|
106
69
|
requestAnimationFrame(() =>
|
|
107
70
|
requestAnimationFrame(() => {
|
|
108
71
|
setVisible(true);
|
|
109
72
|
// Land on the first item so arrow keys work straight away
|
|
110
|
-
|
|
73
|
+
enabledItems()[0]?.focus();
|
|
111
74
|
})
|
|
112
75
|
);
|
|
113
76
|
} else {
|
|
114
|
-
setVisible(false);
|
|
115
77
|
const t = setTimeout(() => setMounted(false), 180);
|
|
116
78
|
return () => clearTimeout(t);
|
|
117
79
|
}
|
|
@@ -119,9 +81,9 @@ export function DropdownMenu({
|
|
|
119
81
|
|
|
120
82
|
useEffect(() => {
|
|
121
83
|
function onOutside(e: MouseEvent) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
84
|
+
const t = e.target as Node;
|
|
85
|
+
if (ref.current?.contains(t) || menuRef.current?.contains(t)) return;
|
|
86
|
+
setOpen(false);
|
|
125
87
|
}
|
|
126
88
|
document.addEventListener("mousedown", onOutside);
|
|
127
89
|
return () => document.removeEventListener("mousedown", onOutside);
|
|
@@ -174,11 +136,6 @@ export function DropdownMenu({
|
|
|
174
136
|
<div
|
|
175
137
|
data-menu-trigger
|
|
176
138
|
onClick={() => {
|
|
177
|
-
if (!open) {
|
|
178
|
-
// Start each opening from the requested side; attachMenu flips if needed
|
|
179
|
-
setPlacement(position);
|
|
180
|
-
setShift(0);
|
|
181
|
-
}
|
|
182
139
|
setOpen(!open);
|
|
183
140
|
}}
|
|
184
141
|
className="inline-flex cursor-pointer"
|
|
@@ -192,28 +149,32 @@ export function DropdownMenu({
|
|
|
192
149
|
: trigger}
|
|
193
150
|
</div>
|
|
194
151
|
{mounted && (
|
|
152
|
+
<Portal>
|
|
195
153
|
<div
|
|
196
154
|
ref={attachMenu}
|
|
197
155
|
id={`${uid}-menu`}
|
|
198
156
|
role="menu"
|
|
199
157
|
onKeyDown={onMenuKeyDown}
|
|
200
|
-
className=
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
158
|
+
className="min-w-[200px] rounded-[16px] p-1.5 transition-[opacity,transform] duration-200 ease-[var(--ease-spring)]"
|
|
159
|
+
style={{
|
|
160
|
+
position: "fixed",
|
|
161
|
+
top: 0,
|
|
162
|
+
left: 0,
|
|
163
|
+
zIndex: LAYER.floating,
|
|
164
|
+
transformOrigin: `${side === "top" ? "bottom" : "top"} ${h === "start" ? "left" : "right"}`,
|
|
165
|
+
opacity: visible ? 1 : 0,
|
|
166
|
+
transform: visible ? "none" : `${ENTER_FROM[side]} scale(0.96)`,
|
|
167
|
+
background: "var(--panel-bezel)",
|
|
168
|
+
boxShadow: "var(--overlay-shadow)",
|
|
169
|
+
}}
|
|
209
170
|
>
|
|
210
171
|
<div
|
|
211
|
-
className="bg-
|
|
172
|
+
className="bg-panel-face rounded-[11px] p-1 flex flex-col"
|
|
212
173
|
style={{ boxShadow: "var(--panel-face-shadow)" }}
|
|
213
174
|
>
|
|
214
175
|
{items.map((item) => {
|
|
215
176
|
if (item.divider) {
|
|
216
|
-
return <div key={item.key} role="separator" className="h-px bg-
|
|
177
|
+
return <div key={item.key} role="separator" className="h-px bg-line my-1 mx-2" />;
|
|
217
178
|
}
|
|
218
179
|
|
|
219
180
|
return (
|
|
@@ -230,15 +191,15 @@ export function DropdownMenu({
|
|
|
230
191
|
"transition-[background,color] duration-100",
|
|
231
192
|
"disabled:opacity-35 disabled:pointer-events-none",
|
|
232
193
|
item.danger
|
|
233
|
-
? "text-
|
|
234
|
-
: "text-
|
|
194
|
+
? "text-danger focus:bg-danger-wash"
|
|
195
|
+
: "text-ink-label focus:text-ink focus:bg-surface-active/80",
|
|
235
196
|
].join(" ")}
|
|
236
197
|
>
|
|
237
198
|
{item.icon && (
|
|
238
199
|
<span
|
|
239
200
|
className={[
|
|
240
201
|
"shrink-0 w-4 h-4 flex items-center justify-center transition-colors duration-100",
|
|
241
|
-
item.danger ? "text-
|
|
202
|
+
item.danger ? "text-danger-soft" : "text-ink-faint group-focus/item:text-ink-label",
|
|
242
203
|
].join(" ")}
|
|
243
204
|
>
|
|
244
205
|
{item.icon}
|
|
@@ -251,6 +212,7 @@ export function DropdownMenu({
|
|
|
251
212
|
})}
|
|
252
213
|
</div>
|
|
253
214
|
</div>
|
|
215
|
+
</Portal>
|
|
254
216
|
)}
|
|
255
217
|
</div>
|
|
256
218
|
);
|
|
@@ -21,7 +21,7 @@ export function EmptyState({
|
|
|
21
21
|
style={{ background: "var(--panel-bezel)", boxShadow: "var(--panel-shadow)" }}
|
|
22
22
|
>
|
|
23
23
|
<div
|
|
24
|
-
className="relative bg-
|
|
24
|
+
className="relative bg-panel-face rounded-[15px] overflow-hidden flex flex-col items-center justify-center text-center py-14 px-6"
|
|
25
25
|
style={{ boxShadow: "var(--panel-face-shadow)" }}
|
|
26
26
|
>
|
|
27
27
|
{/* A faint dotted floor: the space is empty, not broken */}
|
|
@@ -29,24 +29,24 @@ export function EmptyState({
|
|
|
29
29
|
aria-hidden
|
|
30
30
|
className="pointer-events-none absolute inset-0"
|
|
31
31
|
style={{
|
|
32
|
-
backgroundImage: "radial-gradient(
|
|
32
|
+
backgroundImage: "radial-gradient(var(--dots) 1px, transparent 1px)",
|
|
33
33
|
backgroundSize: "14px 14px",
|
|
34
|
-
maskImage: "radial-gradient(60% 70% at 50% 40%,
|
|
35
|
-
WebkitMaskImage: "radial-gradient(60% 70% at 50% 40%,
|
|
34
|
+
maskImage: "radial-gradient(60% 70% at 50% 40%, black 0%, transparent 100%)",
|
|
35
|
+
WebkitMaskImage: "radial-gradient(60% 70% at 50% 40%, black 0%, transparent 100%)",
|
|
36
36
|
}}
|
|
37
37
|
/>
|
|
38
38
|
{icon && (
|
|
39
39
|
<div
|
|
40
40
|
aria-hidden
|
|
41
|
-
className="relative w-12 h-12 rounded-[14px] flex items-center justify-center text-
|
|
42
|
-
style={{ background: "var(--face
|
|
41
|
+
className="relative w-12 h-12 rounded-[14px] flex items-center justify-center text-ink-muted mb-5"
|
|
42
|
+
style={{ background: "var(--face)", boxShadow: "var(--face-shadow), var(--face-drop)" }}
|
|
43
43
|
>
|
|
44
44
|
{icon}
|
|
45
45
|
</div>
|
|
46
46
|
)}
|
|
47
|
-
<p className="relative text-[15px] font-semibold text-
|
|
47
|
+
<p className="relative text-[15px] font-semibold text-ink tracking-[-0.015em]">{title}</p>
|
|
48
48
|
{description && (
|
|
49
|
-
<p className="relative text-[13px] text-
|
|
49
|
+
<p className="relative text-[13px] text-ink-muted mt-1.5 max-w-xs leading-relaxed">{description}</p>
|
|
50
50
|
)}
|
|
51
51
|
{action && <div className="relative mt-6">{action}</div>}
|
|
52
52
|
</div>
|