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.
Files changed (83) hide show
  1. package/AGENTS.md +2 -2
  2. package/dist/components/admin/app-nav.d.ts +15 -1
  3. package/dist/components/admin/app-nav.js +28 -11
  4. package/dist/components/admin/avatar-upload.d.ts +32 -0
  5. package/dist/components/admin/avatar-upload.js +121 -0
  6. package/dist/components/admin/category.d.ts +17 -0
  7. package/dist/components/admin/category.js +35 -0
  8. package/dist/components/admin/celebration.d.ts +7 -0
  9. package/dist/components/admin/celebration.js +61 -0
  10. package/dist/components/admin/checklist-editor.d.ts +20 -0
  11. package/dist/components/admin/checklist-editor.js +69 -0
  12. package/dist/components/admin/color-swatch-picker.d.ts +15 -0
  13. package/dist/components/admin/color-swatch-picker.js +68 -0
  14. package/dist/components/admin/comment-thread.d.ts +26 -0
  15. package/dist/components/admin/comment-thread.js +68 -0
  16. package/dist/components/admin/copy-field.d.ts +3 -1
  17. package/dist/components/admin/copy-field.js +10 -5
  18. package/dist/components/admin/date-time-picker.d.ts +20 -0
  19. package/dist/components/admin/date-time-picker.js +97 -0
  20. package/dist/components/admin/detail-sheet.d.ts +18 -0
  21. package/dist/components/admin/detail-sheet.js +80 -0
  22. package/dist/components/admin/markdown-editor.d.ts +12 -0
  23. package/dist/components/admin/markdown-editor.js +46 -0
  24. package/dist/components/admin/page-header.d.ts +3 -1
  25. package/dist/components/admin/page-header.js +17 -9
  26. package/dist/components/admin/page.d.ts +16 -0
  27. package/dist/components/admin/page.js +42 -0
  28. package/dist/components/admin/prompt-dialog.d.ts +19 -0
  29. package/dist/components/admin/prompt-dialog.js +83 -0
  30. package/dist/components/admin/settings.d.ts +4 -2
  31. package/dist/components/admin/settings.js +37 -30
  32. package/dist/components/admin/time-zone-picker.d.ts +12 -0
  33. package/dist/components/admin/time-zone-picker.js +31 -0
  34. package/dist/components/admin/user-chip.d.ts +34 -3
  35. package/dist/components/admin/user-chip.js +66 -7
  36. package/dist/components/admin/user-menu.d.ts +6 -1
  37. package/dist/components/admin/user-menu.js +22 -10
  38. package/dist/components/ai/ask-box.d.ts +14 -2
  39. package/dist/components/ai/ask-box.js +90 -54
  40. package/dist/components/ai/assistant.d.ts +13 -3
  41. package/dist/components/ai/assistant.js +36 -8
  42. package/dist/components/ai/code-block.d.ts +10 -3
  43. package/dist/components/ai/code-block.js +57 -8
  44. package/dist/components/ai/conversation-list.d.ts +13 -3
  45. package/dist/components/ai/conversation-list.js +15 -26
  46. package/dist/components/ai/markdown.d.ts +13 -0
  47. package/dist/components/ai/markdown.js +41 -0
  48. package/dist/components/auth/auth.d.ts +41 -5
  49. package/dist/components/auth/auth.js +167 -159
  50. package/dist/components/data/agenda-row.d.ts +26 -0
  51. package/dist/components/data/agenda-row.js +62 -0
  52. package/dist/components/data/day-strip.d.ts +27 -0
  53. package/dist/components/data/day-strip.js +78 -0
  54. package/dist/components/data/sectioned-list.d.ts +21 -6
  55. package/dist/components/data/sectioned-list.js +44 -21
  56. package/dist/components/data/task-row.d.ts +3 -0
  57. package/dist/components/data/task-row.js +6 -14
  58. package/dist/components/layout/collapse.js +1 -1
  59. package/dist/components/layout/text.d.ts +3 -3
  60. package/dist/components/layout/text.js +6 -1
  61. package/dist/components/ui/avatar.js +1 -1
  62. package/dist/components/ui/button.js +1 -1
  63. package/dist/components/ui/check-circle.js +1 -1
  64. package/dist/components/ui/code-block.js +2 -2
  65. package/dist/components/ui/combobox.js +1 -0
  66. package/dist/components/ui/input-group.js +1 -1
  67. package/dist/components/ui/input.js +1 -1
  68. package/dist/components/ui/kbd.js +1 -1
  69. package/dist/components/ui/password-input.d.ts +8 -0
  70. package/dist/components/ui/password-input.js +32 -0
  71. package/dist/components/ui/sonner.d.ts +2 -2
  72. package/dist/components/ui/sonner.js +1 -1
  73. package/dist/components/ui/toggle.js +1 -1
  74. package/dist/index.d.ts +23 -7
  75. package/dist/index.js +21 -5
  76. package/dist/lib/internal/date.js +8 -0
  77. package/dist/lib/{toast-clearance.js → internal/toast-clearance.js} +1 -1
  78. package/dist/styles.css +31 -1
  79. package/docs/COMPONENTS.md +302 -28
  80. package/docs/ROADMAP.md +13 -11
  81. package/llms.txt +10 -8
  82. package/package.json +3 -1
  83. 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__ */ jsx(InputGroupAddon, {
25
+ /* @__PURE__ */ jsxs(InputGroupAddon, {
26
26
  align: "inline-end",
27
- children: /* @__PURE__ */ jsx(InputGroupButton, {
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: [/* @__PURE__ */ jsx("h1", {
22
- className: "text-[28px] leading-tight font-semibold tracking-[-0.02em]",
23
- children: title
24
- }), count !== void 0 && /* @__PURE__ */ jsx(Badge, {
25
- variant: "secondary",
26
- className: "tabular-nums",
27
- children: count
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 };