@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,254 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from "@headlessui/react";
|
|
4
|
-
import { Check, ChevronDown } from "lucide-react";
|
|
5
|
-
import {
|
|
6
|
-
Children,
|
|
7
|
-
createContext,
|
|
8
|
-
isValidElement,
|
|
9
|
-
type ReactElement,
|
|
10
|
-
type ReactNode,
|
|
11
|
-
useContext,
|
|
12
|
-
useMemo,
|
|
13
|
-
useState,
|
|
14
|
-
} from "react";
|
|
15
|
-
import { cn } from "../../lib/cn";
|
|
16
|
-
import { omitUndefined } from "../../lib/omit-undefined";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* D17: Radix `Select` → Headless UI `Listbox`. Flagged in the design doc as
|
|
20
|
-
* "the largest single API-shape change in the whole primitives migration" —
|
|
21
|
-
* but Radix `Select`'s own public shape (`Select value/onValueChange` →
|
|
22
|
-
* `SelectTrigger` → `SelectValue` → `SelectContent` → `SelectItem`) is kept
|
|
23
|
-
* byte-identical here, so none of this console's eleven call sites need any
|
|
24
|
-
* change. That's possible because Radix `SelectValue` and Headless UI's
|
|
25
|
-
* `Listbox` solve the "what does the trigger display" problem differently:
|
|
26
|
-
* Radix derives it internally from whichever `SelectItem` matches the
|
|
27
|
-
* current value; `Listbox` has no equivalent (`ListboxButton`'s render prop
|
|
28
|
-
* only exposes the raw `value`, not a matching option's own children/label
|
|
29
|
-
* — e.g. `routes-screen.tsx`'s provider select shows `provider.displayName`
|
|
30
|
-
* for a `value={provider.id}`, not the id itself). `findItemLabel` below
|
|
31
|
-
* replicates Radix's behaviour by walking `Select`'s own `children` tree
|
|
32
|
-
* (not the DOM — `ListboxOptions` may not be mounted while closed) to find
|
|
33
|
-
* the `SelectItem` whose `value` matches, and using *its* children as the
|
|
34
|
-
* label. Keyboard behaviour (type-ahead, `Escape`, arrow-key nav) is
|
|
35
|
-
* genuinely Headless UI's own `Listbox`, not reimplemented here — this file
|
|
36
|
-
* only adds the label-lookup Radix's `SelectValue` used to give for free.
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
interface SelectContextValue {
|
|
40
|
-
value: string | undefined;
|
|
41
|
-
itemLabel: (value: string) => ReactNode | undefined;
|
|
42
|
-
}
|
|
43
|
-
const SelectContext = createContext<SelectContextValue | null>(null);
|
|
44
|
-
|
|
45
|
-
function useSelectContext(component: string): SelectContextValue {
|
|
46
|
-
const ctx = useContext(SelectContext);
|
|
47
|
-
if (ctx === null) {
|
|
48
|
-
throw new Error(`<${component} /> must be rendered inside <Select>.`);
|
|
49
|
-
}
|
|
50
|
-
return ctx;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function findItemLabel(node: ReactNode, value: string): ReactNode | undefined {
|
|
54
|
-
let found: ReactNode | undefined;
|
|
55
|
-
Children.forEach(node, (child) => {
|
|
56
|
-
if (found !== undefined || !isValidElement(child)) return;
|
|
57
|
-
const el = child as ReactElement<{ value?: string; children?: ReactNode }>;
|
|
58
|
-
if (el.type === SelectItem) {
|
|
59
|
-
if (el.props.value === value) found = el.props.children;
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (el.props?.children != null) {
|
|
63
|
-
found = findItemLabel(el.props.children, value);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
return found;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Note the `| undefined` on the optional props below. This package
|
|
71
|
-
* compiles under `exactOptionalPropertyTypes`, where a bare `value?:
|
|
72
|
-
* string` means "you may omit this key" and NOT "you may pass
|
|
73
|
-
* `undefined`" — so the ordinary controlled pattern,
|
|
74
|
-
* `const [v, setV] = useState<string>()` followed by `value={v}`, is a
|
|
75
|
-
* type error. Found writing this component's own story, which is the
|
|
76
|
-
* first code outside the original application to use it.
|
|
77
|
-
*/
|
|
78
|
-
export interface SelectProps {
|
|
79
|
-
value?: string | undefined;
|
|
80
|
-
defaultValue?: string | undefined;
|
|
81
|
-
onValueChange?: ((value: string) => void) | undefined;
|
|
82
|
-
disabled?: boolean | undefined;
|
|
83
|
-
children: ReactNode;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function Select({ value, defaultValue, onValueChange, disabled, children }: SelectProps) {
|
|
87
|
-
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
88
|
-
const isControlled = value !== undefined;
|
|
89
|
-
const currentValue = isControlled ? value : internalValue;
|
|
90
|
-
|
|
91
|
-
function handleChange(next: string) {
|
|
92
|
-
if (!isControlled) setInternalValue(next);
|
|
93
|
-
onValueChange?.(next);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const itemLabel = useMemo(() => (v: string) => findItemLabel(children, v), [children]);
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
// `as="div"` + `relative`: with `SelectContent` no longer portaled
|
|
100
|
-
// (see its own comment), the options position themselves against this
|
|
101
|
-
// element. Headless UI's `Listbox` renders a fragment by default, which
|
|
102
|
-
// would leave `absolute` resolving against whatever ancestor happened
|
|
103
|
-
// to be positioned — usually the drawer, putting the dropdown in the
|
|
104
|
-
// wrong place entirely.
|
|
105
|
-
<Listbox
|
|
106
|
-
as="div"
|
|
107
|
-
className="relative"
|
|
108
|
-
value={currentValue ?? ""}
|
|
109
|
-
onChange={handleChange}
|
|
110
|
-
{...omitUndefined({ disabled })}
|
|
111
|
-
>
|
|
112
|
-
<SelectContext.Provider value={{ value: currentValue, itemLabel }}>
|
|
113
|
-
{children}
|
|
114
|
-
</SelectContext.Provider>
|
|
115
|
-
</Listbox>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Unconsumed today (grepped across `admin/`) — kept for API parity. Radix's
|
|
120
|
-
// `SelectGroup` had no visual treatment of its own beyond semantic
|
|
121
|
-
// grouping; Headless UI's `Listbox` has no equivalent, so this stays a
|
|
122
|
-
// plain wrapper rather than reaching for `Menu`'s `MenuSection` (a
|
|
123
|
-
// different component family).
|
|
124
|
-
export function SelectGroup({ children }: { children: ReactNode }) {
|
|
125
|
-
return <fieldset className="contents border-0 p-0 m-0 min-w-0">{children}</fieldset>;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function SelectTrigger({
|
|
129
|
-
id,
|
|
130
|
-
className,
|
|
131
|
-
children,
|
|
132
|
-
}: {
|
|
133
|
-
id?: string;
|
|
134
|
-
className?: string;
|
|
135
|
-
children: ReactNode;
|
|
136
|
-
}) {
|
|
137
|
-
useSelectContext("SelectTrigger");
|
|
138
|
-
return (
|
|
139
|
-
<ListboxButton
|
|
140
|
-
id={id}
|
|
141
|
-
className={cn(
|
|
142
|
-
"select select-bordered flex w-full items-center justify-between font-sans text-prose",
|
|
143
|
-
className,
|
|
144
|
-
)}
|
|
145
|
-
>
|
|
146
|
-
{children}
|
|
147
|
-
<ChevronDown
|
|
148
|
-
size={14}
|
|
149
|
-
strokeWidth={1.5}
|
|
150
|
-
className="text-muted-foreground"
|
|
151
|
-
aria-hidden="true"
|
|
152
|
-
/>
|
|
153
|
-
</ListboxButton>
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function SelectValue({ placeholder }: { placeholder?: string }) {
|
|
158
|
-
const { value, itemLabel } = useSelectContext("SelectValue");
|
|
159
|
-
if (value === undefined || value === "") {
|
|
160
|
-
return <span className="text-subtle-foreground">{placeholder}</span>;
|
|
161
|
-
}
|
|
162
|
-
return <>{itemLabel(value) ?? value}</>;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function SelectContent({
|
|
166
|
-
className,
|
|
167
|
-
children,
|
|
168
|
-
}: {
|
|
169
|
-
className?: string;
|
|
170
|
-
children: ReactNode;
|
|
171
|
-
}) {
|
|
172
|
-
useSelectContext("SelectContent");
|
|
173
|
-
return (
|
|
174
|
-
<ListboxOptions
|
|
175
|
-
// `portal={false}` is a correctness fix, not a preference.
|
|
176
|
-
//
|
|
177
|
-
// Headless UI's `anchor` prop portals the options into
|
|
178
|
-
// `#headlessui-portal-root`, a top-level sibling of `<body>`. Inside a
|
|
179
|
-
// `vaul` drawer that is fatal: vaul's `Content` mounts a Radix
|
|
180
|
-
// `FocusScope` with `trapped: true`, a document-level `focusin`
|
|
181
|
-
// listener that force-refocuses back into the drawer the instant
|
|
182
|
-
// focus lands outside it. The portaled listbox is outside, so its
|
|
183
|
-
// enter transition stalls mid-flight and it never becomes usable.
|
|
184
|
-
//
|
|
185
|
-
// Measured on the live console rather than inferred — opening the
|
|
186
|
-
// registration status select inside its stacked drawer gave:
|
|
187
|
-
//
|
|
188
|
-
// parentChain [..., DIV#headlessui-portal-root, BODY]
|
|
189
|
-
// insideAnyDrawer false
|
|
190
|
-
// opacity "0"
|
|
191
|
-
// pointerEvents "none"
|
|
192
|
-
// rect [122, 10] (collapsed, not four options tall)
|
|
193
|
-
//
|
|
194
|
-
// — byte-for-byte the signature #274 recorded for a nested Dialog.
|
|
195
|
-
// #282 fixed that class for `Dialog` and never covered `Select`, so
|
|
196
|
-
// every select inside a drawer has been silently unusable since.
|
|
197
|
-
//
|
|
198
|
-
// Rendering inline keeps the options inside the drawer's own subtree,
|
|
199
|
-
// so the focus trap contains them instead of fighting them. The cost
|
|
200
|
-
// is losing `anchor`'s collision detection; `top-full` + `w-full`
|
|
201
|
-
// below reproduces the same "directly under the trigger, matching its
|
|
202
|
-
// width" placement, which is what every call site here wants anyway.
|
|
203
|
-
portal={false}
|
|
204
|
-
// No `transition`, and no `data-closed:*` classes. Headless UI's
|
|
205
|
-
// transition machinery holds `data-closed` until it observes the
|
|
206
|
-
// enter transition finish; inside a drawer it never does. Measured
|
|
207
|
-
// after the portal fix, 1.4s after opening: `data-closed` and
|
|
208
|
-
// `data-enter` both still present, `opacity: 0`, on an element that
|
|
209
|
-
// was otherwise correct (inside the drawer, `pointer-events: auto`,
|
|
210
|
-
// full 146px height, all four options present).
|
|
211
|
-
//
|
|
212
|
-
// That is the same failure mode twice now — #274's nested Dialog was
|
|
213
|
-
// also a stalled enter transition, not a mispositioned element. A
|
|
214
|
-
// 100ms fade on a select dropdown is not worth a second component
|
|
215
|
-
// that silently does not open, so it is gone rather than debugged.
|
|
216
|
-
className={cn(
|
|
217
|
-
"absolute top-full left-0 z-50 mt-1 max-h-80 w-full min-w-[8rem] overflow-y-auto rounded-md border border-edge bg-surface-2 p-1 shadow-[var(--shadow-popover)] focus:outline-none",
|
|
218
|
-
className,
|
|
219
|
-
)}
|
|
220
|
-
>
|
|
221
|
-
{children}
|
|
222
|
-
</ListboxOptions>
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export function SelectItem({
|
|
227
|
-
value,
|
|
228
|
-
className,
|
|
229
|
-
children,
|
|
230
|
-
}: {
|
|
231
|
-
value: string;
|
|
232
|
-
className?: string;
|
|
233
|
-
children: ReactNode;
|
|
234
|
-
}) {
|
|
235
|
-
return (
|
|
236
|
-
<ListboxOption
|
|
237
|
-
value={value}
|
|
238
|
-
className={cn(
|
|
239
|
-
"relative flex cursor-pointer items-center rounded-sm py-1.5 pr-2 pl-7 text-body text-foreground outline-none",
|
|
240
|
-
"data-focus:bg-surface-3",
|
|
241
|
-
className,
|
|
242
|
-
)}
|
|
243
|
-
>
|
|
244
|
-
{({ selected }) => (
|
|
245
|
-
<>
|
|
246
|
-
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
247
|
-
{selected && <Check size={14} strokeWidth={1.5} aria-hidden="true" />}
|
|
248
|
-
</span>
|
|
249
|
-
{children}
|
|
250
|
-
</>
|
|
251
|
-
)}
|
|
252
|
-
</ListboxOption>
|
|
253
|
-
);
|
|
254
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
2
|
-
import { cn } from "../../lib/cn";
|
|
3
|
-
|
|
4
|
-
// D6: `@radix-ui/react-separator` is deleted outright — the Radix primitive
|
|
5
|
-
// was already a near-trivial wrapper around a styled `<div>` with a
|
|
6
|
-
// `role`/`aria-orientation` toggle for the decorative/semantic distinction,
|
|
7
|
-
// so nothing behavioural is lost by hand-rolling that same toggle here.
|
|
8
|
-
//
|
|
9
|
-
// The non-decorative branch renders a real `<hr>` rather than a `<div
|
|
10
|
-
// role="separator">` — found via Biome's own a11y lint, not assumed: an
|
|
11
|
-
// ARIA `separator` role on a generic element is the *interactive* (focusable,
|
|
12
|
-
// resizable-pane-divider) variant per WAI-ARIA, which requires `tabIndex`
|
|
13
|
-
// and `aria-valuenow`; neither is meaningful for a plain visual rule. `<hr>`
|
|
14
|
-
// carries the equivalent *static* separator semantics natively, with no
|
|
15
|
-
// required ARIA attributes, and is what Biome's `useSemanticElements` rule
|
|
16
|
-
// itself suggests.
|
|
17
|
-
export interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
18
|
-
orientation?: "horizontal" | "vertical";
|
|
19
|
-
/** Matches Radix's own default: `true` renders a purely visual rule with
|
|
20
|
-
* no `separator` semantics exposed to assistive tech (`role="none"` on a
|
|
21
|
-
* `<div>`); `false` renders a native `<hr>` with `aria-orientation`. */
|
|
22
|
-
decorative?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const Separator = forwardRef<HTMLDivElement, SeparatorProps>(
|
|
26
|
-
({ className, orientation = "horizontal", decorative = true, ...props }, ref) => {
|
|
27
|
-
const sharedClassName = cn(
|
|
28
|
-
"shrink-0 border-0 bg-edge",
|
|
29
|
-
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
|
30
|
-
className,
|
|
31
|
-
);
|
|
32
|
-
if (decorative) {
|
|
33
|
-
return <div ref={ref} role="none" className={sharedClassName} {...props} />;
|
|
34
|
-
}
|
|
35
|
-
// No consumer forwards a ref through the non-decorative branch today
|
|
36
|
-
// (grepped across `admin/`) — the cast is a documented, zero-risk
|
|
37
|
-
// approximation rather than widening `Separator`'s public ref type to
|
|
38
|
-
// `HTMLDivElement | HTMLHRElement` for a case nothing exercises.
|
|
39
|
-
return (
|
|
40
|
-
<hr
|
|
41
|
-
ref={ref as unknown as React.Ref<HTMLHRElement>}
|
|
42
|
-
aria-orientation={orientation}
|
|
43
|
-
className={sharedClassName}
|
|
44
|
-
{...props}
|
|
45
|
-
/>
|
|
46
|
-
);
|
|
47
|
-
},
|
|
48
|
-
);
|
|
49
|
-
Separator.displayName = "Separator";
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/react";
|
|
4
|
-
import { ChevronDown } from "lucide-react";
|
|
5
|
-
import type { ComponentType, ReactNode } from "react";
|
|
6
|
-
import { cn } from "../../lib/cn";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The console's information architecture (console-redesign.md §4), as data.
|
|
10
|
-
* `@vaam-apps/ui` owns the shape; `frontends/apps/admin/app/nav-groups.ts` owns the actual
|
|
11
|
-
* content (routes, groupings, icons) — this file never hardcodes a route.
|
|
12
|
-
*/
|
|
13
|
-
export interface NavItem {
|
|
14
|
-
label: string;
|
|
15
|
-
href: string;
|
|
16
|
-
/** Any lucide-react icon component. Typed structurally so this file
|
|
17
|
-
* doesn't need to depend on `lucide-react`'s own exported icon type name. */
|
|
18
|
-
icon: ComponentType<{ size?: number; className?: string; "aria-hidden"?: boolean | "true" }>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface NavGroup {
|
|
22
|
-
/** Small-caps section header, e.g. "Messaging" (console-redesign.md §4). */
|
|
23
|
-
label: string;
|
|
24
|
-
items: NavItem[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface SideNavProps {
|
|
28
|
-
/** Flat, ungrouped, always first — §4 ("Dashboard"). */
|
|
29
|
-
topItem: NavItem;
|
|
30
|
-
groups: NavGroup[];
|
|
31
|
-
/** De-emphasized footer utility rows (§1.1: "administrivia, not content"). */
|
|
32
|
-
footerItems: NavItem[];
|
|
33
|
-
/** Current pathname, for active-row highlighting. Plain string, not a
|
|
34
|
-
* `next/navigation` call — this package has no dependency on Next.js, so
|
|
35
|
-
* the caller (`frontends/apps/admin/app/console-shell.tsx`) resolves it via
|
|
36
|
-
* `usePathname()` and passes it down. */
|
|
37
|
-
currentPath: string;
|
|
38
|
-
/** App-specific account/sign-out markup (§4's footer "Signed in as
|
|
39
|
-
* <email> · Sign out" row) — rendered as-is, never built here, so this
|
|
40
|
-
* component stays free of any auth-specific knowledge. */
|
|
41
|
-
accountSlot?: ReactNode;
|
|
42
|
-
className?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function isActive(href: string, currentPath: string): boolean {
|
|
46
|
-
if (href === "/") return currentPath === "/";
|
|
47
|
-
return currentPath === href || currentPath.startsWith(`${href}/`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* One nav row.
|
|
52
|
-
*
|
|
53
|
-
* **CSS-driven, not `useMediaQuery`-driven — found live, corrected after
|
|
54
|
-
* an earlier revision of this file used `@uidotdev/usehooks`' hook here
|
|
55
|
-
* and wrapped `SideNav` in `next/dynamic({ ssr: false })` to work around
|
|
56
|
-
* its SSR crash (see `console-shell.tsx`'s own history / this file's own
|
|
57
|
-
* git log for the full finding).** That fix traded a real cost — the
|
|
58
|
-
* sidebar never appeared in the server-rendered HTML, so every full page
|
|
59
|
-
* load painted an empty slab and popped the real nav in after hydration —
|
|
60
|
-
* for a problem the breakpoint switch itself never needed JS to solve:
|
|
61
|
-
* `variant="responsive"` below expresses "full label off-canvas and at
|
|
62
|
-
* `xl`, `sr-only` at the `lg` icon-rail band" purely with `lg:sr-only
|
|
63
|
-
* xl:not-sr-only`, no hook, no client-only state, no hydration boundary.
|
|
64
|
-
*
|
|
65
|
-
* - **`"responsive"`**: the label is visible off-canvas (`<1024px`, where
|
|
66
|
-
* this row is never reached without the group around it already being
|
|
67
|
-
* fully expanded) and at `≥1280px`, `sr-only` in the `1024–1279px` icon
|
|
68
|
-
* rail band — a daisyUI `.tooltip`/`data-tip` (D5) carries the label
|
|
69
|
-
* back on hover/focus there. Used for every row that can appear in the
|
|
70
|
-
* *persistent* sidebar: `topItem`, `footerItems`, and each group's own
|
|
71
|
-
* "always expanded" desktop rows (`GroupSection` below).
|
|
72
|
-
* - **`"always"`**: the label is always visible, no responsive classes at
|
|
73
|
-
* all. Used only inside the off-canvas accordion tree, which is itself
|
|
74
|
-
* gated to `<1024px` by its own `lg:hidden` wrapper — a per-row
|
|
75
|
-
* breakpoint switch would be redundant there, since that whole tree
|
|
76
|
-
* never renders at any width where a switch could matter.
|
|
77
|
-
*/
|
|
78
|
-
function NavLink({
|
|
79
|
-
item,
|
|
80
|
-
active,
|
|
81
|
-
variant,
|
|
82
|
-
dim = false,
|
|
83
|
-
}: {
|
|
84
|
-
item: NavItem;
|
|
85
|
-
active: boolean;
|
|
86
|
-
variant: "responsive" | "always";
|
|
87
|
-
dim?: boolean;
|
|
88
|
-
}) {
|
|
89
|
-
const Icon = item.icon;
|
|
90
|
-
const responsive = variant === "responsive";
|
|
91
|
-
return (
|
|
92
|
-
<a
|
|
93
|
-
href={item.href}
|
|
94
|
-
aria-current={active ? "page" : undefined}
|
|
95
|
-
data-tip={item.label}
|
|
96
|
-
className={cn(
|
|
97
|
-
"flex items-center gap-3 rounded-field px-3 py-2 transition-colors",
|
|
98
|
-
dim ? "text-caption" : "text-body",
|
|
99
|
-
responsive && "tooltip tooltip-right lg:justify-center lg:px-0 xl:justify-start xl:px-3",
|
|
100
|
-
active && !dim && "bg-base-300 font-medium text-foreground",
|
|
101
|
-
active && dim && "text-foreground",
|
|
102
|
-
!active && "text-muted-foreground hover:bg-base-300/60 hover:text-foreground",
|
|
103
|
-
)}
|
|
104
|
-
>
|
|
105
|
-
<Icon size={16} className="shrink-0" aria-hidden="true" />
|
|
106
|
-
<span className={cn(responsive && "lg:sr-only xl:not-sr-only")}>{item.label}</span>
|
|
107
|
-
</a>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* One group's worth of rows, rendered as **two parallel trees**, one per
|
|
113
|
-
* §6.1 regime, each visible only at its own breakpoint via a plain
|
|
114
|
-
* `hidden`/`lg:hidden` toggle — not one tree whose *content* changes based
|
|
115
|
-
* on a JS-read viewport width. The two regimes genuinely need different
|
|
116
|
-
* DOM, not just different visibility, because the off-canvas tree is
|
|
117
|
-
* interactive (a collapsible `Disclosure`) and the persistent tree is not
|
|
118
|
-
* (every group is always fully expanded there, matching §1.1's LottieFiles
|
|
119
|
-
* reference, which shows no user-collapsible groups on desktop at all) —
|
|
120
|
-
* duplicating ~4 rows per group across two trees is a irrelevant DOM cost
|
|
121
|
-
* next to the alternative (a client-only nav that never appears in the
|
|
122
|
-
* first paint).
|
|
123
|
-
*
|
|
124
|
-
* - **Off-canvas** (`lg:hidden`, so effectively `<1024px` — both the phone
|
|
125
|
-
* band and the tablet band §6.1 explicitly keeps behind the hamburger):
|
|
126
|
-
* a Headless UI `Disclosure`, collapsed by default, expanded only if it
|
|
127
|
-
* contains the current route — "roughly 6–8 tappable rows on open, not
|
|
128
|
-
* 18" (§4). `defaultOpen` is computed from `currentPath`, which is
|
|
129
|
-
* available identically on the server and the client, so this renders
|
|
130
|
-
* correctly on first paint with no hydration mismatch.
|
|
131
|
-
* - **Persistent** (`hidden lg:flex`, so `≥1024px`): always fully
|
|
132
|
-
* expanded. The header/rail-divider split (small-caps text at `≥1280px`,
|
|
133
|
-
* a hairline divider instead in the `1024–1279px` icon rail band) is the
|
|
134
|
-
* same CSS-toggle technique `NavLink`'s own `"responsive"` variant uses.
|
|
135
|
-
*/
|
|
136
|
-
function GroupSection({ group, currentPath }: { group: NavGroup; currentPath: string }) {
|
|
137
|
-
const hasActiveItem = group.items.some((item) => isActive(item.href, currentPath));
|
|
138
|
-
|
|
139
|
-
return (
|
|
140
|
-
<>
|
|
141
|
-
<div className="lg:hidden">
|
|
142
|
-
<Disclosure defaultOpen={hasActiveItem}>
|
|
143
|
-
{({ open }) => (
|
|
144
|
-
<div className="flex flex-col gap-0.5">
|
|
145
|
-
<DisclosureButton className="flex items-center justify-between rounded-field px-3 py-1.5 text-left text-caption text-subtle-foreground uppercase tracking-wide hover:text-foreground">
|
|
146
|
-
{group.label}
|
|
147
|
-
<ChevronDown
|
|
148
|
-
size={14}
|
|
149
|
-
aria-hidden="true"
|
|
150
|
-
className={cn("transition-transform", open && "rotate-180")}
|
|
151
|
-
/>
|
|
152
|
-
</DisclosureButton>
|
|
153
|
-
<DisclosurePanel className="flex flex-col gap-0.5">
|
|
154
|
-
{group.items.map((item) => (
|
|
155
|
-
<NavLink
|
|
156
|
-
key={item.href}
|
|
157
|
-
item={item}
|
|
158
|
-
active={isActive(item.href, currentPath)}
|
|
159
|
-
variant="always"
|
|
160
|
-
/>
|
|
161
|
-
))}
|
|
162
|
-
</DisclosurePanel>
|
|
163
|
-
</div>
|
|
164
|
-
)}
|
|
165
|
-
</Disclosure>
|
|
166
|
-
</div>
|
|
167
|
-
|
|
168
|
-
<div className="hidden lg:flex lg:flex-col lg:gap-0.5">
|
|
169
|
-
<p className="hidden px-3 py-1.5 text-caption text-subtle-foreground uppercase tracking-wide xl:block">
|
|
170
|
-
{group.label}
|
|
171
|
-
</p>
|
|
172
|
-
<div className="mx-3 my-1 border-edge-subtle border-t xl:hidden" aria-hidden="true" />
|
|
173
|
-
{group.items.map((item) => (
|
|
174
|
-
<NavLink
|
|
175
|
-
key={item.href}
|
|
176
|
-
item={item}
|
|
177
|
-
active={isActive(item.href, currentPath)}
|
|
178
|
-
variant="responsive"
|
|
179
|
-
/>
|
|
180
|
-
))}
|
|
181
|
-
</div>
|
|
182
|
-
</>
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* The console's side navigation (console-redesign.md §4, §6.2). A pure
|
|
188
|
-
* structural/layout component — routing, session data, and the drawer's
|
|
189
|
-
* own off-canvas/persistent CSS split all live at the call site
|
|
190
|
-
* (`frontends/apps/admin/app/console-shell.tsx`, D7): this component only ever decides
|
|
191
|
-
* "flat top item → grouped middle → de-emphasized footer" composition and
|
|
192
|
-
* the label/icon-only/accordion treatment per breakpoint.
|
|
193
|
-
*
|
|
194
|
-
* Breakpoint behaviour (§6.1 is the authoritative table here — see that
|
|
195
|
-
* section's own reasoning for why it supersedes §4's looser "Tablet
|
|
196
|
-
* (768–1024px)" prose, which pre-dates that table and was never updated to
|
|
197
|
-
* match it) — entirely CSS-driven (`lg:`/`xl:` Tailwind variants), never a
|
|
198
|
-
* JS breakpoint read:
|
|
199
|
-
* - `<1024px` (phone and tablet alike): off-canvas, full labels,
|
|
200
|
-
* collapsible accordion groups.
|
|
201
|
-
* - `1024–1279px`: persistent icon-only rail, tooltip on hover/focus.
|
|
202
|
-
* - `≥1280px`: persistent full sidebar with labels.
|
|
203
|
-
*
|
|
204
|
-
* This means the whole nav — every row, every group, every breakpoint's
|
|
205
|
-
* shape — is present in the server-rendered HTML on first paint. No
|
|
206
|
-
* `next/dynamic({ ssr: false })`, no client-only mount flash. Verify that
|
|
207
|
-
* claim directly, not by trusting this comment: `curl` a page and grep the
|
|
208
|
-
* raw HTML for a nav item's label.
|
|
209
|
-
*/
|
|
210
|
-
export function SideNav({
|
|
211
|
-
topItem,
|
|
212
|
-
groups,
|
|
213
|
-
footerItems,
|
|
214
|
-
currentPath,
|
|
215
|
-
accountSlot,
|
|
216
|
-
className,
|
|
217
|
-
}: SideNavProps) {
|
|
218
|
-
return (
|
|
219
|
-
<nav
|
|
220
|
-
aria-label="Primary"
|
|
221
|
-
className={cn("flex h-full flex-col gap-4 overflow-y-auto bg-base-200 py-4", className)}
|
|
222
|
-
>
|
|
223
|
-
<div className="flex flex-col gap-0.5 px-2">
|
|
224
|
-
<NavLink item={topItem} active={isActive(topItem.href, currentPath)} variant="responsive" />
|
|
225
|
-
</div>
|
|
226
|
-
|
|
227
|
-
<div className="flex flex-1 flex-col gap-4 px-2">
|
|
228
|
-
{groups.map((group) => (
|
|
229
|
-
<GroupSection key={group.label} group={group} currentPath={currentPath} />
|
|
230
|
-
))}
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
<div className="flex flex-col gap-2 border-edge-subtle border-t px-2 pt-3">
|
|
234
|
-
<div className="flex flex-col gap-0.5">
|
|
235
|
-
{footerItems.map((item) => (
|
|
236
|
-
<NavLink
|
|
237
|
-
key={item.href}
|
|
238
|
-
item={item}
|
|
239
|
-
active={isActive(item.href, currentPath)}
|
|
240
|
-
variant="responsive"
|
|
241
|
-
dim
|
|
242
|
-
/>
|
|
243
|
-
))}
|
|
244
|
-
</div>
|
|
245
|
-
{/* Hidden in the icon rail band only (no room for the account
|
|
246
|
-
block at 64px); shown off-canvas and at the full-label desktop
|
|
247
|
-
width — same `lg:hidden xl:block` toggle technique as the rest
|
|
248
|
-
of this file, not a JS breakpoint check. */}
|
|
249
|
-
{accountSlot != null && <div className="px-3 lg:hidden xl:block">{accountSlot}</div>}
|
|
250
|
-
</div>
|
|
251
|
-
</nav>
|
|
252
|
-
);
|
|
253
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { HTMLAttributes } from "react";
|
|
2
|
-
import { cn } from "../../lib/cn";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Static — never shimmering (design doc §3.8 rule 2: "no skeleton
|
|
6
|
-
* shimmer"; §5.2: skeleton rows must match the real row height exactly so
|
|
7
|
-
* nothing shifts on load). House rule, not negotiable — see this repo's
|
|
8
|
-
* own AGENTS.md-lineage note; do not add animation here.
|
|
9
|
-
*
|
|
10
|
-
* Deliberately hand-rolled `<div>`, not daisyUI's own `.skeleton` class:
|
|
11
|
-
* read `daisyui/components/skeleton.css` directly before deciding this —
|
|
12
|
-
* daisyUI's `.skeleton` ships a `background-position` shimmer under
|
|
13
|
-
* `animation:1.8s ease-in-out infinite skeleton` (guarded only by
|
|
14
|
-
* `prefers-reduced-motion`, which is not the same thing as "off by
|
|
15
|
-
* default"), which is exactly the animation this house rule forbids.
|
|
16
|
-
* Radius stays `rounded-sm` (now `--radius-field`, 12px, via Phase 0's
|
|
17
|
-
* token rewrite) rather than moving to `--radius-box` — this component
|
|
18
|
-
* stands in for table cells and text lines across every screen, all
|
|
19
|
-
* field-scale content, not card-scale surfaces, so the token that already
|
|
20
|
-
* existed here is also the correct D8 tier, not a value left untouched by
|
|
21
|
-
* omission.
|
|
22
|
-
*/
|
|
23
|
-
export function Skeleton({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
24
|
-
return <div className={cn("rounded-sm bg-surface-3", className)} {...props} />;
|
|
25
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { cn } from "../../lib/cn";
|
|
2
|
-
|
|
3
|
-
export interface SpinnerProps {
|
|
4
|
-
size?: "xs" | "sm" | "md" | "lg";
|
|
5
|
-
/** Announced to assistive technology. `undefined` renders the spinner
|
|
6
|
-
* `aria-hidden`, which is correct when a visible sibling already says
|
|
7
|
-
* what is loading — two announcements of the same wait is worse than
|
|
8
|
-
* one. */
|
|
9
|
-
label?: string | undefined;
|
|
10
|
-
className?: string | undefined;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const SIZE_CLASS = {
|
|
14
|
-
xs: "loading-xs",
|
|
15
|
-
sm: "loading-sm",
|
|
16
|
-
md: "loading-md",
|
|
17
|
-
lg: "loading-lg",
|
|
18
|
-
} as const;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* An indeterminate busy indicator, for a wait whose *shape* is unknown.
|
|
22
|
-
*
|
|
23
|
-
* Prefer `Skeleton` wherever the shape of what is arriving is known —
|
|
24
|
-
* a table, a card, a detail pane. A skeleton keeps the layout from
|
|
25
|
-
* jumping when content lands and tells the reader what to expect; a
|
|
26
|
-
* spinner tells them only that something is happening. This is for the
|
|
27
|
-
* rest: a button mid-submit, a poll with no placeholder to draw.
|
|
28
|
-
*/
|
|
29
|
-
export function Spinner({ size = "sm", label, className }: SpinnerProps) {
|
|
30
|
-
return (
|
|
31
|
-
<span
|
|
32
|
-
className={cn("loading loading-spinner", SIZE_CLASS[size], className)}
|
|
33
|
-
role={label === undefined ? undefined : "status"}
|
|
34
|
-
aria-hidden={label === undefined ? true : undefined}
|
|
35
|
-
>
|
|
36
|
-
{label !== undefined && <span className="sr-only">{label}</span>}
|
|
37
|
-
</span>
|
|
38
|
-
);
|
|
39
|
-
}
|