harebear-ui 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/AGENTS.md +2 -2
  2. package/dist/components/admin/app-nav.d.ts +15 -1
  3. package/dist/components/admin/app-nav.js +28 -11
  4. package/dist/components/admin/avatar-upload.d.ts +32 -0
  5. package/dist/components/admin/avatar-upload.js +121 -0
  6. package/dist/components/admin/category.d.ts +17 -0
  7. package/dist/components/admin/category.js +35 -0
  8. package/dist/components/admin/celebration.d.ts +7 -0
  9. package/dist/components/admin/celebration.js +61 -0
  10. package/dist/components/admin/checklist-editor.d.ts +20 -0
  11. package/dist/components/admin/checklist-editor.js +69 -0
  12. package/dist/components/admin/color-swatch-picker.d.ts +15 -0
  13. package/dist/components/admin/color-swatch-picker.js +68 -0
  14. package/dist/components/admin/comment-thread.d.ts +26 -0
  15. package/dist/components/admin/comment-thread.js +68 -0
  16. package/dist/components/admin/copy-field.d.ts +3 -1
  17. package/dist/components/admin/copy-field.js +10 -5
  18. package/dist/components/admin/date-time-picker.d.ts +20 -0
  19. package/dist/components/admin/date-time-picker.js +97 -0
  20. package/dist/components/admin/detail-sheet.d.ts +18 -0
  21. package/dist/components/admin/detail-sheet.js +80 -0
  22. package/dist/components/admin/markdown-editor.d.ts +12 -0
  23. package/dist/components/admin/markdown-editor.js +46 -0
  24. package/dist/components/admin/page-header.d.ts +3 -1
  25. package/dist/components/admin/page-header.js +17 -9
  26. package/dist/components/admin/page.d.ts +16 -0
  27. package/dist/components/admin/page.js +42 -0
  28. package/dist/components/admin/prompt-dialog.d.ts +19 -0
  29. package/dist/components/admin/prompt-dialog.js +83 -0
  30. package/dist/components/admin/settings.d.ts +4 -2
  31. package/dist/components/admin/settings.js +37 -30
  32. package/dist/components/admin/time-zone-picker.d.ts +12 -0
  33. package/dist/components/admin/time-zone-picker.js +31 -0
  34. package/dist/components/admin/user-chip.d.ts +34 -3
  35. package/dist/components/admin/user-chip.js +66 -7
  36. package/dist/components/admin/user-menu.d.ts +6 -1
  37. package/dist/components/admin/user-menu.js +22 -10
  38. package/dist/components/ai/ask-box.d.ts +14 -2
  39. package/dist/components/ai/ask-box.js +90 -54
  40. package/dist/components/ai/assistant.d.ts +13 -3
  41. package/dist/components/ai/assistant.js +36 -8
  42. package/dist/components/ai/code-block.d.ts +10 -3
  43. package/dist/components/ai/code-block.js +57 -8
  44. package/dist/components/ai/conversation-list.d.ts +13 -3
  45. package/dist/components/ai/conversation-list.js +15 -26
  46. package/dist/components/ai/markdown.d.ts +13 -0
  47. package/dist/components/ai/markdown.js +41 -0
  48. package/dist/components/auth/auth.d.ts +41 -5
  49. package/dist/components/auth/auth.js +167 -159
  50. package/dist/components/data/agenda-row.d.ts +26 -0
  51. package/dist/components/data/agenda-row.js +62 -0
  52. package/dist/components/data/day-strip.d.ts +27 -0
  53. package/dist/components/data/day-strip.js +78 -0
  54. package/dist/components/data/sectioned-list.d.ts +21 -6
  55. package/dist/components/data/sectioned-list.js +44 -21
  56. package/dist/components/data/task-row.d.ts +3 -0
  57. package/dist/components/data/task-row.js +6 -14
  58. package/dist/components/layout/collapse.js +1 -1
  59. package/dist/components/layout/text.d.ts +3 -3
  60. package/dist/components/layout/text.js +6 -1
  61. package/dist/components/ui/avatar.js +1 -1
  62. package/dist/components/ui/button.js +1 -1
  63. package/dist/components/ui/check-circle.js +1 -1
  64. package/dist/components/ui/code-block.js +2 -2
  65. package/dist/components/ui/combobox.js +1 -0
  66. package/dist/components/ui/input-group.js +1 -1
  67. package/dist/components/ui/input.js +1 -1
  68. package/dist/components/ui/kbd.js +1 -1
  69. package/dist/components/ui/password-input.d.ts +8 -0
  70. package/dist/components/ui/password-input.js +32 -0
  71. package/dist/components/ui/sonner.d.ts +2 -2
  72. package/dist/components/ui/sonner.js +1 -1
  73. package/dist/components/ui/toggle.js +1 -1
  74. package/dist/index.d.ts +23 -7
  75. package/dist/index.js +21 -5
  76. package/dist/lib/internal/date.js +8 -0
  77. package/dist/lib/{toast-clearance.js → internal/toast-clearance.js} +1 -1
  78. package/dist/styles.css +31 -1
  79. package/docs/COMPONENTS.md +302 -28
  80. package/docs/ROADMAP.md +13 -11
  81. package/llms.txt +10 -8
  82. package/package.json +3 -1
  83. package/skills/harebear-ui/SKILL.md +5 -3
@@ -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
- setPending(false);
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]:underline", "[&_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),
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 by a timestamp (ms or Date),
44
- * newest first, and drops empty groups. Pass `now` for deterministic tests.
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, now?: number | Date): ConversationGroup[];
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 by a timestamp (ms or Date),
98
- * newest first, and drops empty groups. Pass `now` for deterministic tests.
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, now = Date.now()) {
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 [today, yesterday, weekAgo] = [
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
- groups[t >= today ? 0 : t >= yesterday ? 1 : t >= weekAgo ? 2 : 3]?.items.push(toConversation(item));
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, loading, error, className, ...props }: LoginFormProps): React.JSX.Element;
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
- onSubmit?: (values: SignupValues) => void | Promise<void>;
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, loading, error, className, ...props }: SignupFormProps): React.JSX.Element;
85
- export { AppleIcon, AuthLayout, GitHubIcon, GoogleIcon, LoginForm, SignupForm };
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 };