@vaam-apps/ui 0.1.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/CHANGELOG.md +104 -0
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/dist/components/data/code.d.ts +37 -0
- package/dist/components/data/code.d.ts.map +1 -0
- package/dist/components/data/code.js +37 -0
- package/dist/components/data/code.js.map +1 -0
- package/dist/components/data/copy-button.d.ts +39 -0
- package/dist/components/data/copy-button.d.ts.map +1 -0
- package/dist/components/data/copy-button.js +50 -0
- package/dist/components/data/copy-button.js.map +1 -0
- package/dist/components/data/detail-row.d.ts +69 -0
- package/dist/components/data/detail-row.d.ts.map +1 -0
- package/dist/components/data/detail-row.js +27 -0
- package/dist/components/data/detail-row.js.map +1 -0
- package/dist/components/data/id-display.d.ts +33 -0
- package/dist/components/data/id-display.d.ts.map +1 -0
- package/dist/components/data/id-display.js +25 -0
- package/dist/components/data/id-display.js.map +1 -0
- package/dist/components/data/masked-value.d.ts +47 -0
- package/dist/components/data/masked-value.d.ts.map +1 -0
- package/dist/components/data/masked-value.js +46 -0
- package/dist/components/data/masked-value.js.map +1 -0
- package/dist/components/data/money.d.ts +28 -0
- package/dist/components/data/money.d.ts.map +1 -0
- package/dist/components/data/money.js +22 -0
- package/dist/components/data/money.js.map +1 -0
- package/dist/components/data/phone-display.d.ts +45 -0
- package/dist/components/data/phone-display.d.ts.map +1 -0
- package/dist/components/data/phone-display.js +23 -0
- package/dist/components/data/phone-display.js.map +1 -0
- package/dist/components/data/stat-tile.d.ts +33 -0
- package/dist/components/data/stat-tile.d.ts.map +1 -0
- package/dist/components/data/stat-tile.js +18 -0
- package/dist/components/data/stat-tile.js.map +1 -0
- package/dist/components/data/timestamp-display.d.ts +14 -0
- package/dist/components/data/timestamp-display.d.ts.map +1 -0
- package/dist/components/data/timestamp-display.js +77 -0
- package/dist/components/data/timestamp-display.js.map +1 -0
- package/dist/components/patterns/inline-banner.d.ts +22 -0
- package/dist/components/patterns/inline-banner.d.ts.map +1 -0
- package/dist/components/patterns/inline-banner.js +11 -0
- package/dist/components/patterns/inline-banner.js.map +1 -0
- package/dist/components/patterns/inline-empty-state.d.ts +20 -0
- package/dist/components/patterns/inline-empty-state.d.ts.map +1 -0
- package/dist/components/patterns/inline-empty-state.js +11 -0
- package/dist/components/patterns/inline-empty-state.js.map +1 -0
- package/dist/components/patterns/live-row.d.ts +23 -0
- package/dist/components/patterns/live-row.d.ts.map +1 -0
- package/dist/components/patterns/live-row.js +48 -0
- package/dist/components/patterns/live-row.js.map +1 -0
- package/dist/components/patterns/payload-inspector.d.ts +25 -0
- package/dist/components/patterns/payload-inspector.d.ts.map +1 -0
- package/dist/components/patterns/payload-inspector.js +33 -0
- package/dist/components/patterns/payload-inspector.js.map +1 -0
- package/dist/components/patterns/route-skeleton.d.ts +15 -0
- package/dist/components/patterns/route-skeleton.d.ts.map +1 -0
- package/dist/components/patterns/route-skeleton.js +7 -0
- package/dist/components/patterns/route-skeleton.js.map +1 -0
- package/dist/components/patterns/screen-layout.d.ts +14 -0
- package/dist/components/patterns/screen-layout.d.ts.map +1 -0
- package/dist/components/patterns/screen-layout.js +11 -0
- package/dist/components/patterns/screen-layout.js.map +1 -0
- package/dist/components/patterns/stale-write-banner.d.ts +26 -0
- package/dist/components/patterns/stale-write-banner.d.ts.map +1 -0
- package/dist/components/patterns/stale-write-banner.js +28 -0
- package/dist/components/patterns/stale-write-banner.js.map +1 -0
- package/dist/components/patterns/state-timeline.d.ts +58 -0
- package/dist/components/patterns/state-timeline.d.ts.map +1 -0
- package/dist/components/patterns/state-timeline.js +103 -0
- package/dist/components/patterns/state-timeline.js.map +1 -0
- package/dist/components/primitives/badge.d.ts +27 -0
- package/dist/components/primitives/badge.d.ts.map +1 -0
- package/dist/components/primitives/badge.js +33 -0
- package/dist/components/primitives/badge.js.map +1 -0
- package/dist/components/primitives/button.d.ts +12 -0
- package/dist/components/primitives/button.d.ts.map +1 -0
- package/dist/components/primitives/button.js +42 -0
- package/dist/components/primitives/button.js.map +1 -0
- package/dist/components/primitives/calendar.d.ts +39 -0
- package/dist/components/primitives/calendar.d.ts.map +1 -0
- package/dist/components/primitives/calendar.js +102 -0
- package/dist/components/primitives/calendar.js.map +1 -0
- package/dist/components/primitives/card.d.ts +24 -0
- package/dist/components/primitives/card.d.ts.map +1 -0
- package/dist/components/primitives/card.js +28 -0
- package/dist/components/primitives/card.js.map +1 -0
- package/dist/components/primitives/checkbox.d.ts +42 -0
- package/dist/components/primitives/checkbox.d.ts.map +1 -0
- package/dist/components/primitives/checkbox.js +31 -0
- package/dist/components/primitives/checkbox.js.map +1 -0
- package/dist/components/primitives/chip-select.d.ts +38 -0
- package/dist/components/primitives/chip-select.d.ts.map +1 -0
- package/dist/components/primitives/chip-select.js +25 -0
- package/dist/components/primitives/chip-select.js.map +1 -0
- package/dist/components/primitives/command-menu.d.ts +54 -0
- package/dist/components/primitives/command-menu.d.ts.map +1 -0
- package/dist/components/primitives/command-menu.js +24 -0
- package/dist/components/primitives/command-menu.js.map +1 -0
- package/dist/components/primitives/confirm-dialog.d.ts +52 -0
- package/dist/components/primitives/confirm-dialog.d.ts.map +1 -0
- package/dist/components/primitives/confirm-dialog.js +30 -0
- package/dist/components/primitives/confirm-dialog.js.map +1 -0
- package/dist/components/primitives/date-picker.d.ts +68 -0
- package/dist/components/primitives/date-picker.d.ts.map +1 -0
- package/dist/components/primitives/date-picker.js +107 -0
- package/dist/components/primitives/date-picker.js.map +1 -0
- package/dist/components/primitives/dialog.d.ts +50 -0
- package/dist/components/primitives/dialog.d.ts.map +1 -0
- package/dist/components/primitives/dialog.js +90 -0
- package/dist/components/primitives/dialog.js.map +1 -0
- package/dist/components/primitives/drawer.d.ts +71 -0
- package/dist/components/primitives/drawer.d.ts.map +1 -0
- package/dist/components/primitives/drawer.js +93 -0
- package/dist/components/primitives/drawer.js.map +1 -0
- package/dist/components/primitives/dropdown-menu.d.ts +40 -0
- package/dist/components/primitives/dropdown-menu.d.ts.map +1 -0
- package/dist/components/primitives/dropdown-menu.js +53 -0
- package/dist/components/primitives/dropdown-menu.js.map +1 -0
- package/dist/components/primitives/form-field.d.ts +94 -0
- package/dist/components/primitives/form-field.d.ts.map +1 -0
- package/dist/components/primitives/form-field.js +27 -0
- package/dist/components/primitives/form-field.js.map +1 -0
- package/dist/components/primitives/inline-confirm.d.ts +62 -0
- package/dist/components/primitives/inline-confirm.d.ts.map +1 -0
- package/dist/components/primitives/inline-confirm.js +11 -0
- package/dist/components/primitives/inline-confirm.js.map +1 -0
- package/dist/components/primitives/input.d.ts +4 -0
- package/dist/components/primitives/input.d.ts.map +1 -0
- package/dist/components/primitives/input.js +13 -0
- package/dist/components/primitives/input.js.map +1 -0
- package/dist/components/primitives/label.d.ts +26 -0
- package/dist/components/primitives/label.d.ts.map +1 -0
- package/dist/components/primitives/label.js +33 -0
- package/dist/components/primitives/label.js.map +1 -0
- package/dist/components/primitives/pagination.d.ts +54 -0
- package/dist/components/primitives/pagination.d.ts.map +1 -0
- package/dist/components/primitives/pagination.js +31 -0
- package/dist/components/primitives/pagination.js.map +1 -0
- package/dist/components/primitives/popover.d.ts +12 -0
- package/dist/components/primitives/popover.d.ts.map +1 -0
- package/dist/components/primitives/popover.js +22 -0
- package/dist/components/primitives/popover.js.map +1 -0
- package/dist/components/primitives/progress.d.ts +25 -0
- package/dist/components/primitives/progress.d.ts.map +1 -0
- package/dist/components/primitives/progress.js +25 -0
- package/dist/components/primitives/progress.js.map +1 -0
- package/dist/components/primitives/radio-group.d.ts +47 -0
- package/dist/components/primitives/radio-group.d.ts.map +1 -0
- package/dist/components/primitives/radio-group.js +11 -0
- package/dist/components/primitives/radio-group.js.map +1 -0
- package/dist/components/primitives/select.d.ts +39 -0
- package/dist/components/primitives/select.d.ts.map +1 -0
- package/dist/components/primitives/select.js +142 -0
- package/dist/components/primitives/select.js.map +1 -0
- package/dist/components/primitives/separator.d.ts +9 -0
- package/dist/components/primitives/separator.d.ts.map +1 -0
- package/dist/components/primitives/separator.js +16 -0
- package/dist/components/primitives/separator.js.map +1 -0
- package/dist/components/primitives/side-nav.d.ts +65 -0
- package/dist/components/primitives/side-nav.d.ts.map +1 -0
- package/dist/components/primitives/side-nav.js +100 -0
- package/dist/components/primitives/side-nav.js.map +1 -0
- package/dist/components/primitives/skeleton.d.ts +22 -0
- package/dist/components/primitives/skeleton.d.ts.map +1 -0
- package/dist/components/primitives/skeleton.js +25 -0
- package/dist/components/primitives/skeleton.js.map +1 -0
- package/dist/components/primitives/spinner.d.ts +20 -0
- package/dist/components/primitives/spinner.d.ts.map +1 -0
- package/dist/components/primitives/spinner.js +21 -0
- package/dist/components/primitives/spinner.js.map +1 -0
- package/dist/components/primitives/switch.d.ts +29 -0
- package/dist/components/primitives/switch.d.ts.map +1 -0
- package/dist/components/primitives/switch.js +24 -0
- package/dist/components/primitives/switch.js.map +1 -0
- package/dist/components/primitives/table.d.ts +23 -0
- package/dist/components/primitives/table.d.ts.map +1 -0
- package/dist/components/primitives/table.js +81 -0
- package/dist/components/primitives/table.js.map +1 -0
- package/dist/components/primitives/tabs.d.ts +38 -0
- package/dist/components/primitives/tabs.d.ts.map +1 -0
- package/dist/components/primitives/tabs.js +84 -0
- package/dist/components/primitives/tabs.js.map +1 -0
- package/dist/components/primitives/textarea.d.ts +4 -0
- package/dist/components/primitives/textarea.d.ts.map +1 -0
- package/dist/components/primitives/textarea.js +11 -0
- package/dist/components/primitives/textarea.js.map +1 -0
- package/dist/components/primitives/toast.d.ts +24 -0
- package/dist/components/primitives/toast.d.ts.map +1 -0
- package/dist/components/primitives/toast.js +58 -0
- package/dist/components/primitives/toast.js.map +1 -0
- package/dist/components/primitives/tooltip.d.ts +30 -0
- package/dist/components/primitives/tooltip.d.ts.map +1 -0
- package/dist/components/primitives/tooltip.js +6 -0
- package/dist/components/primitives/tooltip.js.map +1 -0
- package/dist/components/status/state-chip.d.ts +53 -0
- package/dist/components/status/state-chip.d.ts.map +1 -0
- package/dist/components/status/state-chip.js +8 -0
- package/dist/components/status/state-chip.js.map +1 -0
- package/dist/components/status/state-mark.d.ts +21 -0
- package/dist/components/status/state-mark.d.ts.map +1 -0
- package/dist/components/status/state-mark.js +90 -0
- package/dist/components/status/state-mark.js.map +1 -0
- package/dist/components/status/status-pill.d.ts +62 -0
- package/dist/components/status/status-pill.d.ts.map +1 -0
- package/dist/components/status/status-pill.js +65 -0
- package/dist/components/status/status-pill.js.map +1 -0
- package/dist/components/status/status-tokens.d.ts +110 -0
- package/dist/components/status/status-tokens.d.ts.map +1 -0
- package/dist/components/status/status-tokens.js +102 -0
- package/dist/components/status/status-tokens.js.map +1 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cn.d.ts +11 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +78 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/lib/money.d.ts +73 -0
- package/dist/lib/money.d.ts.map +1 -0
- package/dist/lib/money.js +123 -0
- package/dist/lib/money.js.map +1 -0
- package/dist/lib/omit-undefined.d.ts +17 -0
- package/dist/lib/omit-undefined.d.ts.map +1 -0
- package/dist/lib/omit-undefined.js +24 -0
- package/dist/lib/omit-undefined.js.map +1 -0
- package/package.json +100 -0
- package/src/components/data/code.tsx +43 -0
- package/src/components/data/copy-button.tsx +97 -0
- package/src/components/data/detail-row.tsx +113 -0
- package/src/components/data/id-display.tsx +49 -0
- package/src/components/data/masked-value.tsx +96 -0
- package/src/components/data/money.tsx +46 -0
- package/src/components/data/phone-display.tsx +60 -0
- package/src/components/data/stat-tile.tsx +68 -0
- package/src/components/data/timestamp-display.tsx +106 -0
- package/src/components/patterns/inline-banner.tsx +60 -0
- package/src/components/patterns/inline-empty-state.tsx +45 -0
- package/src/components/patterns/live-row.tsx +75 -0
- package/src/components/patterns/payload-inspector.tsx +157 -0
- package/src/components/patterns/route-skeleton.tsx +77 -0
- package/src/components/patterns/screen-layout.tsx +44 -0
- package/src/components/patterns/stale-write-banner.tsx +44 -0
- package/src/components/patterns/state-timeline.tsx +229 -0
- package/src/components/primitives/badge.tsx +40 -0
- package/src/components/primitives/button.tsx +54 -0
- package/src/components/primitives/calendar.tsx +136 -0
- package/src/components/primitives/card.tsx +70 -0
- package/src/components/primitives/checkbox.tsx +101 -0
- package/src/components/primitives/chip-select.tsx +105 -0
- package/src/components/primitives/command-menu.tsx +92 -0
- package/src/components/primitives/confirm-dialog.tsx +109 -0
- package/src/components/primitives/date-picker.tsx +271 -0
- package/src/components/primitives/dialog.tsx +195 -0
- package/src/components/primitives/drawer.tsx +326 -0
- package/src/components/primitives/dropdown-menu.tsx +128 -0
- package/src/components/primitives/form-field.tsx +136 -0
- package/src/components/primitives/inline-confirm.tsx +118 -0
- package/src/components/primitives/input.tsx +21 -0
- package/src/components/primitives/label.tsx +43 -0
- package/src/components/primitives/pagination.tsx +110 -0
- package/src/components/primitives/popover.tsx +41 -0
- package/src/components/primitives/progress.tsx +66 -0
- package/src/components/primitives/radio-group.tsx +90 -0
- package/src/components/primitives/select.tsx +254 -0
- package/src/components/primitives/separator.tsx +49 -0
- package/src/components/primitives/side-nav.tsx +253 -0
- package/src/components/primitives/skeleton.tsx +25 -0
- package/src/components/primitives/spinner.tsx +39 -0
- package/src/components/primitives/switch.tsx +74 -0
- package/src/components/primitives/table.tsx +157 -0
- package/src/components/primitives/tabs.tsx +163 -0
- package/src/components/primitives/textarea.tsx +21 -0
- package/src/components/primitives/toast.tsx +117 -0
- package/src/components/primitives/tooltip.tsx +38 -0
- package/src/components/status/state-chip.tsx +70 -0
- package/src/components/status/state-mark.tsx +154 -0
- package/src/components/status/status-pill.tsx +153 -0
- package/src/components/status/status-tokens.ts +173 -0
- package/src/index.ts +96 -0
- package/src/lib/cn.ts +168 -0
- package/src/lib/money.ts +164 -0
- package/src/lib/omit-undefined.ts +25 -0
- package/src/styles/theme.css +381 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Popover as HeadlessPopover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
|
4
|
+
import { CalendarDays, X } from "lucide-react";
|
|
5
|
+
import type { DateRange } from "react-day-picker";
|
|
6
|
+
import { cn } from "../../lib/cn";
|
|
7
|
+
import { omitUndefined } from "../../lib/omit-undefined";
|
|
8
|
+
import { Calendar } from "./calendar";
|
|
9
|
+
|
|
10
|
+
export type { DateRange };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `2026-09-11` — ISO 8601 calendar date, no time, no zone.
|
|
14
|
+
*
|
|
15
|
+
* Every date this module exchanges with a caller is one of these, not a
|
|
16
|
+
* `Date`. A `Date` is an instant, and an instant rendered in a different
|
|
17
|
+
* zone is a different calendar day — which is how a filter for "today"
|
|
18
|
+
* silently returns yesterday's rows for anyone west of the server. The
|
|
19
|
+
* `Date` objects `react-day-picker` works in are confined to this file.
|
|
20
|
+
*/
|
|
21
|
+
export type IsoDate = string;
|
|
22
|
+
|
|
23
|
+
/** `Date` (local midnight) → `YYYY-MM-DD`, without going through UTC. */
|
|
24
|
+
export function toIsoDate(date: Date): IsoDate {
|
|
25
|
+
const year = date.getFullYear();
|
|
26
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
27
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
28
|
+
return `${year}-${month}-${day}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `YYYY-MM-DD` → `Date` at *local* midnight.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately not `new Date("2026-09-11")`, which the spec parses as
|
|
35
|
+
* UTC midnight — in any negative-offset zone that is the previous day,
|
|
36
|
+
* so a calendar built on it highlights the wrong cell.
|
|
37
|
+
*/
|
|
38
|
+
export function fromIsoDate(iso: IsoDate): Date | undefined {
|
|
39
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
40
|
+
if (match === null) return undefined;
|
|
41
|
+
const [, year, month, day] = match as unknown as [string, string, string, string];
|
|
42
|
+
return new Date(Number(year), Number(month) - 1, Number(day));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function displayLabel(iso: IsoDate | undefined, placeholder: string): string {
|
|
46
|
+
return iso === undefined || iso === "" ? placeholder : iso;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface TriggerProps {
|
|
50
|
+
/** Shown when nothing is selected. */
|
|
51
|
+
placeholder: string;
|
|
52
|
+
label: string;
|
|
53
|
+
hasValue: boolean;
|
|
54
|
+
disabled: boolean | undefined;
|
|
55
|
+
onClear: (() => void) | undefined;
|
|
56
|
+
className: string | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The field-looking button that opens the calendar.
|
|
61
|
+
*
|
|
62
|
+
* A `<button>`, not an `<input>`: the value is chosen from the calendar,
|
|
63
|
+
* and a text input that looks editable but is not is worse than a button
|
|
64
|
+
* that looks like a field. Callers who want typed entry should use a
|
|
65
|
+
* plain `<Input type="date">` — the browser's own picker is better than
|
|
66
|
+
* anything worth reimplementing here.
|
|
67
|
+
*/
|
|
68
|
+
function PickerTrigger({
|
|
69
|
+
placeholder,
|
|
70
|
+
label,
|
|
71
|
+
hasValue,
|
|
72
|
+
disabled,
|
|
73
|
+
onClear,
|
|
74
|
+
className,
|
|
75
|
+
}: TriggerProps) {
|
|
76
|
+
return (
|
|
77
|
+
<div className={cn("relative inline-flex w-full items-center", className)}>
|
|
78
|
+
<PopoverButton
|
|
79
|
+
{...omitUndefined({ disabled })}
|
|
80
|
+
className={cn(
|
|
81
|
+
"input input-bordered flex w-full items-center gap-2 text-left font-sans text-prose",
|
|
82
|
+
!hasValue && "text-subtle-foreground",
|
|
83
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
84
|
+
hasValue && onClear !== undefined && "pr-9",
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<CalendarDays size={14} strokeWidth={1.5} className="shrink-0 text-subtle-foreground" />
|
|
88
|
+
<span className="truncate font-mono tabular-nums">{label}</span>
|
|
89
|
+
<span className="sr-only">{placeholder}</span>
|
|
90
|
+
</PopoverButton>
|
|
91
|
+
{hasValue && onClear !== undefined && (
|
|
92
|
+
// Outside the PopoverButton on purpose: nesting a button inside a
|
|
93
|
+
// button is invalid HTML, and browsers resolve it by hoisting the
|
|
94
|
+
// inner one out, which silently detaches the click handler.
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={onClear}
|
|
98
|
+
aria-label={`Clear ${placeholder}`}
|
|
99
|
+
className="-translate-y-1/2 absolute top-1/2 right-2 text-subtle-foreground hover:text-foreground"
|
|
100
|
+
>
|
|
101
|
+
<X size={14} strokeWidth={1.5} />
|
|
102
|
+
</button>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const PANEL_CLASS = cn(
|
|
109
|
+
"z-50 rounded-md border border-edge bg-surface-2 shadow-[var(--shadow-popover)]",
|
|
110
|
+
"[--anchor-gap:6px] focus:outline-none",
|
|
111
|
+
"origin-top transition duration-100 ease-out data-closed:scale-95 data-closed:opacity-0",
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
export interface DatePickerProps {
|
|
115
|
+
value: IsoDate | undefined;
|
|
116
|
+
onValueChange: (value: IsoDate | undefined) => void;
|
|
117
|
+
/** Shown when empty, and used as the control's accessible name. */
|
|
118
|
+
placeholder?: string;
|
|
119
|
+
/** Earliest selectable date, inclusive. */
|
|
120
|
+
min?: IsoDate | undefined;
|
|
121
|
+
/** Latest selectable date, inclusive. */
|
|
122
|
+
max?: IsoDate | undefined;
|
|
123
|
+
disabled?: boolean | undefined;
|
|
124
|
+
/** Show an inline clear affordance once a date is picked. */
|
|
125
|
+
clearable?: boolean;
|
|
126
|
+
className?: string | undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A single calendar date, chosen from a popover calendar. */
|
|
130
|
+
export function DatePicker({
|
|
131
|
+
value,
|
|
132
|
+
onValueChange,
|
|
133
|
+
placeholder = "Pick a date",
|
|
134
|
+
min,
|
|
135
|
+
max,
|
|
136
|
+
disabled,
|
|
137
|
+
clearable = true,
|
|
138
|
+
className,
|
|
139
|
+
}: DatePickerProps) {
|
|
140
|
+
const selected = value === undefined ? undefined : fromIsoDate(value);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<HeadlessPopover className={cn("relative", className)}>
|
|
144
|
+
{({ close }) => (
|
|
145
|
+
<>
|
|
146
|
+
<PickerTrigger
|
|
147
|
+
placeholder={placeholder}
|
|
148
|
+
label={displayLabel(value, placeholder)}
|
|
149
|
+
hasValue={value !== undefined && value !== ""}
|
|
150
|
+
disabled={disabled}
|
|
151
|
+
onClear={clearable ? () => onValueChange(undefined) : undefined}
|
|
152
|
+
className={undefined}
|
|
153
|
+
/>
|
|
154
|
+
<PopoverPanel anchor="bottom start" transition className={PANEL_CLASS}>
|
|
155
|
+
<Calendar
|
|
156
|
+
mode="single"
|
|
157
|
+
{...omitUndefined({
|
|
158
|
+
selected,
|
|
159
|
+
defaultMonth: selected,
|
|
160
|
+
startMonth: min === undefined ? undefined : fromIsoDate(min),
|
|
161
|
+
endMonth: max === undefined ? undefined : fromIsoDate(max),
|
|
162
|
+
})}
|
|
163
|
+
onSelect={(next) => {
|
|
164
|
+
onValueChange(next === undefined ? undefined : toIsoDate(next));
|
|
165
|
+
// Closing on pick is right for a single date and wrong for
|
|
166
|
+
// a range, where the first click is only half the answer.
|
|
167
|
+
if (next !== undefined) close();
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
</PopoverPanel>
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
</HeadlessPopover>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Two ISO dates, inclusive at both ends. `undefined` at either end means open. */
|
|
178
|
+
export interface IsoDateRange {
|
|
179
|
+
from?: IsoDate | undefined;
|
|
180
|
+
to?: IsoDate | undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface DateRangePickerProps {
|
|
184
|
+
value: IsoDateRange | undefined;
|
|
185
|
+
onValueChange: (value: IsoDateRange | undefined) => void;
|
|
186
|
+
placeholder?: string;
|
|
187
|
+
min?: IsoDate | undefined;
|
|
188
|
+
max?: IsoDate | undefined;
|
|
189
|
+
disabled?: boolean | undefined;
|
|
190
|
+
clearable?: boolean;
|
|
191
|
+
/** Months shown side by side. Two makes a cross-month range selectable
|
|
192
|
+
* without navigating, which is the common case for "last 30 days". */
|
|
193
|
+
numberOfMonths?: number;
|
|
194
|
+
className?: string | undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function rangeLabel(value: IsoDateRange | undefined, placeholder: string): string {
|
|
198
|
+
if (value === undefined) return placeholder;
|
|
199
|
+
const { from, to } = value;
|
|
200
|
+
if (from === undefined && to === undefined) return placeholder;
|
|
201
|
+
if (from !== undefined && to === undefined) return `${from} → …`;
|
|
202
|
+
if (from === undefined && to !== undefined) return `… → ${to}`;
|
|
203
|
+
return `${from} → ${to}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A start and end date as one control.
|
|
208
|
+
*
|
|
209
|
+
* Exists because the alternative — two `<input type="date">` side by side
|
|
210
|
+
* — is what applications actually ship, and it cannot express the one
|
|
211
|
+
* thing a range needs: that the two ends constrain each other. Here the
|
|
212
|
+
* second click is always after the first, the fill between them is
|
|
213
|
+
* visible, and a half-finished range is representable (`from` set, `to`
|
|
214
|
+
* open) rather than being a validation error.
|
|
215
|
+
*/
|
|
216
|
+
export function DateRangePicker({
|
|
217
|
+
value,
|
|
218
|
+
onValueChange,
|
|
219
|
+
placeholder = "Pick a date range",
|
|
220
|
+
min,
|
|
221
|
+
max,
|
|
222
|
+
disabled,
|
|
223
|
+
clearable = true,
|
|
224
|
+
numberOfMonths = 2,
|
|
225
|
+
className,
|
|
226
|
+
}: DateRangePickerProps) {
|
|
227
|
+
const selected: DateRange | undefined =
|
|
228
|
+
value === undefined
|
|
229
|
+
? undefined
|
|
230
|
+
: {
|
|
231
|
+
from: value.from === undefined ? undefined : fromIsoDate(value.from),
|
|
232
|
+
to: value.to === undefined ? undefined : fromIsoDate(value.to),
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const hasValue = value !== undefined && (value.from !== undefined || value.to !== undefined);
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<HeadlessPopover className={cn("relative", className)}>
|
|
239
|
+
<PickerTrigger
|
|
240
|
+
placeholder={placeholder}
|
|
241
|
+
label={rangeLabel(value, placeholder)}
|
|
242
|
+
hasValue={hasValue}
|
|
243
|
+
disabled={disabled}
|
|
244
|
+
onClear={clearable ? () => onValueChange(undefined) : undefined}
|
|
245
|
+
className={undefined}
|
|
246
|
+
/>
|
|
247
|
+
<PopoverPanel anchor="bottom start" transition className={PANEL_CLASS}>
|
|
248
|
+
<Calendar
|
|
249
|
+
mode="range"
|
|
250
|
+
numberOfMonths={numberOfMonths}
|
|
251
|
+
{...omitUndefined({
|
|
252
|
+
selected,
|
|
253
|
+
defaultMonth: selected?.from,
|
|
254
|
+
startMonth: min === undefined ? undefined : fromIsoDate(min),
|
|
255
|
+
endMonth: max === undefined ? undefined : fromIsoDate(max),
|
|
256
|
+
})}
|
|
257
|
+
onSelect={(next) => {
|
|
258
|
+
if (next === undefined) {
|
|
259
|
+
onValueChange(undefined);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
onValueChange({
|
|
263
|
+
from: next.from === undefined ? undefined : toIsoDate(next.from),
|
|
264
|
+
to: next.to === undefined ? undefined : toIsoDate(next.to),
|
|
265
|
+
});
|
|
266
|
+
}}
|
|
267
|
+
/>
|
|
268
|
+
</PopoverPanel>
|
|
269
|
+
</HeadlessPopover>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DialogBackdrop,
|
|
5
|
+
DialogPanel,
|
|
6
|
+
Dialog as HeadlessDialog,
|
|
7
|
+
DialogDescription as HeadlessDialogDescription,
|
|
8
|
+
DialogTitle as HeadlessDialogTitle,
|
|
9
|
+
} from "@headlessui/react";
|
|
10
|
+
import { X } from "lucide-react";
|
|
11
|
+
import {
|
|
12
|
+
type ComponentPropsWithoutRef,
|
|
13
|
+
createContext,
|
|
14
|
+
type ElementType,
|
|
15
|
+
type MouseEvent,
|
|
16
|
+
type ReactNode,
|
|
17
|
+
useContext,
|
|
18
|
+
useState,
|
|
19
|
+
} from "react";
|
|
20
|
+
import { cn } from "../../lib/cn";
|
|
21
|
+
|
|
22
|
+
interface DialogContextValue {
|
|
23
|
+
open: boolean;
|
|
24
|
+
setOpen: (open: boolean) => void;
|
|
25
|
+
}
|
|
26
|
+
const DialogContext = createContext<DialogContextValue | null>(null);
|
|
27
|
+
|
|
28
|
+
function useDialogContext(component: string): DialogContextValue {
|
|
29
|
+
const ctx = useContext(DialogContext);
|
|
30
|
+
if (ctx === null) {
|
|
31
|
+
throw new Error(`<${component} /> must be rendered inside <Dialog>.`);
|
|
32
|
+
}
|
|
33
|
+
return ctx;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DialogProps {
|
|
37
|
+
open?: boolean;
|
|
38
|
+
defaultOpen?: boolean;
|
|
39
|
+
onOpenChange?: (open: boolean) => void;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* D3: Radix `Dialog` → Headless UI `Dialog`/`DialogPanel`/`DialogTitle`/`DialogBackdrop`.
|
|
45
|
+
*
|
|
46
|
+
* `Dialog` itself is **not** a Headless UI component — it's a plain context
|
|
47
|
+
* provider. Headless UI's own `Dialog` only wraps the modal
|
|
48
|
+
* backdrop/panel (`DialogContent` below) and is controlled-only
|
|
49
|
+
* (`open`/`onClose`); it has no equivalent to Radix's `Dialog.Trigger`,
|
|
50
|
+
* which rendered in place (outside any portal) and could itself drive an
|
|
51
|
+
* *uncontrolled* open state. Two real consumers need both shapes:
|
|
52
|
+
* `providers-screen.tsx`'s edit dialog is fully controlled
|
|
53
|
+
* (`open={selectedId !== null}`, no `DialogTrigger` at all), while the
|
|
54
|
+
* gallery's demo dialog is uncontrolled, opened by its own in-place
|
|
55
|
+
* `DialogTrigger`. This root supplies the open/close plumbing for both,
|
|
56
|
+
* the same controlled/uncontrolled duality `Select` (D17) uses.
|
|
57
|
+
*/
|
|
58
|
+
export function Dialog({ open, defaultOpen = false, onOpenChange, children }: DialogProps) {
|
|
59
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
60
|
+
const isControlled = open !== undefined;
|
|
61
|
+
const currentOpen = isControlled ? open : internalOpen;
|
|
62
|
+
|
|
63
|
+
function setOpen(next: boolean) {
|
|
64
|
+
if (!isControlled) setInternalOpen(next);
|
|
65
|
+
onOpenChange?.(next);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<DialogContext.Provider value={{ open: currentOpen, setOpen }}>
|
|
70
|
+
{children}
|
|
71
|
+
</DialogContext.Provider>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type TriggerProps<T extends ElementType> = { as?: T } & Omit<ComponentPropsWithoutRef<T>, "as">;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Renders in place, not portaled — `as` selects the rendered element/component,
|
|
79
|
+
* matching Headless UI's own polymorphism convention rather than Radix's
|
|
80
|
+
* `asChild` (D4 already retired that pattern for `Button`; this is the
|
|
81
|
+
* identical call applied to a trigger). Replaces
|
|
82
|
+
* `<DialogTrigger asChild><Button variant="secondary">…</Button></DialogTrigger>`
|
|
83
|
+
* with `<DialogTrigger as={Button} variant="secondary">…</DialogTrigger>`
|
|
84
|
+
* (the gallery's own only consumer, updated in this same change).
|
|
85
|
+
*/
|
|
86
|
+
export function DialogTrigger<T extends ElementType = "button">({
|
|
87
|
+
as,
|
|
88
|
+
onClick,
|
|
89
|
+
...props
|
|
90
|
+
}: TriggerProps<T>) {
|
|
91
|
+
const { setOpen } = useDialogContext("DialogTrigger");
|
|
92
|
+
const Component = (as ?? "button") as ElementType;
|
|
93
|
+
return (
|
|
94
|
+
<Component
|
|
95
|
+
type={as === undefined ? "button" : undefined}
|
|
96
|
+
onClick={(event: MouseEvent) => {
|
|
97
|
+
(onClick as ((e: MouseEvent) => void) | undefined)?.(event);
|
|
98
|
+
setOpen(true);
|
|
99
|
+
}}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Unconsumed today (grepped across `admin/`) — kept for API parity. Same
|
|
106
|
+
* `as`-polymorphic shape as `DialogTrigger`. */
|
|
107
|
+
export function DialogClose<T extends ElementType = "button">({
|
|
108
|
+
as,
|
|
109
|
+
onClick,
|
|
110
|
+
...props
|
|
111
|
+
}: TriggerProps<T>) {
|
|
112
|
+
const { setOpen } = useDialogContext("DialogClose");
|
|
113
|
+
const Component = (as ?? "button") as ElementType;
|
|
114
|
+
return (
|
|
115
|
+
<Component
|
|
116
|
+
type={as === undefined ? "button" : undefined}
|
|
117
|
+
onClick={(event: MouseEvent) => {
|
|
118
|
+
(onClick as ((e: MouseEvent) => void) | undefined)?.(event);
|
|
119
|
+
setOpen(false);
|
|
120
|
+
}}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Radius bumps to --radius-md here on purpose (design doc §3.5, unchanged by
|
|
127
|
+
// D8's radius-scale rewrite — see theme.css): a floating layer reads as
|
|
128
|
+
// detached from the grid beneath it.
|
|
129
|
+
export function DialogContent({ className, children, ...props }: ComponentPropsWithoutRef<"div">) {
|
|
130
|
+
const { open, setOpen } = useDialogContext("DialogContent");
|
|
131
|
+
return (
|
|
132
|
+
<HeadlessDialog open={open} onClose={setOpen} className="relative z-50">
|
|
133
|
+
<DialogBackdrop
|
|
134
|
+
transition
|
|
135
|
+
className="fixed inset-0 bg-black/50 duration-150 ease-out data-closed:opacity-0"
|
|
136
|
+
/>
|
|
137
|
+
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
|
|
138
|
+
<DialogPanel
|
|
139
|
+
transition
|
|
140
|
+
className={cn(
|
|
141
|
+
"relative w-full max-w-[480px] rounded-md border border-edge bg-surface-2 p-6 shadow-[var(--shadow-dialog)]",
|
|
142
|
+
"duration-150 ease-out data-closed:scale-95 data-closed:opacity-0",
|
|
143
|
+
className,
|
|
144
|
+
)}
|
|
145
|
+
{...props}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
aria-label="Close"
|
|
151
|
+
onClick={() => setOpen(false)}
|
|
152
|
+
className="absolute top-4 right-4 text-subtle-foreground hover:text-foreground"
|
|
153
|
+
>
|
|
154
|
+
<X size={16} strokeWidth={1.5} />
|
|
155
|
+
</button>
|
|
156
|
+
</DialogPanel>
|
|
157
|
+
</div>
|
|
158
|
+
</HeadlessDialog>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
163
|
+
return <div className={cn("mb-4 flex flex-col gap-1", className)} {...props} />;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** #56: the confirm/requeue dialog's own action row. Mirrors `DialogHeader`'s
|
|
167
|
+
* shape (a thin, class-composing div, not a Headless-UI-wrapped primitive —
|
|
168
|
+
* a footer has no accessibility semantics either library needs to own). */
|
|
169
|
+
export function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
170
|
+
return <div className={cn("mt-6 flex items-center justify-end gap-2", className)} {...props} />;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function DialogTitle({
|
|
174
|
+
className,
|
|
175
|
+
...props
|
|
176
|
+
}: ComponentPropsWithoutRef<typeof HeadlessDialogTitle>) {
|
|
177
|
+
return (
|
|
178
|
+
<HeadlessDialogTitle
|
|
179
|
+
className={cn("font-medium text-foreground text-title-sm", className)}
|
|
180
|
+
{...props}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function DialogDescription({
|
|
186
|
+
className,
|
|
187
|
+
...props
|
|
188
|
+
}: ComponentPropsWithoutRef<typeof HeadlessDialogDescription>) {
|
|
189
|
+
return (
|
|
190
|
+
<HeadlessDialogDescription
|
|
191
|
+
className={cn("text-muted-foreground text-prose", className)}
|
|
192
|
+
{...props}
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
}
|