harebear-ui 0.9.0 → 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 (53) hide show
  1. package/dist/components/admin/app-nav.d.ts +11 -1
  2. package/dist/components/admin/app-nav.js +26 -9
  3. package/dist/components/admin/checklist-editor.d.ts +20 -0
  4. package/dist/components/admin/checklist-editor.js +69 -0
  5. package/dist/components/admin/comment-thread.d.ts +26 -0
  6. package/dist/components/admin/comment-thread.js +68 -0
  7. package/dist/components/admin/copy-field.d.ts +3 -1
  8. package/dist/components/admin/copy-field.js +10 -5
  9. package/dist/components/admin/date-time-picker.js +1 -1
  10. package/dist/components/admin/markdown-editor.d.ts +2 -5
  11. package/dist/components/admin/markdown-editor.js +13 -9
  12. package/dist/components/admin/page-header.d.ts +3 -1
  13. package/dist/components/admin/page-header.js +17 -9
  14. package/dist/components/admin/page.d.ts +16 -0
  15. package/dist/components/admin/page.js +42 -0
  16. package/dist/components/admin/prompt-dialog.d.ts +19 -0
  17. package/dist/components/admin/prompt-dialog.js +83 -0
  18. package/dist/components/admin/settings.d.ts +4 -2
  19. package/dist/components/admin/settings.js +37 -30
  20. package/dist/components/admin/time-zone-picker.d.ts +12 -0
  21. package/dist/components/admin/time-zone-picker.js +31 -0
  22. package/dist/components/admin/user-menu.d.ts +3 -1
  23. package/dist/components/admin/user-menu.js +21 -10
  24. package/dist/components/ai/assistant.js +1 -1
  25. package/dist/components/ai/code-block.js +3 -3
  26. package/dist/components/ai/markdown.d.ts +13 -0
  27. package/dist/components/ai/markdown.js +41 -0
  28. package/dist/components/auth/auth.d.ts +41 -5
  29. package/dist/components/auth/auth.js +165 -155
  30. package/dist/components/data/day-strip.d.ts +6 -1
  31. package/dist/components/data/day-strip.js +15 -5
  32. package/dist/components/layout/collapse.js +1 -1
  33. package/dist/components/layout/text.d.ts +3 -3
  34. package/dist/components/layout/text.js +6 -1
  35. package/dist/components/ui/button.js +1 -1
  36. package/dist/components/ui/check-circle.js +1 -1
  37. package/dist/components/ui/code-block.js +2 -2
  38. package/dist/components/ui/combobox.js +1 -0
  39. package/dist/components/ui/input-group.js +1 -1
  40. package/dist/components/ui/input.js +1 -1
  41. package/dist/components/ui/sonner.d.ts +2 -2
  42. package/dist/components/ui/sonner.js +1 -1
  43. package/dist/components/ui/toggle.js +1 -1
  44. package/dist/index.d.ts +9 -3
  45. package/dist/index.js +9 -3
  46. package/dist/lib/{date.js → internal/date.js} +1 -1
  47. package/dist/lib/{toast-clearance.js → internal/toast-clearance.js} +1 -1
  48. package/dist/styles.css +6 -1
  49. package/docs/COMPONENTS.md +150 -26
  50. package/docs/ROADMAP.md +1 -0
  51. package/llms.txt +7 -6
  52. package/package.json +3 -1
  53. package/skills/harebear-ui/SKILL.md +5 -4
@@ -12,6 +12,10 @@ export interface AppNavPlace extends Omit<AppNavItem, "badge"> {
12
12
  export interface AppNavGroup {
13
13
  /** Group heading, e.g. "Lists". */
14
14
  label: string;
15
+ /** Makes the heading a link, e.g. "/lists"; the chevron next to it still collapses the group. */
16
+ href?: string;
17
+ /** The heading link is the current page: aria-current and the active text style. */
18
+ active?: boolean;
15
19
  items: AppNavPlace[];
16
20
  /** Expanded at first. Default true. */
17
21
  defaultOpen?: boolean;
@@ -47,6 +51,12 @@ export interface AppNavProps {
47
51
  defaultOpen?: boolean;
48
52
  /** Sidebar width. Default "15.75rem". */
49
53
  width?: string;
54
+ /**
55
+ * Main column exactly fills the screen (minus the tab bar on phones, and the round action button's space when
56
+ * `action` is set) and does not scroll, for a chat or map. The
57
+ * child takes the height with `flex-1 min-h-0` (ChatPanel does) and scrolls inside itself.
58
+ */
59
+ fill?: boolean;
50
60
  /** Classes for the main column. */
51
61
  className?: string;
52
62
  children?: React.ReactNode;
@@ -57,5 +67,5 @@ export interface AppNavProps {
57
67
  * and an optional round action button. The main column gets bottom padding and sets `--hb-bottom-inset` (and
58
68
  * `--hb-fab-inset`) so an AssistantDock rendered inside sits above the tab bar and the action button.
59
69
  */
60
- declare function AppNav({ brand, places, group, footer, action, label, linkComponent, collapsible, storageKey, defaultOpen, width, className, children }: AppNavProps): React.JSX.Element;
70
+ declare function AppNav({ brand, places, group, footer, action, label, linkComponent, collapsible, storageKey, defaultOpen, width, fill, className, children }: AppNavProps): React.JSX.Element;
61
71
  export { AppNav };
@@ -4,7 +4,7 @@ import { Badge } from "../ui/badge.js";
4
4
  import { Button } from "../ui/button.js";
5
5
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
6
6
  import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "../ui/sidebar.js";
7
- import { useToastClearance } from "../../lib/toast-clearance.js";
7
+ import { useToastClearance } from "../../lib/internal/toast-clearance.js";
8
8
  import { cn } from "../../lib/utils.js";
9
9
  import * as React from "react";
10
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -17,7 +17,7 @@ const TAB_LIMIT = 5;
17
17
  * and an optional round action button. The main column gets bottom padding and sets `--hb-bottom-inset` (and
18
18
  * `--hb-fab-inset`) so an AssistantDock rendered inside sits above the tab bar and the action button.
19
19
  */
20
- function AppNav({ brand, places, group, footer, action, label = "Main", linkComponent = "a", collapsible, storageKey, defaultOpen, width = "15.75rem", className, children }) {
20
+ function AppNav({ brand, places, group, footer, action, label = "Main", linkComponent = "a", collapsible, storageKey, defaultOpen, width = "15.75rem", fill, className, children }) {
21
21
  const tabs = places.filter((p) => p.tab !== false).slice(0, TAB_LIMIT);
22
22
  useToastClearance("--hb-toast-nav", "calc(4.5rem + env(safe-area-inset-bottom))", tabs.length > 0);
23
23
  useToastClearance("--hb-toast-fab", "4.5rem", Boolean(action));
@@ -53,7 +53,7 @@ function AppNav({ brand, places, group, footer, action, label = "Main", linkComp
53
53
  Link: linkComponent
54
54
  })]
55
55
  }),
56
- className: cn("max-md:pb-[calc(4.5rem+env(safe-area-inset-bottom))] max-md:[--hb-bottom-inset:calc(4.5rem+env(safe-area-inset-bottom))]", action && "max-md:[--hb-fab-inset:4.5rem]", className),
56
+ className: cn("max-md:pb-[calc(4.5rem+env(safe-area-inset-bottom))] max-md:[--hb-bottom-inset:calc(4.5rem+env(safe-area-inset-bottom))]", action && "max-md:pb-[calc(9rem+env(safe-area-inset-bottom))] max-md:[--hb-fab-inset:4.5rem]", fill && "h-dvh overflow-hidden", className),
57
57
  children: [
58
58
  children,
59
59
  tabs.length > 0 && /* @__PURE__ */ jsx(TabBar, {
@@ -101,6 +101,10 @@ function PlaceItem({ place, Link }) {
101
101
  }
102
102
  function NavGroup({ group, Link }) {
103
103
  const [open, setOpen] = React.useState(group.defaultOpen ?? true);
104
+ const chevron = /* @__PURE__ */ jsx(ChevronDownIcon, {
105
+ "aria-hidden": "true",
106
+ className: cn("size-3.5 transition-transform motion-reduce:transition-none", !open && "-rotate-90")
107
+ });
104
108
  return /* @__PURE__ */ jsxs(Collapsible, {
105
109
  open,
106
110
  onOpenChange: setOpen,
@@ -108,15 +112,28 @@ function NavGroup({ group, Link }) {
108
112
  className: "flex flex-col gap-1 group-data-[collapsible=icon]:hidden",
109
113
  children: [/* @__PURE__ */ jsxs("div", {
110
114
  className: "flex items-center gap-1 pr-1",
111
- children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
112
- className: "flex h-8 min-w-0 flex-1 items-center gap-1.5 rounded-md px-3 text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/40",
115
+ children: [group.href ? /* @__PURE__ */ jsxs(Fragment, { children: [
116
+ /* @__PURE__ */ jsx(Link, {
117
+ href: group.href,
118
+ "aria-current": group.active ? "page" : void 0,
119
+ className: cn("flex h-8 min-w-0 items-center rounded-md px-3 text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring pointer-coarse:h-11", group.active && "text-foreground"),
120
+ children: /* @__PURE__ */ jsx("span", {
121
+ className: "truncate",
122
+ children: group.label
123
+ })
124
+ }),
125
+ /* @__PURE__ */ jsx(CollapsibleTrigger, {
126
+ "aria-label": `${open ? "Collapse" : "Expand"} ${group.label}`,
127
+ className: "-ml-1 grid size-8 shrink-0 place-items-center rounded-md text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring pointer-coarse:size-11",
128
+ children: chevron
129
+ }),
130
+ /* @__PURE__ */ jsx("span", { className: "flex-1" })
131
+ ] }) : /* @__PURE__ */ jsxs(CollapsibleTrigger, {
132
+ className: "flex h-8 min-w-0 flex-1 items-center gap-1.5 rounded-md px-3 text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring",
113
133
  children: [/* @__PURE__ */ jsx("span", {
114
134
  className: "truncate",
115
135
  children: group.label
116
- }), /* @__PURE__ */ jsx(ChevronDownIcon, {
117
- "aria-hidden": "true",
118
- className: cn("size-3.5 transition-transform motion-reduce:transition-none", !open && "-rotate-90")
119
- })]
136
+ }), chevron]
120
137
  }), group.onAdd && /* @__PURE__ */ jsx(Button, {
121
138
  type: "button",
122
139
  variant: "ghost",
@@ -0,0 +1,20 @@
1
+ import * as React from "react";
2
+ export interface ChecklistItem {
3
+ id: string;
4
+ text: string;
5
+ done: boolean;
6
+ }
7
+ export interface ChecklistEditorProps extends Omit<React.ComponentProps<"div">, "children" | "onToggle"> {
8
+ items: ChecklistItem[];
9
+ onToggle: (id: string, done: boolean) => void;
10
+ onRemove: (id: string) => void;
11
+ /** Called with the trimmed text when the add field is submitted with Enter. The field then clears. */
12
+ onAdd: (text: string) => void;
13
+ /** Placeholder and accessible name of the add field. Default "Add an item". */
14
+ addPlaceholder?: string;
15
+ /** Progress line above the items, shown when there are any. Default "2 of 5 done". */
16
+ progressLabel?: (done: number, total: number) => React.ReactNode;
17
+ }
18
+ /** Steps inside a record (a to-do's checklist): tick, remove, add with Enter, and an "x of y done" line. */
19
+ declare function ChecklistEditor({ items, onToggle, onRemove, onAdd, addPlaceholder, progressLabel, className, ...props }: ChecklistEditorProps): React.JSX.Element;
20
+ export { ChecklistEditor };
@@ -0,0 +1,69 @@
1
+ "use client";
2
+ import { Button } from "../ui/button.js";
3
+ import { CheckCircle } from "../ui/check-circle.js";
4
+ import { Input } from "../ui/input.js";
5
+ import { cn } from "../../lib/utils.js";
6
+ import * as React from "react";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { XIcon } from "lucide-react";
9
+ // src/components/admin/checklist-editor.tsx
10
+ const defaultProgress = (done, total) => `${done} of ${total} done`;
11
+ /** Steps inside a record (a to-do's checklist): tick, remove, add with Enter, and an "x of y done" line. */
12
+ function ChecklistEditor({ items, onToggle, onRemove, onAdd, addPlaceholder = "Add an item", progressLabel = defaultProgress, className, ...props }) {
13
+ const [text, setText] = React.useState("");
14
+ const id = React.useId();
15
+ const done = items.filter((i) => i.done).length;
16
+ return /* @__PURE__ */ jsxs("div", {
17
+ role: "group",
18
+ "data-slot": "checklist-editor",
19
+ className: cn("flex flex-col gap-2", className),
20
+ ...props,
21
+ children: [items.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("p", {
22
+ className: "text-xs text-muted-foreground",
23
+ children: progressLabel(done, items.length)
24
+ }), /* @__PURE__ */ jsx("ul", {
25
+ className: "flex flex-col",
26
+ children: items.map((item) => /* @__PURE__ */ jsxs("li", {
27
+ className: "flex min-w-0 items-start gap-3 py-1.5 ps-2",
28
+ children: [
29
+ /* @__PURE__ */ jsx(CheckCircle, {
30
+ id: `${id}-${item.id}`,
31
+ className: "mt-2",
32
+ checked: item.done,
33
+ onCheckedChange: (v) => onToggle(item.id, v === true)
34
+ }),
35
+ /* @__PURE__ */ jsx("label", {
36
+ htmlFor: `${id}-${item.id}`,
37
+ className: cn("min-w-0 flex-1 py-2 text-[15px] leading-6 wrap-anywhere", item.done && "text-muted-foreground line-through decoration-muted-foreground/60"),
38
+ children: item.text
39
+ }),
40
+ /* @__PURE__ */ jsx(Button, {
41
+ type: "button",
42
+ variant: "ghost",
43
+ size: "icon",
44
+ className: "pointer-coarse:size-11",
45
+ "aria-label": `Remove ${item.text}`,
46
+ onClick: () => onRemove(item.id),
47
+ children: /* @__PURE__ */ jsx(XIcon, {})
48
+ })
49
+ ]
50
+ }, item.id))
51
+ })] }), /* @__PURE__ */ jsx("form", {
52
+ onSubmit: (e) => {
53
+ e.preventDefault();
54
+ const value = text.trim();
55
+ if (!value) return;
56
+ onAdd(value);
57
+ setText("");
58
+ },
59
+ children: /* @__PURE__ */ jsx(Input, {
60
+ value: text,
61
+ onChange: (e) => setText(e.target.value),
62
+ placeholder: addPlaceholder,
63
+ "aria-label": addPlaceholder,
64
+ enterKeyHint: "done"
65
+ })
66
+ })]
67
+ });
68
+ }
69
+ export { ChecklistEditor };
@@ -0,0 +1,26 @@
1
+ import { CategoryColor } from "./category.js";
2
+ import * as React from "react";
3
+ export interface CommentThreadComment {
4
+ id: string;
5
+ author: {
6
+ name: string;
7
+ color?: CategoryColor;
8
+ avatarUrl?: string;
9
+ };
10
+ /** Muted text after the name, e.g. "2 h ago" or "via HomeBot · 2 h ago". */
11
+ meta?: React.ReactNode;
12
+ /** Markdown, rendered with `Markdown`. */
13
+ body: string;
14
+ }
15
+ export interface CommentThreadProps extends Omit<React.ComponentProps<"div">, "children" | "onSubmit"> {
16
+ comments: CommentThreadComment[];
17
+ /** Called with the trimmed text. The box then clears. */
18
+ onSubmit: (text: string) => void;
19
+ /** Placeholder and accessible name of the box. Default "Add a comment". */
20
+ placeholder?: string;
21
+ /** Default "Comment". */
22
+ submitLabel?: string;
23
+ }
24
+ /** Comments on a record, oldest first, with a box to add one. Bodies are Markdown. */
25
+ declare function CommentThread({ comments, onSubmit, placeholder, submitLabel, className, ...props }: CommentThreadProps): React.JSX.Element;
26
+ export { CommentThread };
@@ -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
  });
@@ -4,7 +4,7 @@ import { Calendar } from "../ui/calendar.js";
4
4
  import { Input } from "../ui/input.js";
5
5
  import { Label } from "../ui/label.js";
6
6
  import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
7
- import { parseLocalDate } from "../../lib/date.js";
7
+ import { parseLocalDate } from "../../lib/internal/date.js";
8
8
  import { cn } from "../../lib/utils.js";
9
9
  import * as React from "react";
10
10
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -2,11 +2,8 @@ import * as React from "react";
2
2
  export interface MarkdownEditorProps extends Omit<React.ComponentProps<"textarea">, "value" | "defaultValue" | "onChange"> {
3
3
  value: string;
4
4
  onValueChange: (value: string) => void;
5
- /**
6
- * Turns the Markdown into elements for the Preview tab; the result is wrapped in Prose.
7
- * With react-markdown: `(md) => <Markdown components={{ blockquote: ProseBlockquote }}>{md}</Markdown>`.
8
- */
9
- renderPreview: (markdown: string) => React.ReactNode;
5
+ /** Custom Preview renderer; the result is wrapped in Prose. Default: the library's `Markdown`. */
6
+ renderPreview?: (markdown: string) => React.ReactNode;
10
7
  /** Classes for the wrapping Tabs. `className` goes to the textarea. */
11
8
  wrapperClassName?: string;
12
9
  }
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { Prose } from "../ai/code-block.js";
3
+ import { Markdown } from "../ai/markdown.js";
3
4
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs.js";
4
5
  import { Textarea } from "../ui/textarea.js";
5
6
  import { cn } from "../../lib/utils.js";
@@ -12,13 +13,16 @@ function MarkdownEditor({ value, onValueChange, renderPreview, className, wrappe
12
13
  defaultValue: "write",
13
14
  className: cn("gap-3", wrapperClassName),
14
15
  children: [
15
- /* @__PURE__ */ jsxs(TabsList, { children: [/* @__PURE__ */ jsx(TabsTrigger, {
16
- value: "write",
17
- children: "Write"
18
- }), /* @__PURE__ */ jsx(TabsTrigger, {
19
- value: "preview",
20
- children: "Preview"
21
- })] }),
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
+ }),
22
26
  /* @__PURE__ */ jsx(TabsContent, {
23
27
  value: "write",
24
28
  children: /* @__PURE__ */ jsx(Textarea, {
@@ -31,10 +35,10 @@ function MarkdownEditor({ value, onValueChange, renderPreview, className, wrappe
31
35
  /* @__PURE__ */ jsx(TabsContent, {
32
36
  value: "preview",
33
37
  className: "min-h-48 rounded-md border px-3.5 py-3",
34
- children: value.trim() ? /* @__PURE__ */ jsx(Prose, { children: renderPreview(value) }) : /* @__PURE__ */ jsx("p", {
38
+ children: !value.trim() ? /* @__PURE__ */ jsx("p", {
35
39
  className: "text-sm text-muted-foreground",
36
40
  children: "Nothing to preview."
37
- })
41
+ }) : renderPreview ? /* @__PURE__ */ jsx(Prose, { children: renderPreview(value) }) : /* @__PURE__ */ jsx(Markdown, { children: value })
38
42
  })
39
43
  ]
40
44
  });
@@ -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 };