@vaam-apps/ui 0.1.0 → 0.1.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/CHANGELOG.md +25 -0
- package/dist/components/data/code.d.ts +0 -1
- package/dist/components/data/code.js +0 -1
- package/dist/components/data/copy-button.d.ts +0 -1
- package/dist/components/data/copy-button.js +0 -1
- package/dist/components/data/detail-row.d.ts +0 -1
- package/dist/components/data/detail-row.js +0 -1
- package/dist/components/data/id-display.d.ts +0 -1
- package/dist/components/data/id-display.js +0 -1
- package/dist/components/data/masked-value.d.ts +0 -1
- package/dist/components/data/masked-value.js +0 -1
- package/dist/components/data/money.d.ts +0 -1
- package/dist/components/data/money.js +0 -1
- package/dist/components/data/phone-display.d.ts +0 -1
- package/dist/components/data/phone-display.js +0 -1
- package/dist/components/data/stat-tile.d.ts +0 -1
- package/dist/components/data/stat-tile.js +0 -1
- package/dist/components/data/timestamp-display.d.ts +0 -1
- package/dist/components/data/timestamp-display.js +0 -1
- package/dist/components/patterns/inline-banner.d.ts +0 -1
- package/dist/components/patterns/inline-banner.js +0 -1
- package/dist/components/patterns/inline-empty-state.d.ts +0 -1
- package/dist/components/patterns/inline-empty-state.js +0 -1
- package/dist/components/patterns/live-row.d.ts +0 -1
- package/dist/components/patterns/live-row.js +0 -1
- package/dist/components/patterns/payload-inspector.d.ts +0 -1
- package/dist/components/patterns/payload-inspector.js +0 -1
- package/dist/components/patterns/route-skeleton.d.ts +0 -1
- package/dist/components/patterns/route-skeleton.js +0 -1
- package/dist/components/patterns/screen-layout.d.ts +0 -1
- package/dist/components/patterns/screen-layout.js +0 -1
- package/dist/components/patterns/stale-write-banner.d.ts +0 -1
- package/dist/components/patterns/stale-write-banner.js +0 -1
- package/dist/components/patterns/state-timeline.d.ts +0 -1
- package/dist/components/patterns/state-timeline.js +0 -1
- package/dist/components/primitives/badge.d.ts +0 -1
- package/dist/components/primitives/badge.js +0 -1
- package/dist/components/primitives/button.d.ts +0 -1
- package/dist/components/primitives/button.js +0 -1
- package/dist/components/primitives/calendar.d.ts +0 -1
- package/dist/components/primitives/calendar.js +0 -1
- package/dist/components/primitives/card.d.ts +0 -1
- package/dist/components/primitives/card.js +0 -1
- package/dist/components/primitives/checkbox.d.ts +0 -1
- package/dist/components/primitives/checkbox.js +0 -1
- package/dist/components/primitives/chip-select.d.ts +0 -1
- package/dist/components/primitives/chip-select.js +0 -1
- package/dist/components/primitives/command-menu.d.ts +0 -1
- package/dist/components/primitives/command-menu.js +0 -1
- package/dist/components/primitives/confirm-dialog.d.ts +0 -1
- package/dist/components/primitives/confirm-dialog.js +0 -1
- package/dist/components/primitives/date-picker.d.ts +0 -1
- package/dist/components/primitives/date-picker.js +0 -1
- package/dist/components/primitives/dialog.d.ts +0 -1
- package/dist/components/primitives/dialog.js +0 -1
- package/dist/components/primitives/drawer.d.ts +0 -1
- package/dist/components/primitives/drawer.js +0 -1
- package/dist/components/primitives/dropdown-menu.d.ts +0 -1
- package/dist/components/primitives/dropdown-menu.js +0 -1
- package/dist/components/primitives/form-field.d.ts +0 -1
- package/dist/components/primitives/form-field.js +0 -1
- package/dist/components/primitives/inline-confirm.d.ts +0 -1
- package/dist/components/primitives/inline-confirm.js +0 -1
- package/dist/components/primitives/input.d.ts +0 -1
- package/dist/components/primitives/input.js +0 -1
- package/dist/components/primitives/label.d.ts +0 -1
- package/dist/components/primitives/label.js +0 -1
- package/dist/components/primitives/pagination.d.ts +0 -1
- package/dist/components/primitives/pagination.js +0 -1
- package/dist/components/primitives/popover.d.ts +0 -1
- package/dist/components/primitives/popover.js +0 -1
- package/dist/components/primitives/progress.d.ts +0 -1
- package/dist/components/primitives/progress.js +0 -1
- package/dist/components/primitives/radio-group.d.ts +0 -1
- package/dist/components/primitives/radio-group.js +0 -1
- package/dist/components/primitives/select.d.ts +0 -1
- package/dist/components/primitives/select.js +0 -1
- package/dist/components/primitives/separator.d.ts +0 -1
- package/dist/components/primitives/separator.js +0 -1
- package/dist/components/primitives/side-nav.d.ts +0 -1
- package/dist/components/primitives/side-nav.js +0 -1
- package/dist/components/primitives/skeleton.d.ts +0 -1
- package/dist/components/primitives/skeleton.js +0 -1
- package/dist/components/primitives/spinner.d.ts +0 -1
- package/dist/components/primitives/spinner.js +0 -1
- package/dist/components/primitives/switch.d.ts +0 -1
- package/dist/components/primitives/switch.js +0 -1
- package/dist/components/primitives/table.d.ts +0 -1
- package/dist/components/primitives/table.js +0 -1
- package/dist/components/primitives/tabs.d.ts +0 -1
- package/dist/components/primitives/tabs.js +0 -1
- package/dist/components/primitives/textarea.d.ts +0 -1
- package/dist/components/primitives/textarea.js +0 -1
- package/dist/components/primitives/toast.d.ts +0 -1
- package/dist/components/primitives/toast.js +0 -1
- package/dist/components/primitives/tooltip.d.ts +0 -1
- package/dist/components/primitives/tooltip.js +0 -1
- package/dist/components/status/state-chip.d.ts +0 -1
- package/dist/components/status/state-chip.js +0 -1
- package/dist/components/status/state-mark.d.ts +0 -1
- package/dist/components/status/state-mark.js +0 -1
- package/dist/components/status/status-pill.d.ts +0 -1
- package/dist/components/status/status-pill.js +0 -1
- package/dist/components/status/status-tokens.d.ts +0 -1
- package/dist/components/status/status-tokens.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/lib/cn.d.ts +0 -1
- package/dist/lib/cn.js +0 -1
- package/dist/lib/money.d.ts +0 -1
- package/dist/lib/money.js +0 -1
- package/dist/lib/omit-undefined.d.ts +0 -1
- package/dist/lib/omit-undefined.js +0 -1
- package/package.json +3 -7
- package/dist/components/data/code.d.ts.map +0 -1
- package/dist/components/data/code.js.map +0 -1
- package/dist/components/data/copy-button.d.ts.map +0 -1
- package/dist/components/data/copy-button.js.map +0 -1
- package/dist/components/data/detail-row.d.ts.map +0 -1
- package/dist/components/data/detail-row.js.map +0 -1
- package/dist/components/data/id-display.d.ts.map +0 -1
- package/dist/components/data/id-display.js.map +0 -1
- package/dist/components/data/masked-value.d.ts.map +0 -1
- package/dist/components/data/masked-value.js.map +0 -1
- package/dist/components/data/money.d.ts.map +0 -1
- package/dist/components/data/money.js.map +0 -1
- package/dist/components/data/phone-display.d.ts.map +0 -1
- package/dist/components/data/phone-display.js.map +0 -1
- package/dist/components/data/stat-tile.d.ts.map +0 -1
- package/dist/components/data/stat-tile.js.map +0 -1
- package/dist/components/data/timestamp-display.d.ts.map +0 -1
- package/dist/components/data/timestamp-display.js.map +0 -1
- package/dist/components/patterns/inline-banner.d.ts.map +0 -1
- package/dist/components/patterns/inline-banner.js.map +0 -1
- package/dist/components/patterns/inline-empty-state.d.ts.map +0 -1
- package/dist/components/patterns/inline-empty-state.js.map +0 -1
- package/dist/components/patterns/live-row.d.ts.map +0 -1
- package/dist/components/patterns/live-row.js.map +0 -1
- package/dist/components/patterns/payload-inspector.d.ts.map +0 -1
- package/dist/components/patterns/payload-inspector.js.map +0 -1
- package/dist/components/patterns/route-skeleton.d.ts.map +0 -1
- package/dist/components/patterns/route-skeleton.js.map +0 -1
- package/dist/components/patterns/screen-layout.d.ts.map +0 -1
- package/dist/components/patterns/screen-layout.js.map +0 -1
- package/dist/components/patterns/stale-write-banner.d.ts.map +0 -1
- package/dist/components/patterns/stale-write-banner.js.map +0 -1
- package/dist/components/patterns/state-timeline.d.ts.map +0 -1
- package/dist/components/patterns/state-timeline.js.map +0 -1
- package/dist/components/primitives/badge.d.ts.map +0 -1
- package/dist/components/primitives/badge.js.map +0 -1
- package/dist/components/primitives/button.d.ts.map +0 -1
- package/dist/components/primitives/button.js.map +0 -1
- package/dist/components/primitives/calendar.d.ts.map +0 -1
- package/dist/components/primitives/calendar.js.map +0 -1
- package/dist/components/primitives/card.d.ts.map +0 -1
- package/dist/components/primitives/card.js.map +0 -1
- package/dist/components/primitives/checkbox.d.ts.map +0 -1
- package/dist/components/primitives/checkbox.js.map +0 -1
- package/dist/components/primitives/chip-select.d.ts.map +0 -1
- package/dist/components/primitives/chip-select.js.map +0 -1
- package/dist/components/primitives/command-menu.d.ts.map +0 -1
- package/dist/components/primitives/command-menu.js.map +0 -1
- package/dist/components/primitives/confirm-dialog.d.ts.map +0 -1
- package/dist/components/primitives/confirm-dialog.js.map +0 -1
- package/dist/components/primitives/date-picker.d.ts.map +0 -1
- package/dist/components/primitives/date-picker.js.map +0 -1
- package/dist/components/primitives/dialog.d.ts.map +0 -1
- package/dist/components/primitives/dialog.js.map +0 -1
- package/dist/components/primitives/drawer.d.ts.map +0 -1
- package/dist/components/primitives/drawer.js.map +0 -1
- package/dist/components/primitives/dropdown-menu.d.ts.map +0 -1
- package/dist/components/primitives/dropdown-menu.js.map +0 -1
- package/dist/components/primitives/form-field.d.ts.map +0 -1
- package/dist/components/primitives/form-field.js.map +0 -1
- package/dist/components/primitives/inline-confirm.d.ts.map +0 -1
- package/dist/components/primitives/inline-confirm.js.map +0 -1
- package/dist/components/primitives/input.d.ts.map +0 -1
- package/dist/components/primitives/input.js.map +0 -1
- package/dist/components/primitives/label.d.ts.map +0 -1
- package/dist/components/primitives/label.js.map +0 -1
- package/dist/components/primitives/pagination.d.ts.map +0 -1
- package/dist/components/primitives/pagination.js.map +0 -1
- package/dist/components/primitives/popover.d.ts.map +0 -1
- package/dist/components/primitives/popover.js.map +0 -1
- package/dist/components/primitives/progress.d.ts.map +0 -1
- package/dist/components/primitives/progress.js.map +0 -1
- package/dist/components/primitives/radio-group.d.ts.map +0 -1
- package/dist/components/primitives/radio-group.js.map +0 -1
- package/dist/components/primitives/select.d.ts.map +0 -1
- package/dist/components/primitives/select.js.map +0 -1
- package/dist/components/primitives/separator.d.ts.map +0 -1
- package/dist/components/primitives/separator.js.map +0 -1
- package/dist/components/primitives/side-nav.d.ts.map +0 -1
- package/dist/components/primitives/side-nav.js.map +0 -1
- package/dist/components/primitives/skeleton.d.ts.map +0 -1
- package/dist/components/primitives/skeleton.js.map +0 -1
- package/dist/components/primitives/spinner.d.ts.map +0 -1
- package/dist/components/primitives/spinner.js.map +0 -1
- package/dist/components/primitives/switch.d.ts.map +0 -1
- package/dist/components/primitives/switch.js.map +0 -1
- package/dist/components/primitives/table.d.ts.map +0 -1
- package/dist/components/primitives/table.js.map +0 -1
- package/dist/components/primitives/tabs.d.ts.map +0 -1
- package/dist/components/primitives/tabs.js.map +0 -1
- package/dist/components/primitives/textarea.d.ts.map +0 -1
- package/dist/components/primitives/textarea.js.map +0 -1
- package/dist/components/primitives/toast.d.ts.map +0 -1
- package/dist/components/primitives/toast.js.map +0 -1
- package/dist/components/primitives/tooltip.d.ts.map +0 -1
- package/dist/components/primitives/tooltip.js.map +0 -1
- package/dist/components/status/state-chip.d.ts.map +0 -1
- package/dist/components/status/state-chip.js.map +0 -1
- package/dist/components/status/state-mark.d.ts.map +0 -1
- package/dist/components/status/state-mark.js.map +0 -1
- package/dist/components/status/status-pill.d.ts.map +0 -1
- package/dist/components/status/status-pill.js.map +0 -1
- package/dist/components/status/status-tokens.d.ts.map +0 -1
- package/dist/components/status/status-tokens.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/cn.d.ts.map +0 -1
- package/dist/lib/cn.js.map +0 -1
- package/dist/lib/money.d.ts.map +0 -1
- package/dist/lib/money.js.map +0 -1
- package/dist/lib/omit-undefined.d.ts.map +0 -1
- package/dist/lib/omit-undefined.js.map +0 -1
- package/src/components/data/code.tsx +0 -43
- package/src/components/data/copy-button.tsx +0 -97
- package/src/components/data/detail-row.tsx +0 -113
- package/src/components/data/id-display.tsx +0 -49
- package/src/components/data/masked-value.tsx +0 -96
- package/src/components/data/money.tsx +0 -46
- package/src/components/data/phone-display.tsx +0 -60
- package/src/components/data/stat-tile.tsx +0 -68
- package/src/components/data/timestamp-display.tsx +0 -106
- package/src/components/patterns/inline-banner.tsx +0 -60
- package/src/components/patterns/inline-empty-state.tsx +0 -45
- package/src/components/patterns/live-row.tsx +0 -75
- package/src/components/patterns/payload-inspector.tsx +0 -157
- package/src/components/patterns/route-skeleton.tsx +0 -77
- package/src/components/patterns/screen-layout.tsx +0 -44
- package/src/components/patterns/stale-write-banner.tsx +0 -44
- package/src/components/patterns/state-timeline.tsx +0 -229
- package/src/components/primitives/badge.tsx +0 -40
- package/src/components/primitives/button.tsx +0 -54
- package/src/components/primitives/calendar.tsx +0 -136
- package/src/components/primitives/card.tsx +0 -70
- package/src/components/primitives/checkbox.tsx +0 -101
- package/src/components/primitives/chip-select.tsx +0 -105
- package/src/components/primitives/command-menu.tsx +0 -92
- package/src/components/primitives/confirm-dialog.tsx +0 -109
- package/src/components/primitives/date-picker.tsx +0 -271
- package/src/components/primitives/dialog.tsx +0 -195
- package/src/components/primitives/drawer.tsx +0 -326
- package/src/components/primitives/dropdown-menu.tsx +0 -128
- package/src/components/primitives/form-field.tsx +0 -136
- package/src/components/primitives/inline-confirm.tsx +0 -118
- package/src/components/primitives/input.tsx +0 -21
- package/src/components/primitives/label.tsx +0 -43
- package/src/components/primitives/pagination.tsx +0 -110
- package/src/components/primitives/popover.tsx +0 -41
- package/src/components/primitives/progress.tsx +0 -66
- package/src/components/primitives/radio-group.tsx +0 -90
- package/src/components/primitives/select.tsx +0 -254
- package/src/components/primitives/separator.tsx +0 -49
- package/src/components/primitives/side-nav.tsx +0 -253
- package/src/components/primitives/skeleton.tsx +0 -25
- package/src/components/primitives/spinner.tsx +0 -39
- package/src/components/primitives/switch.tsx +0 -74
- package/src/components/primitives/table.tsx +0 -157
- package/src/components/primitives/tabs.tsx +0 -163
- package/src/components/primitives/textarea.tsx +0 -21
- package/src/components/primitives/toast.tsx +0 -117
- package/src/components/primitives/tooltip.tsx +0 -38
- package/src/components/status/state-chip.tsx +0 -70
- package/src/components/status/state-mark.tsx +0 -154
- package/src/components/status/status-pill.tsx +0 -153
- package/src/components/status/status-tokens.ts +0 -173
- package/src/index.ts +0 -96
- package/src/lib/cn.ts +0 -168
- package/src/lib/money.ts +0 -164
- package/src/lib/omit-undefined.ts +0 -25
- /package/{src → dist}/styles/theme.css +0 -0
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { X } from "lucide-react";
|
|
4
|
-
import { forwardRef, type ReactNode } from "react";
|
|
5
|
-
import { Drawer as DrawerPrimitive } from "vaul";
|
|
6
|
-
import { cn } from "../../lib/cn";
|
|
7
|
-
|
|
8
|
-
// vaul is standalone. Direction defaults to "right" — a side drawer reads
|
|
9
|
-
// as the console's Sheet-equivalent (design doc T15: "extend, don't fork");
|
|
10
|
-
// pass `direction="bottom"` for the mobile/narrow-viewport case.
|
|
11
|
-
//
|
|
12
|
-
// This generic composition (`Drawer`/`DrawerTrigger`/`DrawerClose`/
|
|
13
|
-
// `DrawerContent`) is unchanged by console-redesign.md §6.4/D14 — it is
|
|
14
|
-
// still the right tool for a one-off drawer with no quick-vs-more
|
|
15
|
-
// distinction to encode (see the gallery's own "Open drawer" example).
|
|
16
|
-
// `QuickDetailDrawer`/`MoreDetailDrawer` below are a *second*, self-
|
|
17
|
-
// contained API for the specific §3 distinction; they do not replace this
|
|
18
|
-
// one.
|
|
19
|
-
export const Drawer = DrawerPrimitive.Root;
|
|
20
|
-
export const DrawerTrigger = DrawerPrimitive.Trigger;
|
|
21
|
-
export const DrawerClose = DrawerPrimitive.Close;
|
|
22
|
-
export const DrawerPortal = DrawerPrimitive.Portal;
|
|
23
|
-
|
|
24
|
-
export const DrawerOverlay = forwardRef<
|
|
25
|
-
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
|
26
|
-
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
|
27
|
-
>(({ className, ...props }, ref) => (
|
|
28
|
-
<DrawerPrimitive.Overlay
|
|
29
|
-
ref={ref}
|
|
30
|
-
className={cn("fixed inset-0 z-50 bg-black/50", className)}
|
|
31
|
-
{...props}
|
|
32
|
-
/>
|
|
33
|
-
));
|
|
34
|
-
DrawerOverlay.displayName = "DrawerOverlay";
|
|
35
|
-
|
|
36
|
-
export const DrawerContent = forwardRef<
|
|
37
|
-
React.ElementRef<typeof DrawerPrimitive.Content>,
|
|
38
|
-
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
|
39
|
-
>(({ className, children, ...props }, ref) => (
|
|
40
|
-
<DrawerPortal>
|
|
41
|
-
<DrawerOverlay />
|
|
42
|
-
<DrawerPrimitive.Content
|
|
43
|
-
ref={ref}
|
|
44
|
-
className={cn(
|
|
45
|
-
"fixed inset-y-0 right-0 z-50 flex h-full w-full max-w-[560px] flex-col",
|
|
46
|
-
"border-edge border-l bg-surface-2 shadow-[var(--shadow-dialog)]",
|
|
47
|
-
className,
|
|
48
|
-
)}
|
|
49
|
-
{...props}
|
|
50
|
-
>
|
|
51
|
-
{children}
|
|
52
|
-
</DrawerPrimitive.Content>
|
|
53
|
-
</DrawerPortal>
|
|
54
|
-
));
|
|
55
|
-
DrawerContent.displayName = "DrawerContent";
|
|
56
|
-
|
|
57
|
-
/** Not previously exported — `vaul`'s `Content` renders Radix Dialog's
|
|
58
|
-
* `Content` underneath (`vaul/dist/index.js` imports `@radix-ui/react-
|
|
59
|
-
* dialog` directly and forwards its `Content`/`Title`/`Description`), so
|
|
60
|
-
* Radix's own "DialogContent requires a DialogTitle" dev warning applies
|
|
61
|
-
* here exactly as it does to `primitives/dialog.tsx`. `QuickDetailDrawer`/
|
|
62
|
-
* `MoreDetailDrawer` below always render one; exported separately too for
|
|
63
|
-
* any future one-off use of the generic `DrawerContent` above. */
|
|
64
|
-
export const DrawerTitle = forwardRef<
|
|
65
|
-
React.ElementRef<typeof DrawerPrimitive.Title>,
|
|
66
|
-
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
|
67
|
-
>(({ className, ...props }, ref) => (
|
|
68
|
-
<DrawerPrimitive.Title
|
|
69
|
-
ref={ref}
|
|
70
|
-
className={cn("font-medium text-foreground text-title-sm", className)}
|
|
71
|
-
{...props}
|
|
72
|
-
/>
|
|
73
|
-
));
|
|
74
|
-
DrawerTitle.displayName = "DrawerTitle";
|
|
75
|
-
|
|
76
|
-
/** See `DrawerTitle` — same "not previously exported, Radix warns without
|
|
77
|
-
* one" reasoning, for `aria-describedby` instead of the accessible name. */
|
|
78
|
-
export const DrawerDescription = forwardRef<
|
|
79
|
-
React.ElementRef<typeof DrawerPrimitive.Description>,
|
|
80
|
-
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
|
81
|
-
>(({ className, ...props }, ref) => (
|
|
82
|
-
<DrawerPrimitive.Description
|
|
83
|
-
ref={ref}
|
|
84
|
-
className={cn("text-muted-foreground text-prose", className)}
|
|
85
|
-
{...props}
|
|
86
|
-
/>
|
|
87
|
-
));
|
|
88
|
-
DrawerDescription.displayName = "DrawerDescription";
|
|
89
|
-
|
|
90
|
-
// ---------------------------------------------------------------------
|
|
91
|
-
// QuickDetailDrawer / MoreDetailDrawer (console-redesign.md §3, D14, §5)
|
|
92
|
-
// ---------------------------------------------------------------------
|
|
93
|
-
//
|
|
94
|
-
// The design doc's own §6.4 sketch of this file was explicitly flagged as
|
|
95
|
-
// unverified ("the mechanism is now known and worth checking before this
|
|
96
|
-
// sketch is trusted verbatim") for two independent reasons, both checked
|
|
97
|
-
// directly against `vaul@1.1.2`'s compiled source
|
|
98
|
-
// (`node_modules/vaul/dist/index.js`) before writing this, not assumed:
|
|
99
|
-
//
|
|
100
|
-
// 1. **The `useMediaQuery` call the sketch used is gone (D12).** No JS
|
|
101
|
-
// viewport read of any kind happens in this file — every breakpoint
|
|
102
|
-
// switch below is a plain Tailwind `md:` variant, so both variants
|
|
103
|
-
// render identically on the server and on the client's first paint.
|
|
104
|
-
//
|
|
105
|
-
// 2. **A single `vaul` `Drawer.Root` cannot itself be "bottom on phone,
|
|
106
|
-
// right on desktop" — its `direction` prop is not a CSS concern.** It
|
|
107
|
-
// drives real per-instance state: which axis (`translate3d(0,y,0)` vs
|
|
108
|
-
// `(x,0,0)`) the built-in open/close keyframes and the live drag-follow
|
|
109
|
-
// math use (`isVertical(direction)`, `getTranslate(el, direction)`,
|
|
110
|
-
// throughout `dist/index.js`). The sketch never set `direction` at all
|
|
111
|
-
// (it left every caller to supply it on a separately-rendered `<Drawer>`
|
|
112
|
-
// Root, which — combined with `direction`'s own default of `"bottom"` —
|
|
113
|
-
// means the sketch's "more details" `Content` would in practice always
|
|
114
|
-
// have inherited `"bottom"` regardless of viewport anyway). Rather than
|
|
115
|
-
// read the viewport to pick a direction (banned, and also the same
|
|
116
|
-
// hydration-mismatch shape D12 already fixed once — a deep-linked
|
|
117
|
-
// `?panel=<id>` drawer can be `open` on the very first server render,
|
|
118
|
-
// per D14, so the first paint has to be right, not just eventually
|
|
119
|
-
// correct after an effect), both drawers below **fix `direction` to
|
|
120
|
-
// `"bottom"` unconditionally** and let plain `md:` classes handle
|
|
121
|
-
// *position* — matching this file's own established, precedented
|
|
122
|
-
// pattern for a responsive split that can't be one reactive value
|
|
123
|
-
// (`side-nav.tsx`'s `GroupSection`: "two parallel trees... not one tree
|
|
124
|
-
// whose content changes based on a JS-read viewport width").
|
|
125
|
-
//
|
|
126
|
-
// The one real, accepted cost of fixing `direction`: `vaul`'s own
|
|
127
|
-
// open/close animation (`slideFromBottom`/`slideToBottom`, a vertical
|
|
128
|
-
// `translate3d`) plays at every breakpoint, including the `md:` right-
|
|
129
|
-
// panel position — so the panel's *entrance motion* is vertical even
|
|
130
|
-
// though its *resting position* is a right-hand edge. This was
|
|
131
|
-
// deliberately not "fixed" by layering a second, horizontal CSS
|
|
132
|
-
// `transition` on top keyed off `data-[state=]`: `vaul`'s animation is
|
|
133
|
-
// a real `@keyframes` `animation`, which (unlike a plain `transition`)
|
|
134
|
-
// correctly plays on a freshly-inserted node — Radix's `Content` isn't
|
|
135
|
-
// in the DOM at all until `open`, so a competing `transition`-based
|
|
136
|
-
// replacement would very likely not animate on *open* at all (no prior
|
|
137
|
-
// DOM node to transition *from*) while still fighting the real
|
|
138
|
-
// animation for the `transform` property. Keeping `vaul`'s own,
|
|
139
|
-
// already-correct mechanism and accepting a vertical entrance for the
|
|
140
|
-
// desktop panel is the smaller, more honest compromise, and it costs no
|
|
141
|
-
// extra CSS (constraint 7). If this reads as wrong once seen live,
|
|
142
|
-
// revisit by disabling it explicitly (`data-vaul-animate="false"` is
|
|
143
|
-
// force-overridable via a prop — `Content`'s `...rest` spreads *after*
|
|
144
|
-
// vaul's own default in `dist/index.js`) and shipping a real
|
|
145
|
-
// `@keyframes`-based replacement, not a `transition`.
|
|
146
|
-
//
|
|
147
|
-
// Fixing `direction` also fixes the *drag* gesture to the vertical
|
|
148
|
-
// axis everywhere. Left alone, that would make a click-drag inside the
|
|
149
|
-
// desktop panel (e.g. selecting text) register as a vertical swipe
|
|
150
|
-
// attempt. Closed with `handleOnly` on the `Root` (drag can only start
|
|
151
|
-
// from a `Drawer.Handle`) plus rendering that handle only below `md:` —
|
|
152
|
-
// so desktop has no drag surface at all (pointer/click/Escape/overlay
|
|
153
|
-
// close it instead), and phone keeps a real, correctly-axised drag
|
|
154
|
-
// handle, which is also what §3's own mobile paragraph asks for ("...
|
|
155
|
-
// with a drag handle").
|
|
156
|
-
//
|
|
157
|
-
// Both variants are otherwise identical in mechanism and differ only in
|
|
158
|
-
// the two things §3/D14 make load-bearing: `dimmed` (renders an
|
|
159
|
-
// `Overlay` and sets `vaul`'s own `modal` prop — verified live in
|
|
160
|
-
// `dist/index.js` that `modal` is what actually gates both the
|
|
161
|
-
// background `pointer-events`/scroll-lock behaviour *and*, independently,
|
|
162
|
-
// whether an `Overlay` should render at all; Radix's own focus trap and
|
|
163
|
-
// `aria-modal="true"` are unconditional either way — `vaul` never forwards
|
|
164
|
-
// its `modal` prop down to `@radix-ui/react-dialog`'s `Root`, which keeps
|
|
165
|
-
// its own default of `true` regardless. So "quick details" is visually and
|
|
166
|
-
// interactively lighter — no dim, background stays scrollable/clickable —
|
|
167
|
-
// but is exactly as keyboard-trapped and exactly as announced to a screen
|
|
168
|
-
// reader as "more details" is; that's a real, verified limitation of
|
|
169
|
-
// `vaul@1.1.2`, not a gap left open on purpose) and `contentClassName`
|
|
170
|
-
// (width/inset/radius/border, per D14's own two size ranges).
|
|
171
|
-
interface DetailDrawerProps {
|
|
172
|
-
open: boolean;
|
|
173
|
-
onOpenChange: (open: boolean) => void;
|
|
174
|
-
/** Rendered as the drawer's accessible name (`Drawer.Title`) — required,
|
|
175
|
-
* not optional, because both variants always render one and Radix warns
|
|
176
|
-
* loudly in dev without it. */
|
|
177
|
-
title: ReactNode;
|
|
178
|
-
/** Rendered as the drawer's accessible description (`Drawer.Description`).
|
|
179
|
-
* Optional for the *caller* — when omitted, a screen-reader-only
|
|
180
|
-
* fallback is still rendered so `aria-describedby` is always wired,
|
|
181
|
-
* matching `primitives/dialog.tsx`'s own precedent of never shipping a
|
|
182
|
-
* `Content` with no description at all. */
|
|
183
|
-
description?: ReactNode;
|
|
184
|
-
/** The scrollable body — a summary of fields, an edit form, whatever the
|
|
185
|
-
* calling screen owns. This file has no opinion on it. */
|
|
186
|
-
children: ReactNode;
|
|
187
|
-
/** Optional sticky action row (§1.5's Polar reference: "a fixed bottom
|
|
188
|
-
* action row... stays visible without the form needing to scroll"). */
|
|
189
|
-
footer?: ReactNode;
|
|
190
|
-
className?: string;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function DetailDrawerContent({
|
|
194
|
-
open,
|
|
195
|
-
onOpenChange,
|
|
196
|
-
title,
|
|
197
|
-
description,
|
|
198
|
-
children,
|
|
199
|
-
footer,
|
|
200
|
-
className,
|
|
201
|
-
dimmed,
|
|
202
|
-
contentClassName,
|
|
203
|
-
}: DetailDrawerProps & { dimmed: boolean; contentClassName: string }) {
|
|
204
|
-
return (
|
|
205
|
-
<DrawerPrimitive.Root
|
|
206
|
-
open={open}
|
|
207
|
-
onOpenChange={onOpenChange}
|
|
208
|
-
direction="bottom"
|
|
209
|
-
modal={dimmed}
|
|
210
|
-
dismissible
|
|
211
|
-
handleOnly
|
|
212
|
-
// vaul defaults this to `false` (`onOpenAutoFocus` is pre-vented
|
|
213
|
-
// unless the caller opts in) — verified in `dist/index.js`'s `Root`
|
|
214
|
-
// default parameters. Without it, opening either drawer would leave
|
|
215
|
-
// keyboard focus sitting on whatever triggered it instead of moving
|
|
216
|
-
// into the panel, which is a real, checkable regression from plain
|
|
217
|
-
// Radix `Dialog` behaviour (its own default *does* auto-focus
|
|
218
|
-
// `Content`) — set explicitly so focus genuinely lands inside the
|
|
219
|
-
// drawer the moment it opens, not just gets trapped there once the
|
|
220
|
-
// user starts tabbing.
|
|
221
|
-
autoFocus
|
|
222
|
-
>
|
|
223
|
-
<DrawerPrimitive.Portal>
|
|
224
|
-
{dimmed && <DrawerPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50" />}
|
|
225
|
-
<DrawerPrimitive.Content
|
|
226
|
-
className={cn(
|
|
227
|
-
"fixed z-50 flex flex-col bg-surface-2 shadow-[var(--shadow-dialog)] outline-none",
|
|
228
|
-
// Phone: bottom sheet.
|
|
229
|
-
"inset-x-0 bottom-0 rounded-t-box border-edge border-t",
|
|
230
|
-
// `md`+: right-hand panel — every sheet-only property above is
|
|
231
|
-
// reset explicitly (inset, rounding, border side, max-height),
|
|
232
|
-
// not just overridden by a wider max-width, per this file's
|
|
233
|
-
// own D12-correction note above.
|
|
234
|
-
"md:inset-x-auto md:inset-y-0 md:right-0 md:h-full md:max-h-none",
|
|
235
|
-
"md:w-full md:rounded-t-none md:rounded-l-box md:border-t-0 md:border-l",
|
|
236
|
-
contentClassName,
|
|
237
|
-
className,
|
|
238
|
-
)}
|
|
239
|
-
>
|
|
240
|
-
{/* Drag surface (D14's mobile "with a drag handle"), phone only —
|
|
241
|
-
`handleOnly` on Root means dragging is only possible starting
|
|
242
|
-
from this element, and it's hidden at `md:`, so desktop has
|
|
243
|
-
no drag surface at all (see this file's own header comment
|
|
244
|
-
for why: `direction` is fixed to "bottom", so a desktop drag
|
|
245
|
-
would otherwise compute against the wrong axis).
|
|
246
|
-
**`!` (Tailwind v4 important) is load-bearing here, not
|
|
247
|
-
decorative — found live, not assumed:** `vaul` injects
|
|
248
|
-
`[data-vaul-handle]{display:block; ...}` into a `<style>` tag
|
|
249
|
-
it appends to `<head>` itself (`dist/index.js`'s own
|
|
250
|
-
`__insertCSS`), at the same specificity as a plain `md:hidden`
|
|
251
|
-
utility and *after* Tailwind's build-time stylesheet in
|
|
252
|
-
cascade order — so a bare `md:hidden` lost the tie and the
|
|
253
|
-
handle stayed visible at desktop width, confirmed by
|
|
254
|
-
inspecting `getComputedStyle(...).display` in a real browser
|
|
255
|
-
at 1280px before this fix, and confirmed gone after it. */}
|
|
256
|
-
<DrawerPrimitive.Handle className="mt-2 shrink-0 bg-edge-strong md:hidden!" />
|
|
257
|
-
|
|
258
|
-
<div className="flex items-start justify-between gap-3 border-edge border-b px-5 py-4">
|
|
259
|
-
<div className="min-w-0">
|
|
260
|
-
<DrawerTitle className="truncate">{title}</DrawerTitle>
|
|
261
|
-
<DrawerDescription className={description == null ? "sr-only" : "mt-1"}>
|
|
262
|
-
{description ?? "Details panel."}
|
|
263
|
-
</DrawerDescription>
|
|
264
|
-
</div>
|
|
265
|
-
<DrawerPrimitive.Close
|
|
266
|
-
aria-label="Close"
|
|
267
|
-
className="shrink-0 text-subtle-foreground hover:text-foreground"
|
|
268
|
-
>
|
|
269
|
-
<X size={16} strokeWidth={1.5} />
|
|
270
|
-
</DrawerPrimitive.Close>
|
|
271
|
-
</div>
|
|
272
|
-
|
|
273
|
-
<div className="flex-1 overflow-y-auto px-5 py-4">{children}</div>
|
|
274
|
-
|
|
275
|
-
{footer != null && (
|
|
276
|
-
<div className="flex shrink-0 items-center justify-end gap-2 border-edge border-t px-5 py-4">
|
|
277
|
-
{footer}
|
|
278
|
-
</div>
|
|
279
|
-
)}
|
|
280
|
-
</DrawerPrimitive.Content>
|
|
281
|
-
</DrawerPrimitive.Portal>
|
|
282
|
-
</DrawerPrimitive.Root>
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* **Quick details** (§3, D14) — a peek at one row's state without leaving
|
|
288
|
-
* the list. Narrow (`420–480px` at `md`+), undimmed (background stays
|
|
289
|
-
* legible and, per `vaul`'s own `modal={false}` behaviour verified above,
|
|
290
|
-
* scrollable/clickable — matching the Mercury reference, §1.4), no route
|
|
291
|
-
* ownership: the caller owns `open`/`onOpenChange` from local state (e.g.
|
|
292
|
-
* "which row id is selected"), and losing it on refresh is expected and
|
|
293
|
-
* fine (reopening is one click on the same row).
|
|
294
|
-
*
|
|
295
|
-
* Not a `Dialog`, and not for anything destructive — §1.7/§3 reserve that
|
|
296
|
-
* for the centered `Dialog` primitive. This is a read-mostly summary with
|
|
297
|
-
* 1–2 actions, e.g. a "View full details" link that upgrades to
|
|
298
|
-
* `MoreDetailDrawer`.
|
|
299
|
-
*/
|
|
300
|
-
export function QuickDetailDrawer(props: DetailDrawerProps) {
|
|
301
|
-
return (
|
|
302
|
-
<DetailDrawerContent
|
|
303
|
-
{...props}
|
|
304
|
-
dimmed={false}
|
|
305
|
-
contentClassName="max-h-[70vh] md:max-w-[440px]"
|
|
306
|
-
/>
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* **More details** (§3, D14) — the full record: every field, an edit
|
|
312
|
-
* form, destructive actions, short nested history. Wide (`640–720px` at
|
|
313
|
-
* `md`+), dimmed (modal-weight, matching the Polar reference, §1.5). The
|
|
314
|
-
* *caller* is expected to own a shallow `?panel=<recordId>` route so this
|
|
315
|
-
* survives refresh and is linkable (D14) — this component only owns the
|
|
316
|
-
* visual/behavioural weight, never routing; pass `open` derived from that
|
|
317
|
-
* query param and `onOpenChange` wired to update it.
|
|
318
|
-
*
|
|
319
|
-
* A destructive-confirmation step opened *from inside* this drawer (e.g.
|
|
320
|
-
* webhook-secret rotation) stays a nested `Dialog` (§3's own footnote) —
|
|
321
|
-
* that dialog needs a z-index at or above this drawer's `z-50`; setting
|
|
322
|
-
* that is `primitives/dialog.tsx`'s job (§8 risk list), not this file's.
|
|
323
|
-
*/
|
|
324
|
-
export function MoreDetailDrawer(props: DetailDrawerProps) {
|
|
325
|
-
return <DetailDrawerContent {...props} dimmed contentClassName="max-h-[92vh] md:max-w-[680px]" />;
|
|
326
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Menu,
|
|
5
|
-
MenuButton,
|
|
6
|
-
MenuItem,
|
|
7
|
-
MenuItems,
|
|
8
|
-
MenuSection,
|
|
9
|
-
MenuSeparator,
|
|
10
|
-
} from "@headlessui/react";
|
|
11
|
-
import { Check } from "lucide-react";
|
|
12
|
-
import type { ComponentPropsWithoutRef } from "react";
|
|
13
|
-
import { cn } from "../../lib/cn";
|
|
14
|
-
import { omitUndefined } from "../../lib/omit-undefined";
|
|
15
|
-
|
|
16
|
-
// D3: Radix `DropdownMenu` → Headless UI `Menu`/`MenuButton`/`MenuItems`/`MenuItem`.
|
|
17
|
-
export const DropdownMenu = Menu;
|
|
18
|
-
|
|
19
|
-
// `MenuButton` is natively polymorphic (`as`), replacing Radix's `asChild`
|
|
20
|
-
// pattern (D4's same call, applied to a trigger rather than `Button`
|
|
21
|
-
// itself): `<DropdownMenuTrigger as={Button} variant="secondary">…`.
|
|
22
|
-
export const DropdownMenuTrigger = MenuButton;
|
|
23
|
-
|
|
24
|
-
// `DropdownMenuGroup`/`DropdownMenuCheckboxItem` have zero call sites in
|
|
25
|
-
// this codebase today (grepped across `admin/` and `packages/ui`) — kept
|
|
26
|
-
// for API parity rather than dropped silently, mapped onto Headless UI's
|
|
27
|
-
// closest equivalents.
|
|
28
|
-
export const DropdownMenuGroup = MenuSection;
|
|
29
|
-
|
|
30
|
-
export function DropdownMenuSeparator({
|
|
31
|
-
className,
|
|
32
|
-
...props
|
|
33
|
-
}: ComponentPropsWithoutRef<typeof MenuSeparator>) {
|
|
34
|
-
return <MenuSeparator className={cn("my-1 h-px bg-edge", className)} {...props} />;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function DropdownMenuContent({
|
|
38
|
-
className,
|
|
39
|
-
...props
|
|
40
|
-
}: ComponentPropsWithoutRef<typeof MenuItems>) {
|
|
41
|
-
return (
|
|
42
|
-
<MenuItems
|
|
43
|
-
anchor="bottom start"
|
|
44
|
-
transition
|
|
45
|
-
className={cn(
|
|
46
|
-
"z-50 min-w-[180px] rounded-md border border-edge bg-surface-2 p-1 shadow-[var(--shadow-popover)] [--anchor-gap:4px] focus:outline-none",
|
|
47
|
-
"origin-top transition duration-100 ease-out data-closed:scale-95 data-closed:opacity-0",
|
|
48
|
-
className,
|
|
49
|
-
)}
|
|
50
|
-
{...props}
|
|
51
|
-
/>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function DropdownMenuItem({
|
|
56
|
-
className,
|
|
57
|
-
...props
|
|
58
|
-
}: Omit<ComponentPropsWithoutRef<"button">, "type">) {
|
|
59
|
-
return (
|
|
60
|
-
<MenuItem
|
|
61
|
-
as="button"
|
|
62
|
-
type="button"
|
|
63
|
-
className={cn(
|
|
64
|
-
"flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left text-body text-foreground outline-none",
|
|
65
|
-
"data-focus:bg-surface-3",
|
|
66
|
-
"data-disabled:pointer-events-none data-disabled:opacity-50",
|
|
67
|
-
className,
|
|
68
|
-
)}
|
|
69
|
-
{...omitUndefined(props)}
|
|
70
|
-
/>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Unconsumed today (see module doc above) — caller controls `checked`
|
|
75
|
-
* itself, since Headless UI's `Menu` has no built-in checkbox-item state
|
|
76
|
-
* the way Radix's `DropdownMenuCheckboxItem` did. */
|
|
77
|
-
export function DropdownMenuCheckboxItem({
|
|
78
|
-
className,
|
|
79
|
-
checked = false,
|
|
80
|
-
children,
|
|
81
|
-
...props
|
|
82
|
-
}: Omit<ComponentPropsWithoutRef<"button">, "type"> & { checked?: boolean }) {
|
|
83
|
-
return (
|
|
84
|
-
<MenuItem
|
|
85
|
-
as="button"
|
|
86
|
-
type="button"
|
|
87
|
-
role="menuitemcheckbox"
|
|
88
|
-
aria-checked={checked}
|
|
89
|
-
className={cn(
|
|
90
|
-
"flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left text-body text-foreground outline-none",
|
|
91
|
-
"data-focus:bg-surface-3",
|
|
92
|
-
className,
|
|
93
|
-
)}
|
|
94
|
-
{...omitUndefined(props)}
|
|
95
|
-
>
|
|
96
|
-
<span className="flex h-3.5 w-3.5 items-center justify-center">
|
|
97
|
-
{checked && <Check size={14} strokeWidth={1.5} aria-hidden="true" />}
|
|
98
|
-
</span>
|
|
99
|
-
{children}
|
|
100
|
-
</MenuItem>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* **Found live, not assumed**: Headless UI's own `MenuHeading` throws the
|
|
106
|
-
* identical "not inside a relevant parent" error `label.tsx`'s own module
|
|
107
|
-
* doc already found for the standalone `Label` — `MenuHeading` needs a
|
|
108
|
-
* `MenuSection` ancestor to supply that context (`MenuSection`'s own
|
|
109
|
-
* implementation calls `useLabels()` to *create* the provider; `MenuHeading`
|
|
110
|
-
* calls `useLabelContext()` to *consume* it), and every call site here
|
|
111
|
-
* (matching the original Radix `DropdownMenuLabel`'s own shape) renders a
|
|
112
|
-
* standalone label with no surrounding group. Radix's own `Label` was a
|
|
113
|
-
* plain, non-interactive `<div>` with no roving-focus/ARIA-grouping
|
|
114
|
-
* behaviour beyond being read as ordinary text inside the menu's
|
|
115
|
-
* accessible tree — so a plain `<div>` here loses nothing real and avoids
|
|
116
|
-
* a dependency this element doesn't actually need. Reproduced live: a real
|
|
117
|
-
* "Uncaught Error: You used a <Label /> component, but it is not inside a
|
|
118
|
-
* relevant parent" client-side exception, thrown the instant the gallery's
|
|
119
|
-
* demo menu opened, before this fix.
|
|
120
|
-
*/
|
|
121
|
-
export function DropdownMenuLabel({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
122
|
-
return (
|
|
123
|
-
<div
|
|
124
|
-
className={cn("px-2 py-1.5 text-micro text-muted-foreground tracking-[0.03em]", className)}
|
|
125
|
-
{...props}
|
|
126
|
-
/>
|
|
127
|
-
);
|
|
128
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import { cn } from "../../lib/cn";
|
|
3
|
-
import { Label } from "./label";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* A labelled form control with an optional validation error beneath it.
|
|
7
|
-
*
|
|
8
|
-
* This replaces the console's two most-duplicated class strings after the
|
|
9
|
-
* table-column ones:
|
|
10
|
-
*
|
|
11
|
-
* - `"flex flex-col gap-1.5"` — **80 occurrences**, every one of them
|
|
12
|
-
* wrapping exactly `<Label htmlFor>` + one control (+ sometimes an
|
|
13
|
-
* error paragraph).
|
|
14
|
-
* - `"text-caption text-state-danger-fg"` — **35 occurrences**, almost all
|
|
15
|
-
* of them that error paragraph.
|
|
16
|
-
*
|
|
17
|
-
* The shape was identical at every site, checked before this component was
|
|
18
|
-
* designed rather than assumed:
|
|
19
|
-
*
|
|
20
|
-
* ```tsx
|
|
21
|
-
* <div className="flex flex-col gap-1.5">
|
|
22
|
-
* <Label htmlFor="record-msisdn">MSISDN</Label>
|
|
23
|
-
* <Input id="record-msisdn" {...form.register("msisdn")} />
|
|
24
|
-
* <p className="text-caption text-state-danger-fg">{errors.msisdn.message}</p>
|
|
25
|
-
* </div>
|
|
26
|
-
* ```
|
|
27
|
-
*
|
|
28
|
-
* becomes
|
|
29
|
-
*
|
|
30
|
-
* ```tsx
|
|
31
|
-
* <FormField label="MSISDN" htmlFor="record-msisdn" error={errors.msisdn?.message}>
|
|
32
|
-
* <Input id="record-msisdn" {...form.register("msisdn")} />
|
|
33
|
-
* </FormField>
|
|
34
|
-
* ```
|
|
35
|
-
*
|
|
36
|
-
* **Why a semantic component and not a generic `<Stack gap="1.5">`.** A
|
|
37
|
-
* layout primitive parameterised by its own CSS is a `<div>` with extra
|
|
38
|
-
* steps — it moves the class from the call site into a prop at the call
|
|
39
|
-
* site, which is not factorisation. `FormField` instead names the *thing*
|
|
40
|
-
* (a labelled control with an error slot), so the spacing, the label
|
|
41
|
-
* treatment and the error treatment are one decision in one place, and a
|
|
42
|
-
* caller cannot get the error styling wrong by writing the `<p>` itself.
|
|
43
|
-
*
|
|
44
|
-
* `htmlFor` is required rather than optional. Several of the 80 sites had
|
|
45
|
-
* a `<Label>` with no `htmlFor` at all, which silently breaks
|
|
46
|
-
* click-to-focus and screen-reader association; making it required turns
|
|
47
|
-
* that into a compile error instead of an accessibility bug nobody
|
|
48
|
-
* notices.
|
|
49
|
-
*/
|
|
50
|
-
export interface FormFieldProps {
|
|
51
|
-
label: ReactNode;
|
|
52
|
-
/** The `id` of the control this labels. Required — see the module doc. */
|
|
53
|
-
htmlFor: string;
|
|
54
|
-
/** Validation message. `undefined` renders no error element at all. */
|
|
55
|
-
error?: string | undefined;
|
|
56
|
-
/** Help text rendered between the label and the control. */
|
|
57
|
-
hint?: ReactNode;
|
|
58
|
-
/**
|
|
59
|
-
* `"field"` (default) labels a single form control by `htmlFor`.
|
|
60
|
-
*
|
|
61
|
-
* `"group"` is for a `RadioGroup`/`ChipSelect`, where the control is a
|
|
62
|
-
* `role="radiogroup"`/`role="group"` wrapper rather than a labelable
|
|
63
|
-
* element. HTML's `for` only associates with labelable form controls, so
|
|
64
|
-
* pointing it at a `<div>` is invalid *and* dangling — which is exactly
|
|
65
|
-
* what the enum migration shipped: four `<Label for="…">` with no
|
|
66
|
-
* element carrying that id anywhere, found in review.
|
|
67
|
-
*
|
|
68
|
-
* In group mode no `for` is emitted; the label carries
|
|
69
|
-
* [`groupLabelId(htmlFor)`] instead, and the caller wires the group with
|
|
70
|
-
* `aria-labelledby={groupLabelId(htmlFor)}`. Both sides derive the id
|
|
71
|
-
* from the same function so they cannot drift.
|
|
72
|
-
*/
|
|
73
|
-
control?: "field" | "group";
|
|
74
|
-
children: ReactNode;
|
|
75
|
-
className?: string | undefined;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** The id a `control="group"` FormField puts on its label, and the value a
|
|
79
|
-
* grouped control must pass as `aria-labelledby`. One function so the two
|
|
80
|
-
* sides cannot disagree. */
|
|
81
|
-
export function groupLabelId(htmlFor: string): string {
|
|
82
|
-
return `${htmlFor}-label`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function FormField({
|
|
86
|
-
label,
|
|
87
|
-
htmlFor,
|
|
88
|
-
error,
|
|
89
|
-
hint,
|
|
90
|
-
children,
|
|
91
|
-
className,
|
|
92
|
-
control = "field",
|
|
93
|
-
}: FormFieldProps) {
|
|
94
|
-
return (
|
|
95
|
-
<div className={cn("flex flex-col gap-1.5", className)}>
|
|
96
|
-
{control === "group" ? (
|
|
97
|
-
// Not a <Label>: with no `for` it would associate with nothing, and
|
|
98
|
-
// a label wrapping no control is worse than a plain element with an
|
|
99
|
-
// id the group points back at.
|
|
100
|
-
<span id={groupLabelId(htmlFor)} className="font-medium text-body text-foreground">
|
|
101
|
-
{label}
|
|
102
|
-
</span>
|
|
103
|
-
) : (
|
|
104
|
-
<Label htmlFor={htmlFor}>{label}</Label>
|
|
105
|
-
)}
|
|
106
|
-
{hint !== undefined && <p className="text-caption text-muted-foreground">{hint}</p>}
|
|
107
|
-
{children}
|
|
108
|
-
{error !== undefined && <FieldError>{error}</FieldError>}
|
|
109
|
-
</div>
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* A validation message.
|
|
115
|
-
*
|
|
116
|
-
* Exported separately because a handful of the 35 sites are not inside a
|
|
117
|
-
* `FormField` — a form-level error above the submit button, or an error
|
|
118
|
-
* attached to a control group rather than one input. Those should still
|
|
119
|
-
* use the same treatment rather than re-inlining the class.
|
|
120
|
-
*
|
|
121
|
-
* `role="alert"` so the message is announced when it appears, which none
|
|
122
|
-
* of the 35 hand-rolled `<p>` elements did.
|
|
123
|
-
*/
|
|
124
|
-
export function FieldError({
|
|
125
|
-
children,
|
|
126
|
-
className,
|
|
127
|
-
}: {
|
|
128
|
-
children: ReactNode;
|
|
129
|
-
className?: string | undefined;
|
|
130
|
-
}) {
|
|
131
|
-
return (
|
|
132
|
-
<p role="alert" className={cn("text-caption text-state-danger-fg", className)}>
|
|
133
|
-
{children}
|
|
134
|
-
</p>
|
|
135
|
-
);
|
|
136
|
-
}
|