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.
- package/dist/components/admin/app-nav.d.ts +11 -1
- package/dist/components/admin/app-nav.js +26 -9
- package/dist/components/admin/checklist-editor.d.ts +20 -0
- package/dist/components/admin/checklist-editor.js +69 -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.js +1 -1
- package/dist/components/admin/markdown-editor.d.ts +2 -5
- package/dist/components/admin/markdown-editor.js +13 -9
- 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-menu.d.ts +3 -1
- package/dist/components/admin/user-menu.js +21 -10
- package/dist/components/ai/assistant.js +1 -1
- package/dist/components/ai/code-block.js +3 -3
- 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 +165 -155
- package/dist/components/data/day-strip.d.ts +6 -1
- package/dist/components/data/day-strip.js +15 -5
- 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/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/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 +9 -3
- package/dist/index.js +9 -3
- package/dist/lib/{date.js → internal/date.js} +1 -1
- package/dist/lib/{toast-clearance.js → internal/toast-clearance.js} +1 -1
- package/dist/styles.css +6 -1
- package/docs/COMPONENTS.md +150 -26
- package/docs/ROADMAP.md +1 -0
- package/llms.txt +7 -6
- package/package.json +3 -1
- 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(
|
|
112
|
-
|
|
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
|
-
}),
|
|
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__ */
|
|
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
|
});
|
|
@@ -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
|
-
|
|
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, {
|
|
16
|
-
|
|
17
|
-
children:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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(
|
|
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: [
|
|
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 };
|