harebear-ui 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/dist/components/admin/app-nav.d.ts +15 -1
- package/dist/components/admin/app-nav.js +28 -11
- package/dist/components/admin/avatar-upload.d.ts +32 -0
- package/dist/components/admin/avatar-upload.js +121 -0
- package/dist/components/admin/category.d.ts +17 -0
- package/dist/components/admin/category.js +35 -0
- package/dist/components/admin/celebration.d.ts +7 -0
- package/dist/components/admin/celebration.js +61 -0
- package/dist/components/admin/checklist-editor.d.ts +20 -0
- package/dist/components/admin/checklist-editor.js +69 -0
- package/dist/components/admin/color-swatch-picker.d.ts +15 -0
- package/dist/components/admin/color-swatch-picker.js +68 -0
- package/dist/components/admin/comment-thread.d.ts +26 -0
- package/dist/components/admin/comment-thread.js +68 -0
- package/dist/components/admin/copy-field.d.ts +3 -1
- package/dist/components/admin/copy-field.js +10 -5
- package/dist/components/admin/date-time-picker.d.ts +20 -0
- package/dist/components/admin/date-time-picker.js +97 -0
- package/dist/components/admin/detail-sheet.d.ts +18 -0
- package/dist/components/admin/detail-sheet.js +80 -0
- package/dist/components/admin/markdown-editor.d.ts +12 -0
- package/dist/components/admin/markdown-editor.js +46 -0
- package/dist/components/admin/page-header.d.ts +3 -1
- package/dist/components/admin/page-header.js +17 -9
- package/dist/components/admin/page.d.ts +16 -0
- package/dist/components/admin/page.js +42 -0
- package/dist/components/admin/prompt-dialog.d.ts +19 -0
- package/dist/components/admin/prompt-dialog.js +83 -0
- package/dist/components/admin/settings.d.ts +4 -2
- package/dist/components/admin/settings.js +37 -30
- package/dist/components/admin/time-zone-picker.d.ts +12 -0
- package/dist/components/admin/time-zone-picker.js +31 -0
- package/dist/components/admin/user-chip.d.ts +34 -3
- package/dist/components/admin/user-chip.js +66 -7
- package/dist/components/admin/user-menu.d.ts +6 -1
- package/dist/components/admin/user-menu.js +22 -10
- package/dist/components/ai/ask-box.d.ts +14 -2
- package/dist/components/ai/ask-box.js +90 -54
- package/dist/components/ai/assistant.d.ts +13 -3
- package/dist/components/ai/assistant.js +36 -8
- package/dist/components/ai/code-block.d.ts +10 -3
- package/dist/components/ai/code-block.js +57 -8
- package/dist/components/ai/conversation-list.d.ts +13 -3
- package/dist/components/ai/conversation-list.js +15 -26
- package/dist/components/ai/markdown.d.ts +13 -0
- package/dist/components/ai/markdown.js +41 -0
- package/dist/components/auth/auth.d.ts +41 -5
- package/dist/components/auth/auth.js +167 -159
- package/dist/components/data/agenda-row.d.ts +26 -0
- package/dist/components/data/agenda-row.js +62 -0
- package/dist/components/data/day-strip.d.ts +27 -0
- package/dist/components/data/day-strip.js +78 -0
- package/dist/components/data/sectioned-list.d.ts +21 -6
- package/dist/components/data/sectioned-list.js +44 -21
- package/dist/components/data/task-row.d.ts +3 -0
- package/dist/components/data/task-row.js +6 -14
- package/dist/components/layout/collapse.js +1 -1
- package/dist/components/layout/text.d.ts +3 -3
- package/dist/components/layout/text.js +6 -1
- package/dist/components/ui/avatar.js +1 -1
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/check-circle.js +1 -1
- package/dist/components/ui/code-block.js +2 -2
- package/dist/components/ui/combobox.js +1 -0
- package/dist/components/ui/input-group.js +1 -1
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/kbd.js +1 -1
- package/dist/components/ui/password-input.d.ts +8 -0
- package/dist/components/ui/password-input.js +32 -0
- package/dist/components/ui/sonner.d.ts +2 -2
- package/dist/components/ui/sonner.js +1 -1
- package/dist/components/ui/toggle.js +1 -1
- package/dist/index.d.ts +23 -7
- package/dist/index.js +21 -5
- package/dist/lib/internal/date.js +8 -0
- package/dist/lib/{toast-clearance.js → internal/toast-clearance.js} +1 -1
- package/dist/styles.css +31 -1
- package/docs/COMPONENTS.md +302 -28
- package/docs/ROADMAP.md +13 -11
- package/llms.txt +10 -8
- package/package.json +3 -1
- package/skills/harebear-ui/SKILL.md +5 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { AskBox } from "./ask-box.js";
|
|
3
3
|
import { AssistantAvatar, ChatMessage, ChatThread, TypingIndicator } from "./chat.js";
|
|
4
4
|
import { Button } from "../ui/button.js";
|
|
5
|
-
import { useToastClearance } from "../../lib/toast-clearance.js";
|
|
5
|
+
import { useToastClearance } from "../../lib/internal/toast-clearance.js";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
7
7
|
import * as React from "react";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -26,6 +26,7 @@ function AssistantProvider({ onSend, initialMessages = [], defaultOpen = false,
|
|
|
26
26
|
pageRef.current = page;
|
|
27
27
|
const onSendRef = React.useRef(onSend);
|
|
28
28
|
onSendRef.current = onSend;
|
|
29
|
+
const runRef = React.useRef(null);
|
|
29
30
|
const append = React.useCallback((m) => {
|
|
30
31
|
const id = m.id ?? newId();
|
|
31
32
|
setMessages((list) => [...list, {
|
|
@@ -40,43 +41,66 @@ function AssistantProvider({ onSend, initialMessages = [], defaultOpen = false,
|
|
|
40
41
|
content
|
|
41
42
|
} : m));
|
|
42
43
|
}, []);
|
|
43
|
-
const send = React.useCallback(async (text) => {
|
|
44
|
+
const send = React.useCallback(async (text, options) => {
|
|
44
45
|
const before = messagesRef.current;
|
|
46
|
+
runRef.current?.abort();
|
|
47
|
+
const run = new AbortController();
|
|
48
|
+
runRef.current = run;
|
|
45
49
|
append({
|
|
46
50
|
role: "user",
|
|
47
51
|
content: text
|
|
48
52
|
});
|
|
49
|
-
setOpen(true);
|
|
53
|
+
if (options?.open !== false) setOpen(true);
|
|
50
54
|
setPending(true);
|
|
51
55
|
try {
|
|
52
56
|
const reply = await onSendRef.current(text, {
|
|
53
57
|
page: pageRef.current,
|
|
54
58
|
messages: before,
|
|
55
|
-
append,
|
|
56
|
-
update
|
|
59
|
+
append: (m) => run.signal.aborted ? "" : append(m),
|
|
60
|
+
update: (id, content) => {
|
|
61
|
+
if (!run.signal.aborted) update(id, content);
|
|
62
|
+
},
|
|
63
|
+
signal: run.signal
|
|
57
64
|
});
|
|
65
|
+
if (run.signal.aborted) return;
|
|
58
66
|
if (reply !== void 0 && reply !== null && reply !== false) append({
|
|
59
67
|
role: "assistant",
|
|
60
68
|
content: reply
|
|
61
69
|
});
|
|
62
70
|
} catch (e) {
|
|
71
|
+
if (run.signal.aborted) return;
|
|
63
72
|
append({
|
|
64
73
|
role: "assistant",
|
|
65
74
|
content: e instanceof Error ? e.message : "Something went wrong."
|
|
66
75
|
});
|
|
67
76
|
} finally {
|
|
68
|
-
|
|
77
|
+
if (runRef.current === run) {
|
|
78
|
+
runRef.current = null;
|
|
79
|
+
setPending(false);
|
|
80
|
+
}
|
|
69
81
|
}
|
|
70
82
|
}, [append, update]);
|
|
83
|
+
const stop = React.useCallback(() => {
|
|
84
|
+
runRef.current?.abort();
|
|
85
|
+
runRef.current = null;
|
|
86
|
+
setPending(false);
|
|
87
|
+
}, []);
|
|
88
|
+
const reply = React.useMemo(() => {
|
|
89
|
+
const start = messages.findLastIndex((m) => m.role === "user");
|
|
90
|
+
const answer = start === -1 ? [] : messages.slice(start + 1);
|
|
91
|
+
return answer.length ? answer.map((m) => /* @__PURE__ */ jsx("div", { children: m.content }, m.id)) : null;
|
|
92
|
+
}, [messages]);
|
|
71
93
|
const value = React.useMemo(() => ({
|
|
72
94
|
open,
|
|
73
95
|
setOpen,
|
|
74
96
|
toggle: () => setOpen((v) => !v),
|
|
75
97
|
messages,
|
|
76
98
|
pending,
|
|
99
|
+
reply,
|
|
77
100
|
page,
|
|
78
101
|
setPage,
|
|
79
102
|
send,
|
|
103
|
+
stop,
|
|
80
104
|
append,
|
|
81
105
|
update,
|
|
82
106
|
clear: () => setMessages([])
|
|
@@ -84,8 +108,10 @@ function AssistantProvider({ onSend, initialMessages = [], defaultOpen = false,
|
|
|
84
108
|
open,
|
|
85
109
|
messages,
|
|
86
110
|
pending,
|
|
111
|
+
reply,
|
|
87
112
|
page,
|
|
88
113
|
send,
|
|
114
|
+
stop,
|
|
89
115
|
append,
|
|
90
116
|
update
|
|
91
117
|
]);
|
|
@@ -119,7 +145,7 @@ function useAssistantPage(page) {
|
|
|
119
145
|
* Sits above AppNav's phone tab bar.
|
|
120
146
|
*/
|
|
121
147
|
function AssistantDock({ title = "Assistant", icon = /* @__PURE__ */ jsx(SparklesIcon, {}), fullScreenHref, emptyTitle = "How can I help?", emptyDescription, suggestions, placeholder, dictation, launcher = true, launcherLabel, className, ...props }) {
|
|
122
|
-
const { open, setOpen, messages, pending, page, send } = useAssistant();
|
|
148
|
+
const { open, setOpen, messages, pending, page, send, stop } = useAssistant();
|
|
123
149
|
const [draft, setDraft] = React.useState("");
|
|
124
150
|
const titleId = React.useId();
|
|
125
151
|
const panelRef = React.useRef(null);
|
|
@@ -222,7 +248,9 @@ function AssistantDock({ title = "Assistant", icon = /* @__PURE__ */ jsx(Sparkle
|
|
|
222
248
|
onSubmit: submit,
|
|
223
249
|
placeholder: placeholder ?? `Ask ${title}`,
|
|
224
250
|
suggestions: messages.length === 0 ? pills : void 0,
|
|
225
|
-
dictation
|
|
251
|
+
dictation,
|
|
252
|
+
pending,
|
|
253
|
+
onStop: stop
|
|
226
254
|
})
|
|
227
255
|
})
|
|
228
256
|
]
|
|
@@ -22,10 +22,17 @@ export interface InlineCodeProps extends React.ComponentProps<"code"> {
|
|
|
22
22
|
truncate?: number;
|
|
23
23
|
}
|
|
24
24
|
declare function InlineCode({ truncate, className, children, title, ...props }: InlineCodeProps): React.JSX.Element;
|
|
25
|
+
/**
|
|
26
|
+
* A blockquote that becomes a `Callout` when its first word is "Tip:" (the word is dropped).
|
|
27
|
+
* Prose applies it to blockquote children; pass it to react-markdown as `components={{ blockquote: ProseBlockquote }}`.
|
|
28
|
+
*/
|
|
29
|
+
declare function ProseBlockquote({ children, node: _node, ...props }: React.ComponentProps<"blockquote"> & {
|
|
30
|
+
node?: unknown;
|
|
31
|
+
}): React.JSX.Element;
|
|
25
32
|
/**
|
|
26
33
|
* Typography for long-form and model output. Styles plain HTML children
|
|
27
34
|
* (p, ul, ol, li, strong, a, code, pre, h1-h4, blockquote, table), so it works
|
|
28
|
-
* with react-markdown or any HTML renderer.
|
|
35
|
+
* with react-markdown or any HTML renderer. A blockquote starting "Tip:" renders as a Callout.
|
|
29
36
|
*/
|
|
30
|
-
declare function Prose({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
31
|
-
export { CodeSnippet, InlineCode, Prose };
|
|
37
|
+
declare function Prose({ className, children, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
38
|
+
export { CodeSnippet, InlineCode, Prose, ProseBlockquote };
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { Callout } from "../admin/callout.js";
|
|
2
3
|
import { Button } from "../ui/button.js";
|
|
3
4
|
import { CodeBlock, CodeBlockContent, CodeBlockHeader, CodeBlockLine, CodeBlockTitle } from "../ui/code-block.js";
|
|
4
5
|
import { useCopyToClipboard } from "../../hooks/use-copy-to-clipboard.js";
|
|
5
6
|
import { useHighlightedCode } from "../../hooks/use-highlighted-code.js";
|
|
6
7
|
import { cn } from "../../lib/utils.js";
|
|
8
|
+
import * as React from "react";
|
|
7
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
-
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
10
|
+
import { CheckIcon, CopyIcon, LightbulbIcon } from "lucide-react";
|
|
9
11
|
// src/components/ai/code-block.tsx
|
|
10
12
|
/**
|
|
11
13
|
* One-prop code panel: language label, copy button, shiki highlighting (loaded
|
|
@@ -28,9 +30,9 @@ function CodeSnippet({ code, language, highlight = true, lineNumbers, hideCopy,
|
|
|
28
30
|
type: "button",
|
|
29
31
|
variant: "ghost",
|
|
30
32
|
size: "sm",
|
|
31
|
-
className: "ms-auto",
|
|
33
|
+
className: "ms-auto pointer-coarse:h-11",
|
|
32
34
|
onClick: () => copy(code),
|
|
33
|
-
"aria-label": "Copy code",
|
|
35
|
+
"aria-label": copied ? void 0 : "Copy code",
|
|
34
36
|
children: [copied ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx(CopyIcon, {}), copied ? "Copied" : "Copy"]
|
|
35
37
|
})]
|
|
36
38
|
}), /* @__PURE__ */ jsx(CodeBlockContent, {
|
|
@@ -50,16 +52,63 @@ function InlineCode({ truncate, className, children, title, ...props }) {
|
|
|
50
52
|
children: cut ? `${children.slice(0, truncate)}…` : children
|
|
51
53
|
});
|
|
52
54
|
}
|
|
55
|
+
const TIP = /^\s*Tip:\s*/;
|
|
56
|
+
/** The children with a leading "Tip:" removed, or undefined when they do not start with it. */
|
|
57
|
+
function withoutTip(children) {
|
|
58
|
+
const items = React.Children.toArray(children);
|
|
59
|
+
const i = items.findIndex((c) => !(typeof c === "string" && c.trim() === ""));
|
|
60
|
+
const first = items[i];
|
|
61
|
+
let rest;
|
|
62
|
+
if (typeof first === "string") {
|
|
63
|
+
if (!TIP.test(first)) return void 0;
|
|
64
|
+
rest = first.replace(TIP, "");
|
|
65
|
+
} else if (React.isValidElement(first) && typeof first.type === "string") {
|
|
66
|
+
const inner = withoutTip(first.props.children);
|
|
67
|
+
if (inner === void 0) return void 0;
|
|
68
|
+
rest = React.cloneElement(first, void 0, inner);
|
|
69
|
+
} else return;
|
|
70
|
+
return [
|
|
71
|
+
...items.slice(0, i),
|
|
72
|
+
rest,
|
|
73
|
+
...items.slice(i + 1)
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A blockquote that becomes a `Callout` when its first word is "Tip:" (the word is dropped).
|
|
78
|
+
* Prose applies it to blockquote children; pass it to react-markdown as `components={{ blockquote: ProseBlockquote }}`.
|
|
79
|
+
*/
|
|
80
|
+
function ProseBlockquote({ children, node: _node, ...props }) {
|
|
81
|
+
const tip = withoutTip(children);
|
|
82
|
+
if (tip === void 0) return /* @__PURE__ */ jsx("blockquote", {
|
|
83
|
+
...props,
|
|
84
|
+
children
|
|
85
|
+
});
|
|
86
|
+
return /* @__PURE__ */ jsx(Callout, {
|
|
87
|
+
"data-tip": "",
|
|
88
|
+
icon: /* @__PURE__ */ jsx(LightbulbIcon, {}),
|
|
89
|
+
className: props.className,
|
|
90
|
+
children: tip
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** Swaps plain blockquote elements for ProseBlockquote, at any depth of plain HTML elements. */
|
|
94
|
+
function withTips(children) {
|
|
95
|
+
return React.Children.map(children, (c) => {
|
|
96
|
+
if (!React.isValidElement(c) || typeof c.type !== "string") return c;
|
|
97
|
+
if (c.type === "blockquote") return /* @__PURE__ */ jsx(ProseBlockquote, { ...c.props });
|
|
98
|
+
return c.props.children === void 0 ? c : React.cloneElement(c, void 0, withTips(c.props.children));
|
|
99
|
+
});
|
|
100
|
+
}
|
|
53
101
|
/**
|
|
54
102
|
* Typography for long-form and model output. Styles plain HTML children
|
|
55
103
|
* (p, ul, ol, li, strong, a, code, pre, h1-h4, blockquote, table), so it works
|
|
56
|
-
* with react-markdown or any HTML renderer.
|
|
104
|
+
* with react-markdown or any HTML renderer. A blockquote starting "Tip:" renders as a Callout.
|
|
57
105
|
*/
|
|
58
|
-
function Prose({ className, ...props }) {
|
|
106
|
+
function Prose({ className, children, ...props }) {
|
|
59
107
|
return /* @__PURE__ */ jsx("div", {
|
|
60
108
|
"data-slot": "prose",
|
|
61
|
-
className: cn("flex min-w-0 flex-col gap-3.5 text-[15px] leading-[1.65] wrap-anywhere text-foreground", "[&_ol]:flex [&_ol]:list-decimal [&_ol]:flex-col [&_ol]:gap-2 [&_ol]:pl-5.5", "[&_ul]:flex [&_ul]:list-disc [&_ul]:flex-col [&_ul]:gap-2 [&_ul]:pl-5.5", "[&_strong]:font-semibold [&_a]:text-primary [&_a]:underline-offset-4 hover:[&_a]:
|
|
62
|
-
...props
|
|
109
|
+
className: cn("flex min-w-0 flex-col gap-3.5 text-[15px] leading-[1.65] wrap-anywhere text-foreground", "[&_ol]:flex [&_ol]:list-decimal [&_ol]:flex-col [&_ol]:gap-2 [&_ol]:pl-5.5", "[&_ul]:flex [&_ul]:list-disc [&_ul]:flex-col [&_ul]:gap-2 [&_ul]:pl-5.5", "[&_strong]:font-semibold [&_a]:text-primary [&_a]:underline [&_a]:decoration-primary/40 [&_a]:underline-offset-4 hover:[&_a]:decoration-primary [&_a]:rounded-sm [&_a:focus-visible]:outline-2 [&_a:focus-visible]:outline-offset-2 [&_a:focus-visible]:outline-ring", "[&_h1]:text-xl [&_h1]:font-semibold [&_h2]:text-lg [&_h2]:font-semibold [&_h3]:font-semibold [&_h4]:font-semibold", "[&_blockquote]:border-l-2 [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground", "[&_:not(pre)>code]:rounded-[6px] [&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[12.5px]", "[&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-muted [&_pre]:p-4.5 [&_pre]:font-mono [&_pre]:text-[13px]", "[&_table]:w-full [&_table]:text-sm [&_td]:border-b [&_td]:py-2 [&_th]:border-b [&_th]:py-2 [&_th]:text-left [&_th]:font-medium", className),
|
|
110
|
+
...props,
|
|
111
|
+
children: withTips(children)
|
|
63
112
|
});
|
|
64
113
|
}
|
|
65
|
-
export { CodeSnippet, InlineCode, Prose };
|
|
114
|
+
export { CodeSnippet, InlineCode, Prose, ProseBlockquote };
|
|
@@ -39,11 +39,21 @@ export interface ConversationListProps extends Omit<React.ComponentProps<"nav">,
|
|
|
39
39
|
* and hides itself when the sidebar is collapsed to its icon rail.
|
|
40
40
|
*/
|
|
41
41
|
declare function ConversationList({ groups, activeId, onSelect, renderAction, onDelete, deleteDescription, empty, className, ...props }: ConversationListProps): React.JSX.Element;
|
|
42
|
+
export interface GroupByDateOptions {
|
|
43
|
+
/** Reference time. Default now; pass it for deterministic tests. */
|
|
44
|
+
now?: number | Date;
|
|
45
|
+
/**
|
|
46
|
+
* `"day"` (default): Today / Yesterday / Previous 7 days / Earlier. `"week"`: Today / This week (the 6 days before
|
|
47
|
+
* today) / Earlier.
|
|
48
|
+
*/
|
|
49
|
+
buckets?: "day" | "week";
|
|
50
|
+
}
|
|
42
51
|
/**
|
|
43
|
-
* Buckets items into Today / Yesterday / Previous 7 days / Earlier
|
|
44
|
-
* newest first, and drops empty groups.
|
|
52
|
+
* Buckets items into Today / Yesterday / Previous 7 days / Earlier (or Today / This week / Earlier with
|
|
53
|
+
* `buckets: "week"`) by a timestamp (ms or Date), newest first, and drops empty groups. The last argument is the
|
|
54
|
+
* options, or just `now`.
|
|
45
55
|
*/
|
|
46
|
-
declare function groupByDate<T>(items: T[], getDate: (item: T) => number | Date, toConversation: (item: T) => Conversation,
|
|
56
|
+
declare function groupByDate<T>(items: T[], getDate: (item: T) => number | Date, toConversation: (item: T) => Conversation, options?: number | Date | GroupByDateOptions): ConversationGroup[];
|
|
47
57
|
/**
|
|
48
58
|
* Short timestamp for lists and headers: the time today ("9:41 AM"), the date this year ("Nov 3"), else with the
|
|
49
59
|
* year ("Nov 3, 2025"). Uses the browser locale unless `locale` is given. Pass `now` for deterministic tests.
|
|
@@ -94,39 +94,28 @@ function ConversationList({ groups, activeId, onSelect, renderAction, onDelete,
|
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
/**
|
|
97
|
-
* Buckets items into Today / Yesterday / Previous 7 days / Earlier
|
|
98
|
-
* newest first, and drops empty groups.
|
|
97
|
+
* Buckets items into Today / Yesterday / Previous 7 days / Earlier (or Today / This week / Earlier with
|
|
98
|
+
* `buckets: "week"`) by a timestamp (ms or Date), newest first, and drops empty groups. The last argument is the
|
|
99
|
+
* options, or just `now`.
|
|
99
100
|
*/
|
|
100
|
-
function groupByDate(items, getDate, toConversation,
|
|
101
|
+
function groupByDate(items, getDate, toConversation, options = {}) {
|
|
102
|
+
const { now = Date.now(), buckets = "day" } = typeof options === "number" || options instanceof Date ? { now: options } : options;
|
|
101
103
|
const d = new Date(now);
|
|
102
104
|
const midnight = (daysAgo) => new Date(d.getFullYear(), d.getMonth(), d.getDate() - daysAgo).getTime();
|
|
103
|
-
const [
|
|
104
|
-
midnight(0),
|
|
105
|
-
midnight(1),
|
|
106
|
-
midnight(7)
|
|
107
|
-
];
|
|
108
|
-
const groups = [
|
|
109
|
-
{
|
|
110
|
-
label: "Today",
|
|
111
|
-
items: []
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
label: "Yesterday",
|
|
115
|
-
items: []
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
label: "Previous 7 days",
|
|
119
|
-
items: []
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
label: "Earlier",
|
|
123
|
-
items: []
|
|
124
|
-
}
|
|
105
|
+
const bounds = buckets === "week" ? [["Today", midnight(0)], ["This week", midnight(6)]] : [
|
|
106
|
+
["Today", midnight(0)],
|
|
107
|
+
["Yesterday", midnight(1)],
|
|
108
|
+
["Previous 7 days", midnight(7)]
|
|
125
109
|
];
|
|
110
|
+
const groups = [...bounds.map(([label]) => label), "Earlier"].map((label) => ({
|
|
111
|
+
label,
|
|
112
|
+
items: []
|
|
113
|
+
}));
|
|
126
114
|
const sorted = [...items].sort((a, b) => +getDate(b) - +getDate(a));
|
|
127
115
|
for (const item of sorted) {
|
|
128
116
|
const t = +getDate(item);
|
|
129
|
-
|
|
117
|
+
const i = bounds.findIndex(([, from]) => t >= from);
|
|
118
|
+
groups[i === -1 ? bounds.length : i]?.items.push(toConversation(item));
|
|
130
119
|
}
|
|
131
120
|
return groups.filter((g) => g.items.length);
|
|
132
121
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface MarkdownProps extends Omit<React.ComponentProps<"div">, "children"> {
|
|
3
|
+
/** The Markdown source. GitHub flavoured (tables, task lists, strikethrough, autolinks). Raw HTML is dropped. */
|
|
4
|
+
children: string;
|
|
5
|
+
/** Component for in-app links (href without a scheme, e.g. "/lists/2"), e.g. Next.js or React Router Link. Gets `href`. */
|
|
6
|
+
linkComponent?: React.ElementType;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Renders Markdown safely inside Prose: no raw HTML, unsafe URLs (javascript:) removed, `> Tip:` blockquotes as
|
|
10
|
+
* a Callout. Other props go to the Prose wrapper.
|
|
11
|
+
*/
|
|
12
|
+
declare function Markdown({ children, linkComponent: Link, ...props }: MarkdownProps): React.JSX.Element;
|
|
13
|
+
export { Markdown };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Prose, ProseBlockquote } from "./code-block.js";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import ReactMarkdown from "react-markdown";
|
|
6
|
+
import remarkGfm from "remark-gfm";
|
|
7
|
+
// src/components/ai/markdown.tsx
|
|
8
|
+
const plugins = [remarkGfm];
|
|
9
|
+
const SCHEME = /^([a-z][a-z\d+.-]*:|\/\/)/i;
|
|
10
|
+
/**
|
|
11
|
+
* Renders Markdown safely inside Prose: no raw HTML, unsafe URLs (javascript:) removed, `> Tip:` blockquotes as
|
|
12
|
+
* a Callout. Other props go to the Prose wrapper.
|
|
13
|
+
*/
|
|
14
|
+
function Markdown({ children, linkComponent: Link, ...props }) {
|
|
15
|
+
const components = React.useMemo(() => ({
|
|
16
|
+
blockquote: ProseBlockquote,
|
|
17
|
+
a: ({ node: _node, href = "", ...rest }) => Link && !SCHEME.test(href) ? /* @__PURE__ */ jsx(Link, {
|
|
18
|
+
href,
|
|
19
|
+
...rest
|
|
20
|
+
}) : /* @__PURE__ */ jsx("a", {
|
|
21
|
+
href,
|
|
22
|
+
...rest
|
|
23
|
+
}),
|
|
24
|
+
input: ({ node: _node, checked, ...rest }) => /* @__PURE__ */ jsx("input", {
|
|
25
|
+
...rest,
|
|
26
|
+
checked,
|
|
27
|
+
"aria-label": checked ? "Done" : "Not done"
|
|
28
|
+
})
|
|
29
|
+
}), [Link]);
|
|
30
|
+
return /* @__PURE__ */ jsx(Prose, {
|
|
31
|
+
"data-slot": "markdown",
|
|
32
|
+
...props,
|
|
33
|
+
children: /* @__PURE__ */ jsx(ReactMarkdown, {
|
|
34
|
+
remarkPlugins: plugins,
|
|
35
|
+
components,
|
|
36
|
+
skipHtml: true,
|
|
37
|
+
children
|
|
38
|
+
})
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export { Markdown };
|
|
@@ -33,6 +33,23 @@ declare function GitHubIcon(props: React.ComponentProps<"svg">): React.JSX.Eleme
|
|
|
33
33
|
declare function GoogleIcon(props: React.ComponentProps<"svg">): React.JSX.Element;
|
|
34
34
|
/** Apple mark, for AuthProvider icons. */
|
|
35
35
|
declare function AppleIcon(props: React.ComponentProps<"svg">): React.JSX.Element;
|
|
36
|
+
export interface AuthFormProps extends Omit<React.ComponentProps<"form">, "onSubmit" | "title"> {
|
|
37
|
+
title: React.ReactNode;
|
|
38
|
+
description?: React.ReactNode;
|
|
39
|
+
/** The fields. Give each a `name`; they arrive in onSubmit's FormData. */
|
|
40
|
+
children?: React.ReactNode;
|
|
41
|
+
/** Form-level error, e.g. "Invalid email or password". */
|
|
42
|
+
error?: React.ReactNode;
|
|
43
|
+
submitLabel: React.ReactNode;
|
|
44
|
+
/** Shows a spinner on the submit button and disables every control. */
|
|
45
|
+
loading?: boolean;
|
|
46
|
+
/** Called with the form's data once the browser's validation passes. Omit to submit natively (e.g. to an `action`). */
|
|
47
|
+
onSubmit?: (data: FormData) => void | Promise<void>;
|
|
48
|
+
/** Under the submit button: provider buttons, a "Sign in" link. */
|
|
49
|
+
footer?: React.ReactNode;
|
|
50
|
+
}
|
|
51
|
+
/** The shell LoginForm, SignupForm and NewPasswordForm share: heading, your fields, error, submit button. */
|
|
52
|
+
declare function AuthForm({ title, description, children, error, submitLabel, loading, onSubmit, footer, className, ...props }: AuthFormProps): React.JSX.Element;
|
|
36
53
|
interface AuthFormBase extends Omit<React.ComponentProps<"form">, "onSubmit" | "title"> {
|
|
37
54
|
title?: React.ReactNode;
|
|
38
55
|
description?: React.ReactNode;
|
|
@@ -51,7 +68,7 @@ export interface LoginValues {
|
|
|
51
68
|
/** Absent in "email-only" mode. */
|
|
52
69
|
password?: string;
|
|
53
70
|
}
|
|
54
|
-
export interface LoginFormProps extends AuthFormBase {
|
|
71
|
+
export interface LoginFormProps extends Omit<AuthFormBase, "children"> {
|
|
55
72
|
/** Called with the entered values. Omit to let the form submit natively (e.g. to an `action`). */
|
|
56
73
|
onSubmit?: (values: LoginValues) => void | Promise<void>;
|
|
57
74
|
/** "email-only" drops the password field for magic-link or OTP flows (shadcn login-05). */
|
|
@@ -61,14 +78,17 @@ export interface LoginFormProps extends AuthFormBase {
|
|
|
61
78
|
submitLabel?: React.ReactNode;
|
|
62
79
|
}
|
|
63
80
|
/** Email + password sign-in form with optional SSO buttons. Pair with AuthLayout. */
|
|
64
|
-
declare function LoginForm({ title, description, onSubmit, mode, providers, onProviderClick, providersFirst, forgotPasswordHref, signupHref, submitLabel,
|
|
81
|
+
declare function LoginForm({ title, description, onSubmit, mode, providers, onProviderClick, providersFirst, forgotPasswordHref, signupHref, submitLabel, ...props }: LoginFormProps): React.JSX.Element;
|
|
65
82
|
export interface SignupValues {
|
|
66
83
|
name?: string;
|
|
67
84
|
email: string;
|
|
68
85
|
password: string;
|
|
69
86
|
}
|
|
70
87
|
export interface SignupFormProps extends AuthFormBase {
|
|
71
|
-
|
|
88
|
+
/** `data` holds every field, including your own `children` fields. */
|
|
89
|
+
onSubmit?: (values: SignupValues, data: FormData) => void | Promise<void>;
|
|
90
|
+
/** Extra fields shown before the name field (e.g. a household name). Give each a `name`; read it from `data`. */
|
|
91
|
+
children?: React.ReactNode;
|
|
72
92
|
/** Ask for a full name. Default true. */
|
|
73
93
|
nameField?: boolean;
|
|
74
94
|
/** Ask for the password twice; mismatches are reported before onSubmit. Default true. */
|
|
@@ -81,5 +101,21 @@ export interface SignupFormProps extends AuthFormBase {
|
|
|
81
101
|
submitLabel?: React.ReactNode;
|
|
82
102
|
}
|
|
83
103
|
/** Account creation form: name, email, password (+ confirm), optional SSO. Pair with AuthLayout. */
|
|
84
|
-
declare function SignupForm({ title, description, onSubmit, nameField, confirmPassword, passwordHint, minPasswordLength, providers, onProviderClick, providersFirst, loginHref, submitLabel,
|
|
85
|
-
export
|
|
104
|
+
declare function SignupForm({ title, description, onSubmit, children, nameField, confirmPassword, passwordHint, minPasswordLength, providers, onProviderClick, providersFirst, loginHref, submitLabel, ...props }: SignupFormProps): React.JSX.Element;
|
|
105
|
+
export interface NewPasswordFormProps extends Omit<React.ComponentProps<"form">, "onSubmit" | "title" | "children">, Pick<AuthFormProps, "description" | "error" | "loading"> {
|
|
106
|
+
/** Default "Choose a new password". */
|
|
107
|
+
title?: React.ReactNode;
|
|
108
|
+
/** Called once both fields match. */
|
|
109
|
+
onSubmit?: (values: {
|
|
110
|
+
password: string;
|
|
111
|
+
}) => void | Promise<void>;
|
|
112
|
+
/** Minimum length enforced by the browser. Default 8. */
|
|
113
|
+
minPasswordLength?: number;
|
|
114
|
+
/** Hint under the password field. Default "At least N characters." */
|
|
115
|
+
passwordHint?: React.ReactNode;
|
|
116
|
+
/** Default "Save password". */
|
|
117
|
+
submitLabel?: React.ReactNode;
|
|
118
|
+
}
|
|
119
|
+
/** Password reset or first password: new password + confirm. Pair with AuthLayout. */
|
|
120
|
+
declare function NewPasswordForm({ title, onSubmit, minPasswordLength, passwordHint, submitLabel, ...props }: NewPasswordFormProps): React.JSX.Element;
|
|
121
|
+
export { AppleIcon, AuthForm, AuthLayout, GitHubIcon, GoogleIcon, LoginForm, NewPasswordForm, SignupForm };
|