harebear-ui 0.8.1 → 0.10.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/AGENTS.md +2 -2
- package/dist/components/admin/app-nav.d.ts +15 -1
- package/dist/components/admin/app-nav.js +28 -11
- package/dist/components/admin/avatar-upload.d.ts +32 -0
- package/dist/components/admin/avatar-upload.js +121 -0
- package/dist/components/admin/category.d.ts +17 -0
- package/dist/components/admin/category.js +35 -0
- package/dist/components/admin/celebration.d.ts +7 -0
- package/dist/components/admin/celebration.js +61 -0
- package/dist/components/admin/checklist-editor.d.ts +20 -0
- package/dist/components/admin/checklist-editor.js +69 -0
- package/dist/components/admin/color-swatch-picker.d.ts +15 -0
- package/dist/components/admin/color-swatch-picker.js +68 -0
- package/dist/components/admin/comment-thread.d.ts +26 -0
- package/dist/components/admin/comment-thread.js +68 -0
- package/dist/components/admin/copy-field.d.ts +3 -1
- package/dist/components/admin/copy-field.js +10 -5
- package/dist/components/admin/date-time-picker.d.ts +20 -0
- package/dist/components/admin/date-time-picker.js +97 -0
- package/dist/components/admin/detail-sheet.d.ts +18 -0
- package/dist/components/admin/detail-sheet.js +80 -0
- package/dist/components/admin/markdown-editor.d.ts +12 -0
- package/dist/components/admin/markdown-editor.js +46 -0
- package/dist/components/admin/page-header.d.ts +3 -1
- package/dist/components/admin/page-header.js +17 -9
- package/dist/components/admin/page.d.ts +16 -0
- package/dist/components/admin/page.js +42 -0
- package/dist/components/admin/prompt-dialog.d.ts +19 -0
- package/dist/components/admin/prompt-dialog.js +83 -0
- package/dist/components/admin/settings.d.ts +4 -2
- package/dist/components/admin/settings.js +37 -30
- package/dist/components/admin/time-zone-picker.d.ts +12 -0
- package/dist/components/admin/time-zone-picker.js +31 -0
- package/dist/components/admin/user-chip.d.ts +34 -3
- package/dist/components/admin/user-chip.js +66 -7
- package/dist/components/admin/user-menu.d.ts +6 -1
- package/dist/components/admin/user-menu.js +22 -10
- package/dist/components/ai/ask-box.d.ts +14 -2
- package/dist/components/ai/ask-box.js +90 -54
- package/dist/components/ai/assistant.d.ts +13 -3
- package/dist/components/ai/assistant.js +36 -8
- package/dist/components/ai/code-block.d.ts +10 -3
- package/dist/components/ai/code-block.js +57 -8
- package/dist/components/ai/conversation-list.d.ts +13 -3
- package/dist/components/ai/conversation-list.js +15 -26
- package/dist/components/ai/markdown.d.ts +13 -0
- package/dist/components/ai/markdown.js +41 -0
- package/dist/components/auth/auth.d.ts +41 -5
- package/dist/components/auth/auth.js +167 -159
- package/dist/components/data/agenda-row.d.ts +26 -0
- package/dist/components/data/agenda-row.js +62 -0
- package/dist/components/data/day-strip.d.ts +27 -0
- package/dist/components/data/day-strip.js +78 -0
- package/dist/components/data/sectioned-list.d.ts +21 -6
- package/dist/components/data/sectioned-list.js +44 -21
- package/dist/components/data/task-row.d.ts +3 -0
- package/dist/components/data/task-row.js +6 -14
- package/dist/components/layout/collapse.js +1 -1
- package/dist/components/layout/text.d.ts +3 -3
- package/dist/components/layout/text.js +6 -1
- package/dist/components/ui/avatar.js +1 -1
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/check-circle.js +1 -1
- package/dist/components/ui/code-block.js +2 -2
- package/dist/components/ui/combobox.js +1 -0
- package/dist/components/ui/input-group.js +1 -1
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/kbd.js +1 -1
- package/dist/components/ui/password-input.d.ts +8 -0
- package/dist/components/ui/password-input.js +32 -0
- package/dist/components/ui/sonner.d.ts +2 -2
- package/dist/components/ui/sonner.js +1 -1
- package/dist/components/ui/toggle.js +1 -1
- package/dist/index.d.ts +23 -7
- package/dist/index.js +21 -5
- package/dist/lib/internal/date.js +8 -0
- package/dist/lib/{toast-clearance.js → internal/toast-clearance.js} +1 -1
- package/dist/styles.css +31 -1
- package/docs/COMPONENTS.md +302 -28
- package/docs/ROADMAP.md +13 -11
- package/llms.txt +10 -8
- package/package.json +3 -1
- package/skills/harebear-ui/SKILL.md +5 -3
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { MemberAvatar } from "./user-chip.js";
|
|
3
|
+
import { Markdown } from "../ai/markdown.js";
|
|
4
|
+
import { Button } from "../ui/button.js";
|
|
5
|
+
import { Textarea } from "../ui/textarea.js";
|
|
6
|
+
import { cn } from "../../lib/utils.js";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
// src/components/admin/comment-thread.tsx
|
|
10
|
+
/** Comments on a record, oldest first, with a box to add one. Bodies are Markdown. */
|
|
11
|
+
function CommentThread({ comments, onSubmit, placeholder = "Add a comment", submitLabel = "Comment", className, ...props }) {
|
|
12
|
+
const [text, setText] = React.useState("");
|
|
13
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
14
|
+
"data-slot": "comment-thread",
|
|
15
|
+
className: cn("flex flex-col gap-4", className),
|
|
16
|
+
...props,
|
|
17
|
+
children: [comments.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
18
|
+
className: "flex flex-col gap-4",
|
|
19
|
+
children: comments.map((c) => /* @__PURE__ */ jsxs("li", {
|
|
20
|
+
className: "flex min-w-0 items-start gap-3",
|
|
21
|
+
children: [/* @__PURE__ */ jsx(MemberAvatar, {
|
|
22
|
+
size: "sm",
|
|
23
|
+
name: c.author.name,
|
|
24
|
+
avatarUrl: c.author.avatarUrl,
|
|
25
|
+
color: c.author.color,
|
|
26
|
+
"aria-hidden": true
|
|
27
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
28
|
+
className: "flex min-w-0 flex-1 flex-col gap-0.5",
|
|
29
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
30
|
+
className: "text-sm",
|
|
31
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
32
|
+
className: "font-medium",
|
|
33
|
+
children: c.author.name
|
|
34
|
+
}), c.meta && /* @__PURE__ */ jsxs("span", {
|
|
35
|
+
className: "text-xs text-muted-foreground",
|
|
36
|
+
children: [" · ", c.meta]
|
|
37
|
+
})]
|
|
38
|
+
}), /* @__PURE__ */ jsx(Markdown, {
|
|
39
|
+
className: "gap-2 text-sm leading-6",
|
|
40
|
+
children: c.body
|
|
41
|
+
})]
|
|
42
|
+
})]
|
|
43
|
+
}, c.id))
|
|
44
|
+
}), /* @__PURE__ */ jsxs("form", {
|
|
45
|
+
className: "flex flex-col gap-2",
|
|
46
|
+
onSubmit: (e) => {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
const value = text.trim();
|
|
49
|
+
if (!value) return;
|
|
50
|
+
onSubmit(value);
|
|
51
|
+
setText("");
|
|
52
|
+
},
|
|
53
|
+
children: [/* @__PURE__ */ jsx(Textarea, {
|
|
54
|
+
value: text,
|
|
55
|
+
onChange: (e) => setText(e.target.value),
|
|
56
|
+
placeholder,
|
|
57
|
+
"aria-label": placeholder
|
|
58
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
59
|
+
type: "submit",
|
|
60
|
+
variant: "outline",
|
|
61
|
+
className: "self-end pointer-coarse:h-11",
|
|
62
|
+
disabled: !text.trim(),
|
|
63
|
+
children: submitLabel
|
|
64
|
+
})]
|
|
65
|
+
})]
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export { CommentThread };
|
|
@@ -11,7 +11,9 @@ export interface CopyFieldProps extends Omit<React.ComponentProps<typeof InputGr
|
|
|
11
11
|
"aria-label"?: string;
|
|
12
12
|
/** Monospace text. Default true. */
|
|
13
13
|
mono?: boolean;
|
|
14
|
+
/** Shown next to the button, and its accessible name, for a moment after copying. Default "Copied". */
|
|
15
|
+
copiedLabel?: string;
|
|
14
16
|
}
|
|
15
17
|
/** Read-only value with a copy button: IDs, paths, tokens' public parts, URLs. */
|
|
16
|
-
declare function CopyField({ value, display, label, mono, "aria-label": name, className, ...props }: CopyFieldProps): React.JSX.Element;
|
|
18
|
+
declare function CopyField({ value, display, label, mono, copiedLabel, "aria-label": name, className, ...props }: CopyFieldProps): React.JSX.Element;
|
|
17
19
|
export { CopyField };
|
|
@@ -6,7 +6,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
7
7
|
// src/components/admin/copy-field.tsx
|
|
8
8
|
/** Read-only value with a copy button: IDs, paths, tokens' public parts, URLs. */
|
|
9
|
-
function CopyField({ value, display, label, mono = true, "aria-label": name = "Value", className, ...props }) {
|
|
9
|
+
function CopyField({ value, display, label, mono = true, copiedLabel = "Copied", "aria-label": name = "Value", className, ...props }) {
|
|
10
10
|
const { copied, copy } = useCopyToClipboard();
|
|
11
11
|
return /* @__PURE__ */ jsxs(InputGroup, {
|
|
12
12
|
"data-slot": "copy-field",
|
|
@@ -22,14 +22,19 @@ function CopyField({ value, display, label, mono = true, "aria-label": name = "V
|
|
|
22
22
|
className: cn(mono && "font-mono"),
|
|
23
23
|
onFocus: (e) => e.currentTarget.select()
|
|
24
24
|
}),
|
|
25
|
-
/* @__PURE__ */
|
|
25
|
+
/* @__PURE__ */ jsxs(InputGroupAddon, {
|
|
26
26
|
align: "inline-end",
|
|
27
|
-
children: /* @__PURE__ */ jsx(
|
|
27
|
+
children: [/* @__PURE__ */ jsx(InputGroupText, {
|
|
28
|
+
"data-slot": "copy-field-status",
|
|
29
|
+
"aria-hidden": "true",
|
|
30
|
+
className: "text-xs empty:hidden",
|
|
31
|
+
children: copied ? copiedLabel : null
|
|
32
|
+
}), /* @__PURE__ */ jsx(InputGroupButton, {
|
|
28
33
|
size: "icon-xs",
|
|
29
|
-
"aria-label": `Copy ${name.toLowerCase()}`,
|
|
34
|
+
"aria-label": copied ? copiedLabel : `Copy ${name.toLowerCase()}`,
|
|
30
35
|
onClick: () => copy(value),
|
|
31
36
|
children: copied ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx(CopyIcon, {})
|
|
32
|
-
})
|
|
37
|
+
})]
|
|
33
38
|
})
|
|
34
39
|
]
|
|
35
40
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** A floating local date and optional time, e.g. `{ date: "2026-11-12", time: "09:30" }`. No time zone. */
|
|
3
|
+
export interface DateTimeValue {
|
|
4
|
+
/** `YYYY-MM-DD` */
|
|
5
|
+
date: string;
|
|
6
|
+
/** `HH:MM`, 24-hour. */
|
|
7
|
+
time?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface DateTimePickerProps extends Omit<React.ComponentProps<"button">, "value" | "onChange"> {
|
|
10
|
+
value: DateTimeValue | null;
|
|
11
|
+
/** Called with the new value, or null when cleared. */
|
|
12
|
+
onValueChange: (value: DateTimeValue | null) => void;
|
|
13
|
+
/** Show a time field under the calendar. */
|
|
14
|
+
withTime?: boolean;
|
|
15
|
+
/** Trigger text when there is no value. Default "Pick a date". */
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Date field: a button that opens a Calendar in a Popover, with an optional time field and Clear. */
|
|
19
|
+
declare function DateTimePicker({ value, onValueChange, withTime, placeholder, className, ...props }: DateTimePickerProps): React.JSX.Element;
|
|
20
|
+
export { DateTimePicker };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Button } from "../ui/button.js";
|
|
3
|
+
import { Calendar } from "../ui/calendar.js";
|
|
4
|
+
import { Input } from "../ui/input.js";
|
|
5
|
+
import { Label } from "../ui/label.js";
|
|
6
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
|
|
7
|
+
import { parseLocalDate } from "../../lib/internal/date.js";
|
|
8
|
+
import { cn } from "../../lib/utils.js";
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { CalendarIcon } from "lucide-react";
|
|
12
|
+
// src/components/admin/date-time-picker.tsx
|
|
13
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
14
|
+
const toISODate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
15
|
+
function formatValue({ date, time }) {
|
|
16
|
+
const at = parseLocalDate(date, time);
|
|
17
|
+
const day = at.toLocaleDateString(void 0, {
|
|
18
|
+
weekday: "short",
|
|
19
|
+
day: "numeric",
|
|
20
|
+
month: "short",
|
|
21
|
+
year: "numeric"
|
|
22
|
+
});
|
|
23
|
+
return time ? `${day}, ${at.toLocaleTimeString(void 0, {
|
|
24
|
+
hour: "numeric",
|
|
25
|
+
minute: "2-digit"
|
|
26
|
+
})}` : day;
|
|
27
|
+
}
|
|
28
|
+
/** Date field: a button that opens a Calendar in a Popover, with an optional time field and Clear. */
|
|
29
|
+
function DateTimePicker({ value, onValueChange, withTime, placeholder = "Pick a date", className, ...props }) {
|
|
30
|
+
const [open, setOpen] = React.useState(false);
|
|
31
|
+
const timeId = React.useId();
|
|
32
|
+
const selected = value ? parseLocalDate(value.date) : void 0;
|
|
33
|
+
return /* @__PURE__ */ jsxs(Popover, {
|
|
34
|
+
open,
|
|
35
|
+
onOpenChange: setOpen,
|
|
36
|
+
children: [/* @__PURE__ */ jsx(PopoverTrigger, {
|
|
37
|
+
asChild: true,
|
|
38
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
39
|
+
type: "button",
|
|
40
|
+
"data-slot": "date-time-picker",
|
|
41
|
+
"data-empty": value ? void 0 : "",
|
|
42
|
+
className: cn("flex h-10 w-full min-w-0 items-center gap-2.5 rounded-md border border-transparent bg-muted px-3.5 text-left text-base transition-[border-color,box-shadow,background-color] outline-none focus-visible:border-primary focus-visible:bg-card focus-visible:ring-4 focus-visible:ring-primary/15 disabled:pointer-events-none disabled:opacity-50 data-empty:text-muted-foreground md:text-sm [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-muted-foreground", className),
|
|
43
|
+
...props,
|
|
44
|
+
children: [/* @__PURE__ */ jsx(CalendarIcon, { "aria-hidden": "true" }), /* @__PURE__ */ jsx("span", {
|
|
45
|
+
className: "truncate",
|
|
46
|
+
children: value ? formatValue(value) : placeholder
|
|
47
|
+
})]
|
|
48
|
+
})
|
|
49
|
+
}), /* @__PURE__ */ jsxs(PopoverContent, {
|
|
50
|
+
align: "start",
|
|
51
|
+
className: "w-auto gap-3 p-3",
|
|
52
|
+
children: [
|
|
53
|
+
/* @__PURE__ */ jsx(Calendar, {
|
|
54
|
+
mode: "single",
|
|
55
|
+
required: true,
|
|
56
|
+
selected,
|
|
57
|
+
defaultMonth: selected,
|
|
58
|
+
onSelect: (d) => {
|
|
59
|
+
onValueChange({
|
|
60
|
+
date: toISODate(d),
|
|
61
|
+
...value?.time ? { time: value.time } : {}
|
|
62
|
+
});
|
|
63
|
+
if (!withTime) setOpen(false);
|
|
64
|
+
},
|
|
65
|
+
className: "p-0 [--cell-size:--spacing(11)] md:[--cell-size:--spacing(9)]"
|
|
66
|
+
}),
|
|
67
|
+
withTime && /* @__PURE__ */ jsxs("div", {
|
|
68
|
+
className: "flex items-center gap-3",
|
|
69
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
70
|
+
htmlFor: timeId,
|
|
71
|
+
className: "shrink-0",
|
|
72
|
+
children: "Time"
|
|
73
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
74
|
+
id: timeId,
|
|
75
|
+
type: "time",
|
|
76
|
+
disabled: !value,
|
|
77
|
+
value: value?.time ?? "",
|
|
78
|
+
onChange: (e) => value && onValueChange({
|
|
79
|
+
date: value.date,
|
|
80
|
+
...e.target.value ? { time: e.target.value } : {}
|
|
81
|
+
})
|
|
82
|
+
})]
|
|
83
|
+
}),
|
|
84
|
+
value && /* @__PURE__ */ jsx(Button, {
|
|
85
|
+
variant: "ghost",
|
|
86
|
+
className: "max-md:h-11",
|
|
87
|
+
onClick: () => {
|
|
88
|
+
onValueChange(null);
|
|
89
|
+
setOpen(false);
|
|
90
|
+
},
|
|
91
|
+
children: "Clear"
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
})]
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export { DateTimePicker };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface DetailSheetProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
/** Heading and accessible name, e.g. "To-do". */
|
|
6
|
+
title: React.ReactNode;
|
|
7
|
+
description?: React.ReactNode;
|
|
8
|
+
/** Pinned under the scrolling body, e.g. a Delete button. */
|
|
9
|
+
footer?: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A record's details: a floating side sheet from 768px up, a full-screen drawer on phones.
|
|
15
|
+
* Both have a close button and a scrolling body.
|
|
16
|
+
*/
|
|
17
|
+
declare function DetailSheet({ open, onOpenChange, title, description, footer, className, children }: DetailSheetProps): React.JSX.Element;
|
|
18
|
+
export { DetailSheet };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Button } from "../ui/button.js";
|
|
3
|
+
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerTitle } from "../ui/drawer.js";
|
|
4
|
+
import { Sheet, SheetContent, SheetDescription, SheetTitle } from "../ui/sheet.js";
|
|
5
|
+
import { useMediaQuery } from "../../hooks/use-media-query.js";
|
|
6
|
+
import { cn } from "../../lib/utils.js";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { XIcon } from "lucide-react";
|
|
9
|
+
// src/components/admin/detail-sheet.tsx
|
|
10
|
+
/**
|
|
11
|
+
* A record's details: a floating side sheet from 768px up, a full-screen drawer on phones.
|
|
12
|
+
* Both have a close button and a scrolling body.
|
|
13
|
+
*/
|
|
14
|
+
function DetailSheet({ open, onOpenChange, title, description, footer, className, children }) {
|
|
15
|
+
const desktop = useMediaQuery("(min-width: 768px)");
|
|
16
|
+
const noDescription = description ? {} : { "aria-describedby": void 0 };
|
|
17
|
+
const body = (padBottom) => /* @__PURE__ */ jsx("div", {
|
|
18
|
+
className: cn("min-h-0 flex-1 overflow-y-auto px-6 pb-6", !footer && padBottom),
|
|
19
|
+
children
|
|
20
|
+
});
|
|
21
|
+
const foot = (padBottom) => footer && /* @__PURE__ */ jsx("div", {
|
|
22
|
+
className: cn("flex flex-wrap items-center gap-2 border-t px-6 py-4", padBottom),
|
|
23
|
+
children: footer
|
|
24
|
+
});
|
|
25
|
+
if (desktop) return /* @__PURE__ */ jsx(Sheet, {
|
|
26
|
+
open,
|
|
27
|
+
onOpenChange,
|
|
28
|
+
children: /* @__PURE__ */ jsxs(SheetContent, {
|
|
29
|
+
"data-slot": "detail-sheet",
|
|
30
|
+
"data-layout": "sheet",
|
|
31
|
+
...noDescription,
|
|
32
|
+
className: cn("gap-0 data-[side=right]:inset-y-4 data-[side=right]:right-4 data-[side=right]:h-auto data-[side=right]:w-[min(420px,calc(100%-32px))] data-[side=right]:rounded-xl data-[side=right]:border data-[side=right]:sm:max-w-none", className),
|
|
33
|
+
children: [
|
|
34
|
+
/* @__PURE__ */ jsxs("div", {
|
|
35
|
+
className: "flex flex-col gap-1 p-6 pr-14",
|
|
36
|
+
children: [/* @__PURE__ */ jsx(SheetTitle, {
|
|
37
|
+
className: "text-lg font-semibold",
|
|
38
|
+
children: title
|
|
39
|
+
}), description && /* @__PURE__ */ jsx(SheetDescription, { children: description })]
|
|
40
|
+
}),
|
|
41
|
+
body(),
|
|
42
|
+
foot()
|
|
43
|
+
]
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
return /* @__PURE__ */ jsx(Drawer, {
|
|
47
|
+
open,
|
|
48
|
+
onOpenChange,
|
|
49
|
+
children: /* @__PURE__ */ jsxs(DrawerContent, {
|
|
50
|
+
"data-slot": "detail-sheet",
|
|
51
|
+
"data-layout": "drawer",
|
|
52
|
+
...noDescription,
|
|
53
|
+
className: cn("h-dvh data-[vaul-drawer-direction=bottom]:mt-0 data-[vaul-drawer-direction=bottom]:max-h-none data-[vaul-drawer-direction=bottom]:rounded-none data-[vaul-drawer-direction=bottom]:border-t-0", className),
|
|
54
|
+
children: [
|
|
55
|
+
/* @__PURE__ */ jsxs("div", {
|
|
56
|
+
className: "flex items-start gap-2 px-6 pt-[max(0.5rem,env(safe-area-inset-top))] pb-4",
|
|
57
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
58
|
+
className: "flex min-w-0 flex-1 flex-col gap-1 pt-2.5",
|
|
59
|
+
children: [/* @__PURE__ */ jsx(DrawerTitle, {
|
|
60
|
+
className: "text-lg font-semibold",
|
|
61
|
+
children: title
|
|
62
|
+
}), description && /* @__PURE__ */ jsx(DrawerDescription, { children: description })]
|
|
63
|
+
}), /* @__PURE__ */ jsx(DrawerClose, {
|
|
64
|
+
asChild: true,
|
|
65
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
66
|
+
variant: "ghost",
|
|
67
|
+
size: "icon-lg",
|
|
68
|
+
"aria-label": "Close",
|
|
69
|
+
className: "-mr-2.5",
|
|
70
|
+
children: /* @__PURE__ */ jsx(XIcon, {})
|
|
71
|
+
})
|
|
72
|
+
})]
|
|
73
|
+
}),
|
|
74
|
+
body("pb-[max(1.5rem,env(safe-area-inset-bottom))]"),
|
|
75
|
+
foot("pb-[max(1rem,env(safe-area-inset-bottom))]")
|
|
76
|
+
]
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export { DetailSheet };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface MarkdownEditorProps extends Omit<React.ComponentProps<"textarea">, "value" | "defaultValue" | "onChange"> {
|
|
3
|
+
value: string;
|
|
4
|
+
onValueChange: (value: string) => void;
|
|
5
|
+
/** Custom Preview renderer; the result is wrapped in Prose. Default: the library's `Markdown`. */
|
|
6
|
+
renderPreview?: (markdown: string) => React.ReactNode;
|
|
7
|
+
/** Classes for the wrapping Tabs. `className` goes to the textarea. */
|
|
8
|
+
wrapperClassName?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Markdown field with Write and Preview tabs. Other props go to the Textarea (id, placeholder, aria-*). */
|
|
11
|
+
declare function MarkdownEditor({ value, onValueChange, renderPreview, className, wrapperClassName, ...props }: MarkdownEditorProps): React.JSX.Element;
|
|
12
|
+
export { MarkdownEditor };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Prose } from "../ai/code-block.js";
|
|
3
|
+
import { Markdown } from "../ai/markdown.js";
|
|
4
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs.js";
|
|
5
|
+
import { Textarea } from "../ui/textarea.js";
|
|
6
|
+
import { cn } from "../../lib/utils.js";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
// src/components/admin/markdown-editor.tsx
|
|
9
|
+
/** Markdown field with Write and Preview tabs. Other props go to the Textarea (id, placeholder, aria-*). */
|
|
10
|
+
function MarkdownEditor({ value, onValueChange, renderPreview, className, wrapperClassName, ...props }) {
|
|
11
|
+
return /* @__PURE__ */ jsxs(Tabs, {
|
|
12
|
+
"data-slot": "markdown-editor",
|
|
13
|
+
defaultValue: "write",
|
|
14
|
+
className: cn("gap-3", wrapperClassName),
|
|
15
|
+
children: [
|
|
16
|
+
/* @__PURE__ */ jsxs(TabsList, {
|
|
17
|
+
className: "pointer-coarse:group-data-horizontal/tabs:h-13",
|
|
18
|
+
children: [/* @__PURE__ */ jsx(TabsTrigger, {
|
|
19
|
+
value: "write",
|
|
20
|
+
children: "Write"
|
|
21
|
+
}), /* @__PURE__ */ jsx(TabsTrigger, {
|
|
22
|
+
value: "preview",
|
|
23
|
+
children: "Preview"
|
|
24
|
+
})]
|
|
25
|
+
}),
|
|
26
|
+
/* @__PURE__ */ jsx(TabsContent, {
|
|
27
|
+
value: "write",
|
|
28
|
+
children: /* @__PURE__ */ jsx(Textarea, {
|
|
29
|
+
value,
|
|
30
|
+
onChange: (e) => onValueChange(e.target.value),
|
|
31
|
+
className: cn("min-h-48", className),
|
|
32
|
+
...props
|
|
33
|
+
})
|
|
34
|
+
}),
|
|
35
|
+
/* @__PURE__ */ jsx(TabsContent, {
|
|
36
|
+
value: "preview",
|
|
37
|
+
className: "min-h-48 rounded-md border px-3.5 py-3",
|
|
38
|
+
children: !value.trim() ? /* @__PURE__ */ jsx("p", {
|
|
39
|
+
className: "text-sm text-muted-foreground",
|
|
40
|
+
children: "Nothing to preview."
|
|
41
|
+
}) : renderPreview ? /* @__PURE__ */ jsx(Prose, { children: renderPreview(value) }) : /* @__PURE__ */ jsx(Markdown, { children: value })
|
|
42
|
+
})
|
|
43
|
+
]
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export { MarkdownEditor };
|
|
@@ -8,7 +8,9 @@ export interface PageHeaderProps extends Omit<React.ComponentProps<"section">, "
|
|
|
8
8
|
actions?: React.ReactNode;
|
|
9
9
|
/** Text above the title, e.g. a date. */
|
|
10
10
|
eyebrow?: React.ReactNode;
|
|
11
|
+
/** Leading mark before the title, e.g. a CategoryDot, an icon tile or a MemberAvatar. */
|
|
12
|
+
icon?: React.ReactNode;
|
|
11
13
|
}
|
|
12
14
|
/** Page title row with description and actions. Wraps on narrow screens. */
|
|
13
|
-
declare function PageHeader({ title, description, count, actions, eyebrow, className, ...props }: PageHeaderProps): React.JSX.Element;
|
|
15
|
+
declare function PageHeader({ title, description, count, actions, eyebrow, icon, className, ...props }: PageHeaderProps): React.JSX.Element;
|
|
14
16
|
export { PageHeader };
|
|
@@ -4,7 +4,7 @@ import { cn } from "../../lib/utils.js";
|
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
// src/components/admin/page-header.tsx
|
|
6
6
|
/** Page title row with description and actions. Wraps on narrow screens. */
|
|
7
|
-
function PageHeader({ title, description, count, actions, eyebrow, className, ...props }) {
|
|
7
|
+
function PageHeader({ title, description, count, actions, eyebrow, icon, className, ...props }) {
|
|
8
8
|
return /* @__PURE__ */ jsxs("section", {
|
|
9
9
|
"data-slot": "page-header",
|
|
10
10
|
className: cn("flex flex-wrap items-end justify-between gap-4", className),
|
|
@@ -18,14 +18,22 @@ function PageHeader({ title, description, count, actions, eyebrow, className, ..
|
|
|
18
18
|
}),
|
|
19
19
|
/* @__PURE__ */ jsxs("div", {
|
|
20
20
|
className: "flex items-center gap-3",
|
|
21
|
-
children: [
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
children: [
|
|
22
|
+
icon && /* @__PURE__ */ jsx("span", {
|
|
23
|
+
"data-slot": "page-header-icon",
|
|
24
|
+
className: "flex shrink-0 items-center [&_svg:not([class*='size-'])]:size-6",
|
|
25
|
+
children: icon
|
|
26
|
+
}),
|
|
27
|
+
/* @__PURE__ */ jsx("h1", {
|
|
28
|
+
className: "text-[28px] leading-tight font-semibold tracking-[-0.02em]",
|
|
29
|
+
children: title
|
|
30
|
+
}),
|
|
31
|
+
count !== void 0 && /* @__PURE__ */ jsx(Badge, {
|
|
32
|
+
variant: "secondary",
|
|
33
|
+
className: "tabular-nums",
|
|
34
|
+
children: count
|
|
35
|
+
})
|
|
36
|
+
]
|
|
29
37
|
}),
|
|
30
38
|
description && /* @__PURE__ */ jsx("p", {
|
|
31
39
|
className: "text-muted-foreground",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface PageProps extends React.ComponentProps<"div"> {
|
|
3
|
+
/** Column width. Default "md" (42rem), like FocusLayout. */
|
|
4
|
+
width?: "sm" | "md" | "lg";
|
|
5
|
+
}
|
|
6
|
+
/** The page column inside AppNav or SidebarLayout: centred, 16px gutters on phones, children spaced like sections. */
|
|
7
|
+
declare function Page({ width, className, ...props }: PageProps): React.JSX.Element;
|
|
8
|
+
export interface PageSectionProps extends Omit<React.ComponentProps<"section">, "title"> {
|
|
9
|
+
/** The section's h2, which also names the region. */
|
|
10
|
+
title: React.ReactNode;
|
|
11
|
+
/** At the right of the heading, e.g. a small ghost Button "See all". */
|
|
12
|
+
action?: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/** A titled part of a page: a region named by its h2 (under the PageHeader h1), an optional action, then children. */
|
|
15
|
+
declare function PageSection({ title, action, className, children, ...props }: PageSectionProps): React.JSX.Element;
|
|
16
|
+
export { Page, PageSection };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Heading } from "../layout/text.js";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
// src/components/admin/page.tsx
|
|
7
|
+
const widthClass = {
|
|
8
|
+
sm: "max-w-xl",
|
|
9
|
+
md: "max-w-2xl",
|
|
10
|
+
lg: "max-w-4xl"
|
|
11
|
+
};
|
|
12
|
+
/** The page column inside AppNav or SidebarLayout: centred, 16px gutters on phones, children spaced like sections. */
|
|
13
|
+
function Page({ width = "md", className, ...props }) {
|
|
14
|
+
return /* @__PURE__ */ jsx("div", {
|
|
15
|
+
"data-slot": "page",
|
|
16
|
+
className: cn("mx-auto flex w-full flex-col gap-7 px-4 py-6 md:px-8 md:py-10", widthClass[width], className),
|
|
17
|
+
...props
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** A titled part of a page: a region named by its h2 (under the PageHeader h1), an optional action, then children. */
|
|
21
|
+
function PageSection({ title, action, className, children, ...props }) {
|
|
22
|
+
const id = React.useId();
|
|
23
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
24
|
+
"data-slot": "page-section",
|
|
25
|
+
"aria-labelledby": id,
|
|
26
|
+
className: cn("flex min-w-0 flex-col gap-3", className),
|
|
27
|
+
...props,
|
|
28
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
29
|
+
className: "flex min-w-0 items-center gap-2",
|
|
30
|
+
children: [/* @__PURE__ */ jsx(Heading, {
|
|
31
|
+
level: 2,
|
|
32
|
+
id,
|
|
33
|
+
className: "min-w-0 flex-1",
|
|
34
|
+
children: title
|
|
35
|
+
}), action && /* @__PURE__ */ jsx("div", {
|
|
36
|
+
className: "flex shrink-0 items-center pointer-coarse:[&>*]:min-h-11",
|
|
37
|
+
children: action
|
|
38
|
+
})]
|
|
39
|
+
}), children]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export { Page, PageSection };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface PromptDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
title: React.ReactNode;
|
|
6
|
+
description?: React.ReactNode;
|
|
7
|
+
/** Label of the text field. */
|
|
8
|
+
label: React.ReactNode;
|
|
9
|
+
/** Starting text, e.g. the current name when renaming. */
|
|
10
|
+
defaultValue?: string;
|
|
11
|
+
submitLabel?: React.ReactNode;
|
|
12
|
+
cancelLabel?: React.ReactNode;
|
|
13
|
+
maxLength?: number;
|
|
14
|
+
/** Gets the trimmed text. The dialog closes when it returns or resolves; a thrown error's message shows under the field. */
|
|
15
|
+
onSubmit: (value: string) => void | Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/** Controlled dialog that asks for one line of text: New list, Rename, Add section. Sibling of ConfirmDialog. */
|
|
18
|
+
declare function PromptDialog({ open, onOpenChange, ...props }: PromptDialogProps): React.JSX.Element;
|
|
19
|
+
export { PromptDialog };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Button } from "../ui/button.js";
|
|
3
|
+
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog.js";
|
|
4
|
+
import { Field, FieldError, FieldLabel } from "../ui/field.js";
|
|
5
|
+
import { Input } from "../ui/input.js";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
// src/components/admin/prompt-dialog.tsx
|
|
9
|
+
/** Controlled dialog that asks for one line of text: New list, Rename, Add section. Sibling of ConfirmDialog. */
|
|
10
|
+
function PromptDialog({ open, onOpenChange, ...props }) {
|
|
11
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
12
|
+
open,
|
|
13
|
+
onOpenChange,
|
|
14
|
+
children: /* @__PURE__ */ jsx(DialogContent, {
|
|
15
|
+
"data-slot": "prompt-dialog",
|
|
16
|
+
children: /* @__PURE__ */ jsx(PromptForm, {
|
|
17
|
+
onDone: () => onOpenChange(false),
|
|
18
|
+
...props
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function PromptForm({ title, description, label, defaultValue = "", submitLabel = "Save", cancelLabel = "Cancel", maxLength, onSubmit, onDone }) {
|
|
24
|
+
const id = React.useId();
|
|
25
|
+
const [value, setValue] = React.useState(defaultValue);
|
|
26
|
+
const [error, setError] = React.useState(null);
|
|
27
|
+
const [busy, setBusy] = React.useState(false);
|
|
28
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
29
|
+
className: "grid gap-4",
|
|
30
|
+
onSubmit: async (e) => {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
setBusy(true);
|
|
33
|
+
setError(null);
|
|
34
|
+
try {
|
|
35
|
+
await onSubmit(value.trim());
|
|
36
|
+
onDone();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
39
|
+
setBusy(false);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
children: [
|
|
43
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogTitle, { children: title }), description && /* @__PURE__ */ jsx(DialogDescription, { children: description })] }),
|
|
44
|
+
/* @__PURE__ */ jsxs(Field, {
|
|
45
|
+
"data-invalid": error ? true : void 0,
|
|
46
|
+
children: [
|
|
47
|
+
/* @__PURE__ */ jsx(FieldLabel, {
|
|
48
|
+
htmlFor: id,
|
|
49
|
+
children: label
|
|
50
|
+
}),
|
|
51
|
+
/* @__PURE__ */ jsx(Input, {
|
|
52
|
+
id,
|
|
53
|
+
value,
|
|
54
|
+
onChange: (e) => setValue(e.target.value),
|
|
55
|
+
maxLength,
|
|
56
|
+
autoComplete: "off",
|
|
57
|
+
required: true,
|
|
58
|
+
"aria-invalid": error ? true : void 0,
|
|
59
|
+
"aria-describedby": error ? `${id}-error` : void 0
|
|
60
|
+
}),
|
|
61
|
+
error && /* @__PURE__ */ jsx(FieldError, {
|
|
62
|
+
id: `${id}-error`,
|
|
63
|
+
children: error
|
|
64
|
+
})
|
|
65
|
+
]
|
|
66
|
+
}),
|
|
67
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(DialogClose, {
|
|
68
|
+
asChild: true,
|
|
69
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
70
|
+
type: "button",
|
|
71
|
+
variant: "outline",
|
|
72
|
+
children: cancelLabel
|
|
73
|
+
})
|
|
74
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
75
|
+
type: "submit",
|
|
76
|
+
loading: busy,
|
|
77
|
+
disabled: !value.trim(),
|
|
78
|
+
children: submitLabel
|
|
79
|
+
})] })
|
|
80
|
+
]
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export { PromptDialog };
|
|
@@ -15,6 +15,8 @@ export interface SettingsRowProps extends Omit<React.ComponentProps<"div">, "con
|
|
|
15
15
|
htmlFor?: string;
|
|
16
16
|
/** The control at the right: Switch, Select, ToggleGroup, Button. */
|
|
17
17
|
control?: React.ReactNode;
|
|
18
|
+
/** Always shown under the label, full width, with no toggle (a list of connections, a hint, a short form). */
|
|
19
|
+
body?: React.ReactNode;
|
|
18
20
|
/**
|
|
19
21
|
* Inline content that expands under the row (change password, add a connection). Without a `control`, the row
|
|
20
22
|
* gets a toggle button (`expandLabel` / "Cancel").
|
|
@@ -26,6 +28,6 @@ export interface SettingsRowProps extends Omit<React.ComponentProps<"div">, "con
|
|
|
26
28
|
/** Label of the default toggle button. Default "Change". */
|
|
27
29
|
expandLabel?: React.ReactNode;
|
|
28
30
|
}
|
|
29
|
-
/** One setting: label and description at the left, a control at the right, optional expandable content. */
|
|
30
|
-
declare function SettingsRow({ label, description, htmlFor, control, content, open: openProp, defaultOpen, onOpenChange, expandLabel, className, ...props }: SettingsRowProps): React.JSX.Element;
|
|
31
|
+
/** One setting: label and description at the left, a control at the right, optional body and expandable content. */
|
|
32
|
+
declare function SettingsRow({ label, description, htmlFor, control, body, content, open: openProp, defaultOpen, onOpenChange, expandLabel, className, ...props }: SettingsRowProps): React.JSX.Element;
|
|
31
33
|
export { SettingsRow, SettingsSection };
|