@vaam-apps/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/dist/components/data/code.d.ts +37 -0
- package/dist/components/data/code.d.ts.map +1 -0
- package/dist/components/data/code.js +37 -0
- package/dist/components/data/code.js.map +1 -0
- package/dist/components/data/copy-button.d.ts +39 -0
- package/dist/components/data/copy-button.d.ts.map +1 -0
- package/dist/components/data/copy-button.js +50 -0
- package/dist/components/data/copy-button.js.map +1 -0
- package/dist/components/data/detail-row.d.ts +69 -0
- package/dist/components/data/detail-row.d.ts.map +1 -0
- package/dist/components/data/detail-row.js +27 -0
- package/dist/components/data/detail-row.js.map +1 -0
- package/dist/components/data/id-display.d.ts +33 -0
- package/dist/components/data/id-display.d.ts.map +1 -0
- package/dist/components/data/id-display.js +25 -0
- package/dist/components/data/id-display.js.map +1 -0
- package/dist/components/data/masked-value.d.ts +47 -0
- package/dist/components/data/masked-value.d.ts.map +1 -0
- package/dist/components/data/masked-value.js +46 -0
- package/dist/components/data/masked-value.js.map +1 -0
- package/dist/components/data/money.d.ts +28 -0
- package/dist/components/data/money.d.ts.map +1 -0
- package/dist/components/data/money.js +22 -0
- package/dist/components/data/money.js.map +1 -0
- package/dist/components/data/phone-display.d.ts +45 -0
- package/dist/components/data/phone-display.d.ts.map +1 -0
- package/dist/components/data/phone-display.js +23 -0
- package/dist/components/data/phone-display.js.map +1 -0
- package/dist/components/data/stat-tile.d.ts +33 -0
- package/dist/components/data/stat-tile.d.ts.map +1 -0
- package/dist/components/data/stat-tile.js +18 -0
- package/dist/components/data/stat-tile.js.map +1 -0
- package/dist/components/data/timestamp-display.d.ts +14 -0
- package/dist/components/data/timestamp-display.d.ts.map +1 -0
- package/dist/components/data/timestamp-display.js +77 -0
- package/dist/components/data/timestamp-display.js.map +1 -0
- package/dist/components/patterns/inline-banner.d.ts +22 -0
- package/dist/components/patterns/inline-banner.d.ts.map +1 -0
- package/dist/components/patterns/inline-banner.js +11 -0
- package/dist/components/patterns/inline-banner.js.map +1 -0
- package/dist/components/patterns/inline-empty-state.d.ts +20 -0
- package/dist/components/patterns/inline-empty-state.d.ts.map +1 -0
- package/dist/components/patterns/inline-empty-state.js +11 -0
- package/dist/components/patterns/inline-empty-state.js.map +1 -0
- package/dist/components/patterns/live-row.d.ts +23 -0
- package/dist/components/patterns/live-row.d.ts.map +1 -0
- package/dist/components/patterns/live-row.js +48 -0
- package/dist/components/patterns/live-row.js.map +1 -0
- package/dist/components/patterns/payload-inspector.d.ts +25 -0
- package/dist/components/patterns/payload-inspector.d.ts.map +1 -0
- package/dist/components/patterns/payload-inspector.js +33 -0
- package/dist/components/patterns/payload-inspector.js.map +1 -0
- package/dist/components/patterns/route-skeleton.d.ts +15 -0
- package/dist/components/patterns/route-skeleton.d.ts.map +1 -0
- package/dist/components/patterns/route-skeleton.js +7 -0
- package/dist/components/patterns/route-skeleton.js.map +1 -0
- package/dist/components/patterns/screen-layout.d.ts +14 -0
- package/dist/components/patterns/screen-layout.d.ts.map +1 -0
- package/dist/components/patterns/screen-layout.js +11 -0
- package/dist/components/patterns/screen-layout.js.map +1 -0
- package/dist/components/patterns/stale-write-banner.d.ts +26 -0
- package/dist/components/patterns/stale-write-banner.d.ts.map +1 -0
- package/dist/components/patterns/stale-write-banner.js +28 -0
- package/dist/components/patterns/stale-write-banner.js.map +1 -0
- package/dist/components/patterns/state-timeline.d.ts +58 -0
- package/dist/components/patterns/state-timeline.d.ts.map +1 -0
- package/dist/components/patterns/state-timeline.js +103 -0
- package/dist/components/patterns/state-timeline.js.map +1 -0
- package/dist/components/primitives/badge.d.ts +27 -0
- package/dist/components/primitives/badge.d.ts.map +1 -0
- package/dist/components/primitives/badge.js +33 -0
- package/dist/components/primitives/badge.js.map +1 -0
- package/dist/components/primitives/button.d.ts +12 -0
- package/dist/components/primitives/button.d.ts.map +1 -0
- package/dist/components/primitives/button.js +42 -0
- package/dist/components/primitives/button.js.map +1 -0
- package/dist/components/primitives/calendar.d.ts +39 -0
- package/dist/components/primitives/calendar.d.ts.map +1 -0
- package/dist/components/primitives/calendar.js +102 -0
- package/dist/components/primitives/calendar.js.map +1 -0
- package/dist/components/primitives/card.d.ts +24 -0
- package/dist/components/primitives/card.d.ts.map +1 -0
- package/dist/components/primitives/card.js +28 -0
- package/dist/components/primitives/card.js.map +1 -0
- package/dist/components/primitives/checkbox.d.ts +42 -0
- package/dist/components/primitives/checkbox.d.ts.map +1 -0
- package/dist/components/primitives/checkbox.js +31 -0
- package/dist/components/primitives/checkbox.js.map +1 -0
- package/dist/components/primitives/chip-select.d.ts +38 -0
- package/dist/components/primitives/chip-select.d.ts.map +1 -0
- package/dist/components/primitives/chip-select.js +25 -0
- package/dist/components/primitives/chip-select.js.map +1 -0
- package/dist/components/primitives/command-menu.d.ts +54 -0
- package/dist/components/primitives/command-menu.d.ts.map +1 -0
- package/dist/components/primitives/command-menu.js +24 -0
- package/dist/components/primitives/command-menu.js.map +1 -0
- package/dist/components/primitives/confirm-dialog.d.ts +52 -0
- package/dist/components/primitives/confirm-dialog.d.ts.map +1 -0
- package/dist/components/primitives/confirm-dialog.js +30 -0
- package/dist/components/primitives/confirm-dialog.js.map +1 -0
- package/dist/components/primitives/date-picker.d.ts +68 -0
- package/dist/components/primitives/date-picker.d.ts.map +1 -0
- package/dist/components/primitives/date-picker.js +107 -0
- package/dist/components/primitives/date-picker.js.map +1 -0
- package/dist/components/primitives/dialog.d.ts +50 -0
- package/dist/components/primitives/dialog.d.ts.map +1 -0
- package/dist/components/primitives/dialog.js +90 -0
- package/dist/components/primitives/dialog.js.map +1 -0
- package/dist/components/primitives/drawer.d.ts +71 -0
- package/dist/components/primitives/drawer.d.ts.map +1 -0
- package/dist/components/primitives/drawer.js +93 -0
- package/dist/components/primitives/drawer.js.map +1 -0
- package/dist/components/primitives/dropdown-menu.d.ts +40 -0
- package/dist/components/primitives/dropdown-menu.d.ts.map +1 -0
- package/dist/components/primitives/dropdown-menu.js +53 -0
- package/dist/components/primitives/dropdown-menu.js.map +1 -0
- package/dist/components/primitives/form-field.d.ts +94 -0
- package/dist/components/primitives/form-field.d.ts.map +1 -0
- package/dist/components/primitives/form-field.js +27 -0
- package/dist/components/primitives/form-field.js.map +1 -0
- package/dist/components/primitives/inline-confirm.d.ts +62 -0
- package/dist/components/primitives/inline-confirm.d.ts.map +1 -0
- package/dist/components/primitives/inline-confirm.js +11 -0
- package/dist/components/primitives/inline-confirm.js.map +1 -0
- package/dist/components/primitives/input.d.ts +4 -0
- package/dist/components/primitives/input.d.ts.map +1 -0
- package/dist/components/primitives/input.js +13 -0
- package/dist/components/primitives/input.js.map +1 -0
- package/dist/components/primitives/label.d.ts +26 -0
- package/dist/components/primitives/label.d.ts.map +1 -0
- package/dist/components/primitives/label.js +33 -0
- package/dist/components/primitives/label.js.map +1 -0
- package/dist/components/primitives/pagination.d.ts +54 -0
- package/dist/components/primitives/pagination.d.ts.map +1 -0
- package/dist/components/primitives/pagination.js +31 -0
- package/dist/components/primitives/pagination.js.map +1 -0
- package/dist/components/primitives/popover.d.ts +12 -0
- package/dist/components/primitives/popover.d.ts.map +1 -0
- package/dist/components/primitives/popover.js +22 -0
- package/dist/components/primitives/popover.js.map +1 -0
- package/dist/components/primitives/progress.d.ts +25 -0
- package/dist/components/primitives/progress.d.ts.map +1 -0
- package/dist/components/primitives/progress.js +25 -0
- package/dist/components/primitives/progress.js.map +1 -0
- package/dist/components/primitives/radio-group.d.ts +47 -0
- package/dist/components/primitives/radio-group.d.ts.map +1 -0
- package/dist/components/primitives/radio-group.js +11 -0
- package/dist/components/primitives/radio-group.js.map +1 -0
- package/dist/components/primitives/select.d.ts +39 -0
- package/dist/components/primitives/select.d.ts.map +1 -0
- package/dist/components/primitives/select.js +142 -0
- package/dist/components/primitives/select.js.map +1 -0
- package/dist/components/primitives/separator.d.ts +9 -0
- package/dist/components/primitives/separator.d.ts.map +1 -0
- package/dist/components/primitives/separator.js +16 -0
- package/dist/components/primitives/separator.js.map +1 -0
- package/dist/components/primitives/side-nav.d.ts +65 -0
- package/dist/components/primitives/side-nav.d.ts.map +1 -0
- package/dist/components/primitives/side-nav.js +100 -0
- package/dist/components/primitives/side-nav.js.map +1 -0
- package/dist/components/primitives/skeleton.d.ts +22 -0
- package/dist/components/primitives/skeleton.d.ts.map +1 -0
- package/dist/components/primitives/skeleton.js +25 -0
- package/dist/components/primitives/skeleton.js.map +1 -0
- package/dist/components/primitives/spinner.d.ts +20 -0
- package/dist/components/primitives/spinner.d.ts.map +1 -0
- package/dist/components/primitives/spinner.js +21 -0
- package/dist/components/primitives/spinner.js.map +1 -0
- package/dist/components/primitives/switch.d.ts +29 -0
- package/dist/components/primitives/switch.d.ts.map +1 -0
- package/dist/components/primitives/switch.js +24 -0
- package/dist/components/primitives/switch.js.map +1 -0
- package/dist/components/primitives/table.d.ts +23 -0
- package/dist/components/primitives/table.d.ts.map +1 -0
- package/dist/components/primitives/table.js +81 -0
- package/dist/components/primitives/table.js.map +1 -0
- package/dist/components/primitives/tabs.d.ts +38 -0
- package/dist/components/primitives/tabs.d.ts.map +1 -0
- package/dist/components/primitives/tabs.js +84 -0
- package/dist/components/primitives/tabs.js.map +1 -0
- package/dist/components/primitives/textarea.d.ts +4 -0
- package/dist/components/primitives/textarea.d.ts.map +1 -0
- package/dist/components/primitives/textarea.js +11 -0
- package/dist/components/primitives/textarea.js.map +1 -0
- package/dist/components/primitives/toast.d.ts +24 -0
- package/dist/components/primitives/toast.d.ts.map +1 -0
- package/dist/components/primitives/toast.js +58 -0
- package/dist/components/primitives/toast.js.map +1 -0
- package/dist/components/primitives/tooltip.d.ts +30 -0
- package/dist/components/primitives/tooltip.d.ts.map +1 -0
- package/dist/components/primitives/tooltip.js +6 -0
- package/dist/components/primitives/tooltip.js.map +1 -0
- package/dist/components/status/state-chip.d.ts +53 -0
- package/dist/components/status/state-chip.d.ts.map +1 -0
- package/dist/components/status/state-chip.js +8 -0
- package/dist/components/status/state-chip.js.map +1 -0
- package/dist/components/status/state-mark.d.ts +21 -0
- package/dist/components/status/state-mark.d.ts.map +1 -0
- package/dist/components/status/state-mark.js +90 -0
- package/dist/components/status/state-mark.js.map +1 -0
- package/dist/components/status/status-pill.d.ts +62 -0
- package/dist/components/status/status-pill.d.ts.map +1 -0
- package/dist/components/status/status-pill.js +65 -0
- package/dist/components/status/status-pill.js.map +1 -0
- package/dist/components/status/status-tokens.d.ts +110 -0
- package/dist/components/status/status-tokens.d.ts.map +1 -0
- package/dist/components/status/status-tokens.js +102 -0
- package/dist/components/status/status-tokens.js.map +1 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cn.d.ts +11 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +78 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/lib/money.d.ts +73 -0
- package/dist/lib/money.d.ts.map +1 -0
- package/dist/lib/money.js +123 -0
- package/dist/lib/money.js.map +1 -0
- package/dist/lib/omit-undefined.d.ts +17 -0
- package/dist/lib/omit-undefined.d.ts.map +1 -0
- package/dist/lib/omit-undefined.js +24 -0
- package/dist/lib/omit-undefined.js.map +1 -0
- package/package.json +100 -0
- package/src/components/data/code.tsx +43 -0
- package/src/components/data/copy-button.tsx +97 -0
- package/src/components/data/detail-row.tsx +113 -0
- package/src/components/data/id-display.tsx +49 -0
- package/src/components/data/masked-value.tsx +96 -0
- package/src/components/data/money.tsx +46 -0
- package/src/components/data/phone-display.tsx +60 -0
- package/src/components/data/stat-tile.tsx +68 -0
- package/src/components/data/timestamp-display.tsx +106 -0
- package/src/components/patterns/inline-banner.tsx +60 -0
- package/src/components/patterns/inline-empty-state.tsx +45 -0
- package/src/components/patterns/live-row.tsx +75 -0
- package/src/components/patterns/payload-inspector.tsx +157 -0
- package/src/components/patterns/route-skeleton.tsx +77 -0
- package/src/components/patterns/screen-layout.tsx +44 -0
- package/src/components/patterns/stale-write-banner.tsx +44 -0
- package/src/components/patterns/state-timeline.tsx +229 -0
- package/src/components/primitives/badge.tsx +40 -0
- package/src/components/primitives/button.tsx +54 -0
- package/src/components/primitives/calendar.tsx +136 -0
- package/src/components/primitives/card.tsx +70 -0
- package/src/components/primitives/checkbox.tsx +101 -0
- package/src/components/primitives/chip-select.tsx +105 -0
- package/src/components/primitives/command-menu.tsx +92 -0
- package/src/components/primitives/confirm-dialog.tsx +109 -0
- package/src/components/primitives/date-picker.tsx +271 -0
- package/src/components/primitives/dialog.tsx +195 -0
- package/src/components/primitives/drawer.tsx +326 -0
- package/src/components/primitives/dropdown-menu.tsx +128 -0
- package/src/components/primitives/form-field.tsx +136 -0
- package/src/components/primitives/inline-confirm.tsx +118 -0
- package/src/components/primitives/input.tsx +21 -0
- package/src/components/primitives/label.tsx +43 -0
- package/src/components/primitives/pagination.tsx +110 -0
- package/src/components/primitives/popover.tsx +41 -0
- package/src/components/primitives/progress.tsx +66 -0
- package/src/components/primitives/radio-group.tsx +90 -0
- package/src/components/primitives/select.tsx +254 -0
- package/src/components/primitives/separator.tsx +49 -0
- package/src/components/primitives/side-nav.tsx +253 -0
- package/src/components/primitives/skeleton.tsx +25 -0
- package/src/components/primitives/spinner.tsx +39 -0
- package/src/components/primitives/switch.tsx +74 -0
- package/src/components/primitives/table.tsx +157 -0
- package/src/components/primitives/tabs.tsx +163 -0
- package/src/components/primitives/textarea.tsx +21 -0
- package/src/components/primitives/toast.tsx +117 -0
- package/src/components/primitives/tooltip.tsx +38 -0
- package/src/components/status/state-chip.tsx +70 -0
- package/src/components/status/state-mark.tsx +154 -0
- package/src/components/status/status-pill.tsx +153 -0
- package/src/components/status/status-tokens.ts +173 -0
- package/src/index.ts +96 -0
- package/src/lib/cn.ts +168 -0
- package/src/lib/money.ts +164 -0
- package/src/lib/omit-undefined.ts +25 -0
- package/src/styles/theme.css +381 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import type { ButtonHTMLAttributes } from "react";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
|
|
6
|
+
// daisyUI does the heavy lifting (`btn-primary`, `btn-outline`, `btn-ghost`,
|
|
7
|
+
// `btn-error`); the design doc's rule (§5.1) is that there is no
|
|
8
|
+
// "success"/"warning" button variant — those hues are reserved for status,
|
|
9
|
+
// so only these four exist. `btn-primary` is genuinely the achromatic
|
|
10
|
+
// inverse fill (§1.3: near-black on light, near-white on dark) — `neutral`
|
|
11
|
+
// is a *different*, deliberately quieter token (this theme's surface-3-ish
|
|
12
|
+
// fill, used for badges) and using it here read as a washed-out primary
|
|
13
|
+
// button in light theme; caught visually, not by inspection.
|
|
14
|
+
//
|
|
15
|
+
// D4/D11: rebuilt on `cva()` per docs/design/console-redesign.md §6.3 —
|
|
16
|
+
// `Slot`/`asChild` are gone (nothing in this repo used `<Button asChild>`,
|
|
17
|
+
// confirmed by grep before removing it), and `buttonVariants` is exported
|
|
18
|
+
// standalone so a link that must look like a button reaches for the class
|
|
19
|
+
// string directly: `<a className={buttonVariants({ variant: "secondary" })}>`.
|
|
20
|
+
// `rounded-field` in the base string is the one deliberate D8 addition —
|
|
21
|
+
// daisyUI's own `.btn` class already applies `--radius-field` per corner
|
|
22
|
+
// internally, so this doesn't change the rendered radius, only makes the
|
|
23
|
+
// D8 register explicit at the call site, matching §6.3's own sketch
|
|
24
|
+
// verbatim.
|
|
25
|
+
export const buttonVariants = cva("btn font-sans font-semibold rounded-field", {
|
|
26
|
+
variants: {
|
|
27
|
+
variant: {
|
|
28
|
+
primary: "btn-primary",
|
|
29
|
+
secondary: "btn-outline",
|
|
30
|
+
ghost: "btn-ghost",
|
|
31
|
+
destructive: "btn-error",
|
|
32
|
+
},
|
|
33
|
+
size: {
|
|
34
|
+
sm: "btn-sm",
|
|
35
|
+
md: "",
|
|
36
|
+
icon: "btn-square btn-sm",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
defaultVariants: { variant: "primary", size: "md" },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export type ButtonVariant = NonNullable<VariantProps<typeof buttonVariants>["variant"]>;
|
|
43
|
+
export type ButtonSize = NonNullable<VariantProps<typeof buttonVariants>["size"]>;
|
|
44
|
+
|
|
45
|
+
export interface ButtonProps
|
|
46
|
+
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
|
47
|
+
VariantProps<typeof buttonVariants> {}
|
|
48
|
+
|
|
49
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
50
|
+
({ className, variant, size, ...props }, ref) => (
|
|
51
|
+
<button ref={ref} className={cn(buttonVariants({ variant, size }), className)} {...props} />
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
Button.displayName = "Button";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
|
+
import { DayPicker, type DayPickerProps } from "react-day-picker";
|
|
5
|
+
import { cn } from "../../lib/cn";
|
|
6
|
+
|
|
7
|
+
export type CalendarProps = DayPickerProps & { className?: string | undefined };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `react-day-picker`, themed with this design system's own tokens.
|
|
11
|
+
*
|
|
12
|
+
* # Why the whole `classNames` map is spelled out
|
|
13
|
+
*
|
|
14
|
+
* `react-day-picker` ships a stylesheet (`react-day-picker/style.css`)
|
|
15
|
+
* built on its own `--rdp-*` variables. Importing it would put a second,
|
|
16
|
+
* independent theme in the page whose colours are set from somewhere
|
|
17
|
+
* other than `theme.css` — so the first time anyone changed a surface
|
|
18
|
+
* colour, the calendar would not follow, and the mismatch would be
|
|
19
|
+
* invisible until someone opened a date picker.
|
|
20
|
+
*
|
|
21
|
+
* The stylesheet is therefore **not** imported, and every element gets a
|
|
22
|
+
* class from the tokens here instead. That is more code than a handful of
|
|
23
|
+
* overrides, and it is the point: there is exactly one place this
|
|
24
|
+
* component's colours come from, and it is the same place every other
|
|
25
|
+
* component's come from.
|
|
26
|
+
*
|
|
27
|
+
* # `selected` without `onSelect` is not a binding
|
|
28
|
+
*
|
|
29
|
+
* This component is a thin pass-through, so react-day-picker's own
|
|
30
|
+
* control semantics apply unchanged: given `selected` but no `onSelect`,
|
|
31
|
+
* it manages selection internally and treats `selected` as an initial
|
|
32
|
+
* value. A caller who expects a controlled component gets one that
|
|
33
|
+
* silently diverges from its own prop the first time someone clicks a
|
|
34
|
+
* day — found exactly that way, in a gallery demo. Pass both, or neither.
|
|
35
|
+
*
|
|
36
|
+
* # Layout is deliberately not themed away
|
|
37
|
+
*
|
|
38
|
+
* The grid itself is a `<table>` and needs `border-collapse` plus fixed
|
|
39
|
+
* cell sizing to stay square; those are structural, not thematic, and
|
|
40
|
+
* live here rather than in a token.
|
|
41
|
+
*/
|
|
42
|
+
export function Calendar({
|
|
43
|
+
className,
|
|
44
|
+
classNames,
|
|
45
|
+
showOutsideDays = true,
|
|
46
|
+
...props
|
|
47
|
+
}: CalendarProps) {
|
|
48
|
+
return (
|
|
49
|
+
<DayPicker
|
|
50
|
+
showOutsideDays={showOutsideDays}
|
|
51
|
+
className={cn("p-3", className)}
|
|
52
|
+
classNames={{
|
|
53
|
+
root: "text-body text-foreground",
|
|
54
|
+
months: "flex flex-col gap-4 sm:flex-row",
|
|
55
|
+
month: "flex flex-col gap-3",
|
|
56
|
+
month_caption: "flex h-8 items-center justify-center",
|
|
57
|
+
caption_label: "font-medium text-body text-foreground",
|
|
58
|
+
nav: "flex items-center gap-1",
|
|
59
|
+
// The nav buttons are absolutely positioned by RDP's own layout at
|
|
60
|
+
// the top corners of the month; these restyle them in place.
|
|
61
|
+
button_previous: cn(
|
|
62
|
+
"absolute top-3 left-3 inline-flex size-7 items-center justify-center rounded-sm",
|
|
63
|
+
"text-muted-foreground transition-colors hover:bg-surface-3 hover:text-foreground",
|
|
64
|
+
"disabled:pointer-events-none disabled:opacity-40",
|
|
65
|
+
),
|
|
66
|
+
button_next: cn(
|
|
67
|
+
"absolute top-3 right-3 inline-flex size-7 items-center justify-center rounded-sm",
|
|
68
|
+
"text-muted-foreground transition-colors hover:bg-surface-3 hover:text-foreground",
|
|
69
|
+
"disabled:pointer-events-none disabled:opacity-40",
|
|
70
|
+
),
|
|
71
|
+
month_grid: "w-full border-collapse",
|
|
72
|
+
weekdays: "flex",
|
|
73
|
+
weekday: "w-8 font-normal text-caption text-subtle-foreground",
|
|
74
|
+
week: "mt-1 flex w-full",
|
|
75
|
+
// Selection state lives on the CELL, not on the button inside it.
|
|
76
|
+
// Read off the real DOM, not guessed: react-day-picker puts
|
|
77
|
+
// `aria-selected` and `data-selected` on the `<td>` and leaves the
|
|
78
|
+
// `<button>` carrying only `type`/`tabindex`/`aria-label`. An
|
|
79
|
+
// earlier revision of this file styled the button with
|
|
80
|
+
// `aria-selected:bg-primary` and the cell with
|
|
81
|
+
// `[&:has([data-range-middle])]`, and BOTH silently matched
|
|
82
|
+
// nothing — no selected day had a fill and no range had a tint.
|
|
83
|
+
// Neither `tsc` nor the classNames-key test could see it: the keys
|
|
84
|
+
// were all real, and the class strings were valid CSS for a DOM
|
|
85
|
+
// shape that does not exist. Found by rendering it.
|
|
86
|
+
day: "relative size-8 p-0 text-center",
|
|
87
|
+
day_button: cn(
|
|
88
|
+
"inline-flex size-8 items-center justify-center rounded-sm font-mono text-caption tabular-nums",
|
|
89
|
+
"text-foreground transition-colors hover:bg-surface-3",
|
|
90
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
91
|
+
),
|
|
92
|
+
// Fills the button from the cell's own state, which is where the
|
|
93
|
+
// state actually is. Covers single mode outright and both ends of
|
|
94
|
+
// a range; `range_middle` below overrides it for the interior.
|
|
95
|
+
selected:
|
|
96
|
+
"[&>button]:bg-primary [&>button]:text-primary-content [&>button]:hover:bg-primary",
|
|
97
|
+
today: "font-semibold text-state-uncertain-fg",
|
|
98
|
+
outside: "text-subtle-foreground opacity-50",
|
|
99
|
+
disabled: "pointer-events-none opacity-30",
|
|
100
|
+
hidden: "invisible",
|
|
101
|
+
// The interior of a range: tint the CELL so consecutive days meet
|
|
102
|
+
// with no gap, and knock the button's own fill back out.
|
|
103
|
+
//
|
|
104
|
+
// Two things here were wrong on the first attempt and are worth
|
|
105
|
+
// keeping written down. The tint was `bg-state-neutral-bg`, which
|
|
106
|
+
// is a real token that resolves to `transparent` — `neutral` is
|
|
107
|
+
// the quiet hue and deliberately has no fill — so the interior
|
|
108
|
+
// rendered as nothing at all. It is a wash of the selection
|
|
109
|
+
// colour instead, which is also what it means. And the `!` is
|
|
110
|
+
// load-bearing: a middle day also carries `selected` above, and
|
|
111
|
+
// both rules are arbitrary variants of equal specificity, so
|
|
112
|
+
// without it the winner is whichever class Tailwind emits last.
|
|
113
|
+
range_middle:
|
|
114
|
+
"bg-primary/15 [&>button]:bg-transparent! [&>button]:text-foreground! [&>button]:hover:bg-surface-3!",
|
|
115
|
+
range_start: "rounded-l-sm",
|
|
116
|
+
range_end: "rounded-r-sm",
|
|
117
|
+
week_number: "w-8 font-mono text-caption text-subtle-foreground",
|
|
118
|
+
footer: "pt-2 text-caption text-muted-foreground",
|
|
119
|
+
...classNames,
|
|
120
|
+
}}
|
|
121
|
+
components={{
|
|
122
|
+
// RDP renders one `Chevron` for both directions and flips it with
|
|
123
|
+
// `orientation`; swapping in two real icons reads better than a
|
|
124
|
+
// CSS rotation and keeps the stroke weight matching every other
|
|
125
|
+
// icon in the system.
|
|
126
|
+
Chevron: ({ orientation, ...iconProps }) =>
|
|
127
|
+
orientation === "left" ? (
|
|
128
|
+
<ChevronLeft size={16} strokeWidth={1.5} {...iconProps} />
|
|
129
|
+
) : (
|
|
130
|
+
<ChevronRight size={16} strokeWidth={1.5} {...iconProps} />
|
|
131
|
+
),
|
|
132
|
+
}}
|
|
133
|
+
{...props}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type CardProps = HTMLAttributes<HTMLDivElement>;
|
|
5
|
+
|
|
6
|
+
// Borders, not shadows (design doc §3.6): every card is `--shadow-none` +
|
|
7
|
+
// a 1px border. daisyUI's own `card` class would add `shadow-xl` under
|
|
8
|
+
// some presets — explicitly opted out here rather than stripped after the
|
|
9
|
+
// fact, since this is a hand-built component, not a generated one.
|
|
10
|
+
//
|
|
11
|
+
// D8: the previous `rounded-sm` override is dropped, not swapped for
|
|
12
|
+
// `rounded-box` — daisyUI's own `.card` rule already sets
|
|
13
|
+
// `border-radius: var(--radius-box)` (confirmed by reading
|
|
14
|
+
// `daisyui/components/card.css` directly), so an explicit override was
|
|
15
|
+
// fighting the component's own class at equal specificity for no reason.
|
|
16
|
+
// This *is* the deliberate D8 value change §5's inventory calls for
|
|
17
|
+
// ("new radius (`--radius-box`)") — the previous `rounded-sm` resolved to
|
|
18
|
+
// `--radius-field` (12px) once Phase 0 rewrote the alias, one tier tighter
|
|
19
|
+
// than the box-tier corners the reference lock (§1.1/§1.2/§1.5) shows for
|
|
20
|
+
// card/drawer/panel chrome.
|
|
21
|
+
export function Card({ className, ...props }: CardProps) {
|
|
22
|
+
return (
|
|
23
|
+
<div className={cn("card border border-edge bg-base-300 shadow-none", className)} {...props} />
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CardHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
28
|
+
title: ReactNode;
|
|
29
|
+
/**
|
|
30
|
+
* Heading level for `title`. Defaults to `h3`, which is right for a
|
|
31
|
+
* card inside a section that already has an `h2`.
|
|
32
|
+
*
|
|
33
|
+
* It is a prop rather than a constant because a heading level is a
|
|
34
|
+
* property of the page's outline, not of the card: a card placed
|
|
35
|
+
* directly under the screen's `h1` with `h3` skips a level, which axe
|
|
36
|
+
* reports as `heading-order` and a screen-reader user experiences as a
|
|
37
|
+
* missing section.
|
|
38
|
+
*/
|
|
39
|
+
headingLevel?: 2 | 3 | 4 | 5 | 6;
|
|
40
|
+
/** Optional mono metadata line beneath the title. */
|
|
41
|
+
meta?: ReactNode;
|
|
42
|
+
/** Right-aligned slot, e.g. a row of buttons. */
|
|
43
|
+
action?: ReactNode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function CardHeader({
|
|
47
|
+
title,
|
|
48
|
+
meta,
|
|
49
|
+
action,
|
|
50
|
+
headingLevel = 3,
|
|
51
|
+
className,
|
|
52
|
+
...props
|
|
53
|
+
}: CardHeaderProps) {
|
|
54
|
+
const Heading = `h${headingLevel}` as const;
|
|
55
|
+
return (
|
|
56
|
+
<div className={cn("flex items-start justify-between gap-4 p-4", className)} {...props}>
|
|
57
|
+
<div className="min-w-0">
|
|
58
|
+
<Heading className="truncate font-medium text-foreground text-title-sm">{title}</Heading>
|
|
59
|
+
{meta != null && (
|
|
60
|
+
<p className="mt-1 truncate font-mono text-caption text-subtle-foreground">{meta}</p>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
{action != null && <div className="shrink-0">{action}</div>}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function CardBody({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
69
|
+
return <div className={cn("px-4 pb-4", className)} {...props} />;
|
|
70
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Field, Checkbox as HeadlessCheckbox, Label as HeadlessLabel } from "@headlessui/react";
|
|
4
|
+
import { Check, Minus } from "lucide-react";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import { cn } from "../../lib/cn";
|
|
7
|
+
|
|
8
|
+
export interface CheckboxProps {
|
|
9
|
+
checked: boolean;
|
|
10
|
+
/** Omit for an uncontrolled display-only checkbox (e.g. inside a row
|
|
11
|
+
* whose whole surface handles the click). */
|
|
12
|
+
onCheckedChange?: ((checked: boolean) => void) | undefined;
|
|
13
|
+
/** Neither on nor off — a parent whose children disagree. Rendered as a
|
|
14
|
+
* dash. `checked` is still what a click toggles from. */
|
|
15
|
+
indeterminate?: boolean;
|
|
16
|
+
disabled?: boolean | undefined;
|
|
17
|
+
/** Required when no visible `<CheckboxField>` label wraps it. */
|
|
18
|
+
"aria-label"?: string | undefined;
|
|
19
|
+
"aria-labelledby"?: string | undefined;
|
|
20
|
+
className?: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A checkbox, rendered as a real focusable control rather than a styled
|
|
25
|
+
* `<input>`.
|
|
26
|
+
*
|
|
27
|
+
* Headless UI gives it `role="checkbox"`, `aria-checked`, space-to-toggle
|
|
28
|
+
* and focus management; everything here is appearance. A native
|
|
29
|
+
* `<input type="checkbox">` styled with `appearance-none` would be
|
|
30
|
+
* equivalent for the plain case but cannot express `indeterminate`
|
|
31
|
+
* without an imperative DOM write on a ref, which is how that state ends
|
|
32
|
+
* up out of sync with React's own.
|
|
33
|
+
*/
|
|
34
|
+
export function Checkbox({
|
|
35
|
+
checked,
|
|
36
|
+
onCheckedChange,
|
|
37
|
+
indeterminate = false,
|
|
38
|
+
disabled,
|
|
39
|
+
className,
|
|
40
|
+
...aria
|
|
41
|
+
}: CheckboxProps) {
|
|
42
|
+
return (
|
|
43
|
+
<HeadlessCheckbox
|
|
44
|
+
checked={checked}
|
|
45
|
+
onChange={onCheckedChange ?? (() => undefined)}
|
|
46
|
+
disabled={disabled ?? false}
|
|
47
|
+
indeterminate={indeterminate}
|
|
48
|
+
className={cn(
|
|
49
|
+
"group inline-flex size-4 shrink-0 items-center justify-center rounded-selector border border-edge-strong bg-surface-2",
|
|
50
|
+
"transition-colors data-checked:border-primary data-checked:bg-primary",
|
|
51
|
+
"data-indeterminate:border-primary data-indeterminate:bg-primary",
|
|
52
|
+
"data-focus:outline-none data-focus:ring-1 data-focus:ring-ring",
|
|
53
|
+
"data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
{...aria}
|
|
57
|
+
>
|
|
58
|
+
{indeterminate ? (
|
|
59
|
+
<Minus size={12} strokeWidth={2.5} className="text-primary-content" />
|
|
60
|
+
) : (
|
|
61
|
+
<Check
|
|
62
|
+
size={12}
|
|
63
|
+
strokeWidth={2.5}
|
|
64
|
+
className="text-primary-content opacity-0 group-data-checked:opacity-100"
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
</HeadlessCheckbox>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface CheckboxFieldProps extends Omit<CheckboxProps, "aria-label" | "aria-labelledby"> {
|
|
72
|
+
label: ReactNode;
|
|
73
|
+
/** One line beneath the label. */
|
|
74
|
+
description?: ReactNode;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A checkbox with a clickable label beside it.
|
|
79
|
+
*
|
|
80
|
+
* Separate from `FormField`, which stacks a label *above* a control:
|
|
81
|
+
* a checkbox's label belongs on the same line and must itself be a click
|
|
82
|
+
* target, which is a different layout and a different association
|
|
83
|
+
* (`Field`/`Label` wiring rather than `htmlFor`).
|
|
84
|
+
*/
|
|
85
|
+
export function CheckboxField({ label, description, className, ...props }: CheckboxFieldProps) {
|
|
86
|
+
return (
|
|
87
|
+
<Field className={cn("flex items-start gap-2", className)}>
|
|
88
|
+
<span className="mt-0.5">
|
|
89
|
+
<Checkbox {...props} />
|
|
90
|
+
</span>
|
|
91
|
+
<span className="flex flex-col gap-0.5">
|
|
92
|
+
<HeadlessLabel className="cursor-pointer text-body text-foreground data-disabled:cursor-not-allowed data-disabled:opacity-50">
|
|
93
|
+
{label}
|
|
94
|
+
</HeadlessLabel>
|
|
95
|
+
{description !== undefined && (
|
|
96
|
+
<span className="text-caption text-muted-foreground">{description}</span>
|
|
97
|
+
)}
|
|
98
|
+
</span>
|
|
99
|
+
</Field>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// No `CheckboxGroup` — Headless UI 2.2.10 does not export one (checked,
|
|
4
|
+
// not assumed: `tsc` rejected it), so the grouping is done here.
|
|
5
|
+
//
|
|
6
|
+
// A `<fieldset>`, not `<div role="group">`. Biome's `a11y/useSemanticElements`
|
|
7
|
+
// is right that the native element beats the ARIA role, and it caught the
|
|
8
|
+
// div in CI after a local `biome check frontends` had passed — CI runs
|
|
9
|
+
// `pnpm biome ci .`, which is not the same command. Run CI's own.
|
|
10
|
+
//
|
|
11
|
+
// `min-w-0` because a `<fieldset>` has a UA `min-width: min-content` that a
|
|
12
|
+
// `<div>` does not, which would otherwise stop the chips wrapping inside a
|
|
13
|
+
// narrow drawer.
|
|
14
|
+
import { Checkbox, Field, Label } from "@headlessui/react";
|
|
15
|
+
import { Check } from "lucide-react";
|
|
16
|
+
import type { ReactNode } from "react";
|
|
17
|
+
import { cn } from "../../lib/cn";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Multi-select over a small, fixed vocabulary, rendered as toggleable
|
|
21
|
+
* chips.
|
|
22
|
+
*
|
|
23
|
+
* # Why chips rather than a text field or a multi-select
|
|
24
|
+
*
|
|
25
|
+
* The motivating case is OAuth scopes when provisioning a client. That
|
|
26
|
+
* field was a free-text input: an operator had to already know both that
|
|
27
|
+
* scopes are space-delimited *and* what the fourteen valid strings are,
|
|
28
|
+
* with a typo silently producing a client that is denied at Layer 2 with
|
|
29
|
+
* no hint why. Showing the vocabulary is the whole point.
|
|
30
|
+
*
|
|
31
|
+
* Not a `Select` with `multiple`: this renders inline with no portal and
|
|
32
|
+
* no transition, so it cannot hit the focus-trap bug that made `Select`
|
|
33
|
+
* unusable inside a drawer (#315). Same reasoning as `RadioGroup` — for a
|
|
34
|
+
* bounded vocabulary in a drawer, the inline control is the safer one.
|
|
35
|
+
*
|
|
36
|
+
* Not a `<datalist>` or a tag input either: both still let a caller type
|
|
37
|
+
* something outside the vocabulary, which is exactly the property being
|
|
38
|
+
* removed.
|
|
39
|
+
*/
|
|
40
|
+
export interface ChipOption<T extends string> {
|
|
41
|
+
value: T;
|
|
42
|
+
label: ReactNode;
|
|
43
|
+
/** Shown beneath the label — for scopes, what the scope actually permits. */
|
|
44
|
+
description?: ReactNode;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ChipSelectProps<T extends string> {
|
|
48
|
+
value: readonly T[];
|
|
49
|
+
onValueChange: (value: T[]) => void;
|
|
50
|
+
options: readonly ChipOption<T>[];
|
|
51
|
+
disabled?: boolean | undefined;
|
|
52
|
+
"aria-label"?: string | undefined;
|
|
53
|
+
className?: string | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function ChipSelect<T extends string>({
|
|
57
|
+
value,
|
|
58
|
+
onValueChange,
|
|
59
|
+
options,
|
|
60
|
+
disabled,
|
|
61
|
+
className,
|
|
62
|
+
...aria
|
|
63
|
+
}: ChipSelectProps<T>) {
|
|
64
|
+
return (
|
|
65
|
+
<fieldset className={cn("flex min-w-0 flex-wrap gap-2", className)} {...aria}>
|
|
66
|
+
{options.map((option) => {
|
|
67
|
+
const checked = value.includes(option.value);
|
|
68
|
+
return (
|
|
69
|
+
<Field key={option.value}>
|
|
70
|
+
<Checkbox
|
|
71
|
+
checked={checked}
|
|
72
|
+
disabled={disabled ?? false}
|
|
73
|
+
onChange={(next: boolean) => {
|
|
74
|
+
onValueChange(
|
|
75
|
+
next ? [...value, option.value] : value.filter((v) => v !== option.value),
|
|
76
|
+
);
|
|
77
|
+
}}
|
|
78
|
+
className={cn(
|
|
79
|
+
"flex cursor-pointer items-start gap-2 rounded-sm border border-edge bg-surface-2 px-3 py-2 text-body text-muted-foreground",
|
|
80
|
+
"data-checked:border-state-success-border data-checked:bg-state-success-bg data-checked:text-state-success-fg",
|
|
81
|
+
"data-focus:outline-none data-focus:ring-1 data-focus:ring-state-success-border",
|
|
82
|
+
"data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
<span
|
|
86
|
+
className={cn(
|
|
87
|
+
"mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-xs border border-edge",
|
|
88
|
+
checked && "border-state-success-fg",
|
|
89
|
+
)}
|
|
90
|
+
>
|
|
91
|
+
{checked && <Check className="size-3" aria-hidden="true" />}
|
|
92
|
+
</span>
|
|
93
|
+
<span className="flex flex-col gap-0.5">
|
|
94
|
+
<Label className="cursor-pointer font-mono font-medium">{option.label}</Label>
|
|
95
|
+
{option.description !== undefined && (
|
|
96
|
+
<span className="text-caption text-subtle-foreground">{option.description}</span>
|
|
97
|
+
)}
|
|
98
|
+
</span>
|
|
99
|
+
</Checkbox>
|
|
100
|
+
</Field>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
</fieldset>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Command as CommandPrimitive } from "cmdk";
|
|
4
|
+
import { Search } from "lucide-react";
|
|
5
|
+
import { forwardRef } from "react";
|
|
6
|
+
import { cn } from "../../lib/cn";
|
|
7
|
+
|
|
8
|
+
// cmdk is standalone (no Radix wrapper needed — it already owns its own
|
|
9
|
+
// keyboard nav/ARIA). Styled with the same surface-2/border-edge/radius-md
|
|
10
|
+
// floating-layer treatment as Popover/DropdownMenu for visual consistency.
|
|
11
|
+
|
|
12
|
+
export const CommandMenu = forwardRef<
|
|
13
|
+
React.ElementRef<typeof CommandPrimitive>,
|
|
14
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
|
15
|
+
>(({ className, ...props }, ref) => (
|
|
16
|
+
<CommandPrimitive
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={cn(
|
|
19
|
+
"flex w-full flex-col overflow-hidden rounded-md border border-edge bg-surface-2 text-foreground",
|
|
20
|
+
className,
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
));
|
|
25
|
+
CommandMenu.displayName = "CommandMenu";
|
|
26
|
+
|
|
27
|
+
export function CommandMenuInput(
|
|
28
|
+
props: React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>,
|
|
29
|
+
) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex items-center gap-2 border-edge border-b px-3">
|
|
32
|
+
<Search size={14} strokeWidth={1.5} className="shrink-0 text-subtle-foreground" />
|
|
33
|
+
<CommandPrimitive.Input
|
|
34
|
+
className="h-10 w-full bg-transparent text-prose outline-none placeholder:text-subtle-foreground"
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const CommandMenuList = forwardRef<
|
|
42
|
+
React.ElementRef<typeof CommandPrimitive.List>,
|
|
43
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
|
44
|
+
>(({ className, ...props }, ref) => (
|
|
45
|
+
<CommandPrimitive.List
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cn("max-h-80 overflow-y-auto overflow-x-hidden p-1", className)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
));
|
|
51
|
+
CommandMenuList.displayName = "CommandMenuList";
|
|
52
|
+
|
|
53
|
+
export function CommandMenuEmpty(
|
|
54
|
+
props: React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>,
|
|
55
|
+
) {
|
|
56
|
+
return (
|
|
57
|
+
<CommandPrimitive.Empty
|
|
58
|
+
className="px-3 py-6 text-center text-body text-muted-foreground"
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const CommandMenuGroup = forwardRef<
|
|
65
|
+
React.ElementRef<typeof CommandPrimitive.Group>,
|
|
66
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
|
67
|
+
>(({ className, ...props }, ref) => (
|
|
68
|
+
<CommandPrimitive.Group
|
|
69
|
+
ref={ref}
|
|
70
|
+
className={cn(
|
|
71
|
+
"text-body [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-micro [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
));
|
|
77
|
+
CommandMenuGroup.displayName = "CommandMenuGroup";
|
|
78
|
+
|
|
79
|
+
export const CommandMenuItem = forwardRef<
|
|
80
|
+
React.ElementRef<typeof CommandPrimitive.Item>,
|
|
81
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
|
82
|
+
>(({ className, ...props }, ref) => (
|
|
83
|
+
<CommandPrimitive.Item
|
|
84
|
+
ref={ref}
|
|
85
|
+
className={cn(
|
|
86
|
+
"flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-body data-[selected=true]:bg-surface-3",
|
|
87
|
+
className,
|
|
88
|
+
)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
));
|
|
92
|
+
CommandMenuItem.displayName = "CommandMenuItem";
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from "./dialog";
|
|
13
|
+
import { Spinner } from "./spinner";
|
|
14
|
+
|
|
15
|
+
export interface ConfirmDialogProps {
|
|
16
|
+
open: boolean;
|
|
17
|
+
onOpenChange: (open: boolean) => void;
|
|
18
|
+
title: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* What will happen, in the user's terms — and for anything
|
|
21
|
+
* irreversible, say so here. "This cannot be undone" is not filler;
|
|
22
|
+
* it is the one sentence that changes the decision.
|
|
23
|
+
*/
|
|
24
|
+
description: ReactNode;
|
|
25
|
+
/** Names the action, e.g. "Delete endpoint". Never "OK" or "Yes": the
|
|
26
|
+
* confirm button is often the only text read before clicking, so it has
|
|
27
|
+
* to say what it does on its own. */
|
|
28
|
+
confirmLabel: string;
|
|
29
|
+
cancelLabel?: string;
|
|
30
|
+
/** `destructive` renders the confirm button in the error variant. Use
|
|
31
|
+
* it for anything that deletes, revokes or cancels — not merely for
|
|
32
|
+
* anything important. */
|
|
33
|
+
tone?: "default" | "destructive";
|
|
34
|
+
onConfirm: () => void;
|
|
35
|
+
/** In-flight: both buttons disabled, confirm shows a spinner. The
|
|
36
|
+
* dialog does not close itself — the caller closes it when the write
|
|
37
|
+
* actually lands, so a failure leaves the dialog open with its error
|
|
38
|
+
* rather than vanishing and losing the context. */
|
|
39
|
+
busy?: boolean;
|
|
40
|
+
/** Rendered above the buttons. The place for a failed attempt's message. */
|
|
41
|
+
children?: ReactNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A modal yes/no for an action worth interrupting someone over.
|
|
46
|
+
*
|
|
47
|
+
* # When *not* to use it
|
|
48
|
+
*
|
|
49
|
+
* Most of the time. A modal confirm is an interruption, and an
|
|
50
|
+
* interruption that appears on every delete trains people to dismiss it
|
|
51
|
+
* without reading — at which point it protects nothing and costs a click.
|
|
52
|
+
* `InlineConfirm` (the two-step button that expands in place) is the
|
|
53
|
+
* right default for a row-level action; this is for the ones where
|
|
54
|
+
* getting it wrong is expensive and hard to reverse.
|
|
55
|
+
*
|
|
56
|
+
* # Why the caller closes it
|
|
57
|
+
*
|
|
58
|
+
* `onConfirm` returning does not close the dialog, and `busy` is a prop
|
|
59
|
+
* rather than internal state. Both follow from the same requirement: the
|
|
60
|
+
* dialog must still be open, with its context intact, when a write fails.
|
|
61
|
+
* A self-closing confirm reports failure through a toast on a screen the
|
|
62
|
+
* operator has already been returned to, which is where error messages go
|
|
63
|
+
* to be missed.
|
|
64
|
+
*/
|
|
65
|
+
export function ConfirmDialog({
|
|
66
|
+
open,
|
|
67
|
+
onOpenChange,
|
|
68
|
+
title,
|
|
69
|
+
description,
|
|
70
|
+
confirmLabel,
|
|
71
|
+
cancelLabel = "Cancel",
|
|
72
|
+
tone = "default",
|
|
73
|
+
onConfirm,
|
|
74
|
+
busy = false,
|
|
75
|
+
children,
|
|
76
|
+
}: ConfirmDialogProps) {
|
|
77
|
+
return (
|
|
78
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
79
|
+
<DialogContent className="max-w-sm">
|
|
80
|
+
<DialogHeader>
|
|
81
|
+
<DialogTitle>{title}</DialogTitle>
|
|
82
|
+
<DialogDescription>{description}</DialogDescription>
|
|
83
|
+
</DialogHeader>
|
|
84
|
+
{children}
|
|
85
|
+
<DialogFooter>
|
|
86
|
+
<Button
|
|
87
|
+
type="button"
|
|
88
|
+
variant="ghost"
|
|
89
|
+
size="sm"
|
|
90
|
+
disabled={busy}
|
|
91
|
+
onClick={() => onOpenChange(false)}
|
|
92
|
+
>
|
|
93
|
+
{cancelLabel}
|
|
94
|
+
</Button>
|
|
95
|
+
<Button
|
|
96
|
+
type="button"
|
|
97
|
+
variant={tone === "destructive" ? "destructive" : "primary"}
|
|
98
|
+
size="sm"
|
|
99
|
+
disabled={busy}
|
|
100
|
+
onClick={onConfirm}
|
|
101
|
+
>
|
|
102
|
+
{busy && <Spinner size="xs" />}
|
|
103
|
+
{confirmLabel}
|
|
104
|
+
</Button>
|
|
105
|
+
</DialogFooter>
|
|
106
|
+
</DialogContent>
|
|
107
|
+
</Dialog>
|
|
108
|
+
);
|
|
109
|
+
}
|