harebear-ui 0.10.0 → 0.12.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.
@@ -3,6 +3,7 @@ import { Button } from "../ui/button.js";
3
3
  import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerTitle } from "../ui/drawer.js";
4
4
  import { Sheet, SheetContent, SheetDescription, SheetTitle } from "../ui/sheet.js";
5
5
  import { useMediaQuery } from "../../hooks/use-media-query.js";
6
+ import { useToastClearance } from "../../lib/internal/toast-clearance.js";
6
7
  import { cn } from "../../lib/utils.js";
7
8
  import { jsx, jsxs } from "react/jsx-runtime";
8
9
  import { XIcon } from "lucide-react";
@@ -13,6 +14,8 @@ import { XIcon } from "lucide-react";
13
14
  */
14
15
  function DetailSheet({ open, onOpenChange, title, description, footer, className, children }) {
15
16
  const desktop = useMediaQuery("(min-width: 768px)");
17
+ useToastClearance("--hb-toast-sheet", "calc(min(420px, 100vw - 32px) + 1rem)", open && desktop);
18
+ useToastClearance("--hb-toast-drawer", footer ? "calc(3.5625rem + max(1rem, env(safe-area-inset-bottom)))" : "env(safe-area-inset-bottom, 0px)", open && !desktop);
16
19
  const noDescription = description ? {} : { "aria-describedby": void 0 };
17
20
  const body = (padBottom) => /* @__PURE__ */ jsx("div", {
18
21
  className: cn("min-h-0 flex-1 overflow-y-auto px-6 pb-6", !footer && padBottom),
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  export interface ActionReceiptProps extends Omit<React.ComponentProps<"div">, "title"> {
3
3
  /** What was done, past tense: "Added milk to Groceries". */
4
4
  title: React.ReactNode;
5
- /** One line, e.g. "Groceries · just now". */
5
+ /** Short line under the title, e.g. "Groceries · just now". Wraps when long. */
6
6
  meta?: React.ReactNode;
7
7
  /** Shows an Undo button. */
8
8
  onUndo?: () => void;
@@ -26,7 +26,7 @@ function ActionReceipt({ title, meta, onUndo, undoLabel = "Undo", undone, classN
26
26
  className: cn("text-sm font-medium wrap-anywhere", undone && "text-muted-foreground line-through"),
27
27
  children: title
28
28
  }), meta && /* @__PURE__ */ jsx("span", {
29
- className: "truncate text-xs text-muted-foreground",
29
+ className: "text-xs text-muted-foreground wrap-anywhere",
30
30
  children: meta
31
31
  })]
32
32
  }),
@@ -27,6 +27,6 @@ export interface ApprovalResultProps extends React.ComponentProps<"div"> {
27
27
  /** Trailing time, e.g. "10:42". */
28
28
  time?: React.ReactNode;
29
29
  }
30
- /** One-line record of a decision in the thread, e.g. "You approved `docker restart x`, once". */
30
+ /** Short record of a decision in the thread, e.g. "You approved `docker restart x`, once". */
31
31
  declare function ApprovalResult({ decision, time, className, children, ...props }: ApprovalResultProps): React.JSX.Element;
32
32
  export { ApprovalCard, ApprovalResult };
@@ -56,7 +56,7 @@ function ApprovalCard({ title, description, icon = /* @__PURE__ */ jsx(ShieldAle
56
56
  ]
57
57
  });
58
58
  }
59
- /** One-line record of a decision in the thread, e.g. "You approved `docker restart x`, once". */
59
+ /** Short record of a decision in the thread, e.g. "You approved `docker restart x`, once". */
60
60
  function ApprovalResult({ decision, time, className, children, ...props }) {
61
61
  const denied = decision === "denied";
62
62
  return /* @__PURE__ */ jsxs("div", {
@@ -71,7 +71,7 @@ function ApprovalResult({ decision, time, className, children, ...props }) {
71
71
  children: denied ? /* @__PURE__ */ jsx(XIcon, {}) : /* @__PURE__ */ jsx(CheckIcon, {})
72
72
  }),
73
73
  /* @__PURE__ */ jsx("span", {
74
- className: "min-w-0 flex-1 truncate",
74
+ className: "min-w-0 flex-1 wrap-anywhere",
75
75
  children
76
76
  }),
77
77
  time && /* @__PURE__ */ jsx("span", {
@@ -18,8 +18,13 @@ export interface AskBoxProps extends Omit<React.ComponentProps<"form">, "onSubmi
18
18
  lang?: string;
19
19
  disabled?: boolean;
20
20
  autoFocus?: boolean;
21
- /** The assistant is working: sending is blocked and, with `onStop`, the send button becomes Stop. */
21
+ /** The assistant is working: sending is blocked (unless `queue`) and, with `onStop`, the send button becomes Stop. */
22
22
  pending?: boolean;
23
+ /**
24
+ * Allow sending while `pending`, so the app can queue a follow-up. Enter submits and the send button shows beside
25
+ * Stop. Default false.
26
+ */
27
+ queue?: boolean;
23
28
  /** Stops the assistant. Shown as a Stop button while `pending`. */
24
29
  onStop?: () => void;
25
30
  /**
@@ -33,7 +38,7 @@ export interface AskBoxProps extends Omit<React.ComponentProps<"form">, "onSubmi
33
38
  /**
34
39
  * The large rounded input an assistant starts from: auto-growing text, chips while typing, suggestion pills and the
35
40
  * DictationButton (mic, stop, send). The focus ring is on the whole box. With `reply` the answer shows under the box;
36
- * with `pending` and `onStop` the send button becomes Stop.
41
+ * with `pending` and `onStop` the send button becomes Stop (with `queue`, send stays beside it).
37
42
  */
38
- declare function AskBox({ value, onValueChange, onSubmit, placeholder, chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, pending, onStop, reply, chatHref, className, ...props }: AskBoxProps): React.JSX.Element;
43
+ declare function AskBox({ value, onValueChange, onSubmit, placeholder, chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, pending, queue, onStop, reply, chatHref, className, ...props }: AskBoxProps): React.JSX.Element;
39
44
  export { AskBox };
@@ -14,13 +14,13 @@ const noSpeech = {
14
14
  /**
15
15
  * The large rounded input an assistant starts from: auto-growing text, chips while typing, suggestion pills and the
16
16
  * DictationButton (mic, stop, send). The focus ring is on the whole box. With `reply` the answer shows under the box;
17
- * with `pending` and `onStop` the send button becomes Stop.
17
+ * with `pending` and `onStop` the send button becomes Stop (with `queue`, send stays beside it).
18
18
  */
19
- function AskBox({ value, onValueChange, onSubmit, placeholder = "Ask anything", chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, pending, onStop, reply, chatHref, className, ...props }) {
19
+ function AskBox({ value, onValueChange, onSubmit, placeholder = "Ask anything", chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, pending, queue, onStop, reply, chatHref, className, ...props }) {
20
20
  const id = React.useId();
21
21
  const submit = () => {
22
22
  const text = value.trim();
23
- if (!text || disabled || pending) return;
23
+ if (!text || disabled || pending && !queue) return;
24
24
  onSubmit(text);
25
25
  document.getElementById(id)?.focus();
26
26
  };
@@ -62,7 +62,7 @@ function AskBox({ value, onValueChange, onSubmit, placeholder = "Ask anything",
62
62
  },
63
63
  className: "field-sizing-content max-h-48 min-h-10 min-w-0 flex-1 resize-none border-0 bg-transparent py-2 text-base leading-6 text-foreground outline-none placeholder:text-muted-foreground"
64
64
  }),
65
- pending && onStop ? /* @__PURE__ */ jsx(Button, {
65
+ pending && onStop && /* @__PURE__ */ jsx(Button, {
66
66
  type: "button",
67
67
  size: "icon",
68
68
  shape: "pill",
@@ -73,9 +73,10 @@ function AskBox({ value, onValueChange, onSubmit, placeholder = "Ask anything",
73
73
  document.getElementById(id)?.focus();
74
74
  },
75
75
  children: /* @__PURE__ */ jsx(SquareIcon, { className: "size-3.5 fill-current" })
76
- }) : /* @__PURE__ */ jsx(DictationButton, {
76
+ }),
77
+ (!pending || !onStop || queue) && /* @__PURE__ */ jsx(DictationButton, {
77
78
  value,
78
- disabled: disabled || pending,
79
+ disabled: disabled || pending && !queue,
79
80
  source: dictation === false ? noSpeech : dictation,
80
81
  lang,
81
82
  onTranscript: (text) => onValueChange(value ? `${value.replace(/\s+$/, "")} ${text}` : text)
@@ -91,7 +92,7 @@ function AskBox({ value, onValueChange, onSubmit, placeholder = "Ask anything",
91
92
  "data-slot": "ask-box-reply",
92
93
  "aria-live": "polite",
93
94
  "aria-busy": pending || void 0,
94
- className: "flex min-w-0 flex-col gap-3 px-2 text-[15px] leading-relaxed",
95
+ className: "flex min-w-0 flex-col gap-3 px-2 text-[15px] leading-relaxed wrap-anywhere",
95
96
  children: [hasReply ? reply : /* @__PURE__ */ jsx(TypingIndicator, { label: "Working" }), chatHref && /* @__PURE__ */ jsx(Button, {
96
97
  asChild: true,
97
98
  variant: "link",
@@ -25,7 +25,12 @@ export interface AssistantSendContext {
25
25
  }) => string;
26
26
  /** Replace a message's content, e.g. while a reply streams in. */
27
27
  update: (id: string, content: React.ReactNode) => void;
28
- /** Aborted when the user presses Stop. Pass it to `fetch`, or listen for `abort` to tell your server to stop. */
28
+ /**
29
+ * Aborted when the user presses Stop (`signal.reason` is "stop") or when the next send starts (`signal.reason` is
30
+ * "superseded"). Pass it to `fetch`, or listen for `abort` and tell your server to stop only on "stop". After either
31
+ * abort, this send's return value and later `append`/`update` calls are dropped; on "superseded", show the rest of
32
+ * the run through the new send.
33
+ */
29
34
  signal: AbortSignal;
30
35
  }
31
36
  export interface AssistantSendOptions {
@@ -45,7 +50,7 @@ export interface AssistantContextValue {
45
50
  setPage: (page: AssistantPage | null) => void;
46
51
  /** Adds the user message, opens the dock (unless `{ open: false }`) and runs the app's onSend. */
47
52
  send: (text: string, options?: AssistantSendOptions) => Promise<void>;
48
- /** Stops the running send: aborts its `signal`, ends `pending` and drops its return value and later `append`/`update` calls. */
53
+ /** Stops the running send: aborts its `signal` with reason "stop", ends `pending` and drops its return value and later `append`/`update` calls. */
49
54
  stop: () => void;
50
55
  append: AssistantSendContext["append"];
51
56
  update: AssistantSendContext["update"];
@@ -80,6 +85,8 @@ export interface AssistantDockProps extends Omit<React.ComponentProps<"section">
80
85
  icon?: React.ReactNode;
81
86
  /** Link to the full-screen chat. Shows an expand button in the header. */
82
87
  fullScreenHref?: string;
88
+ /** Header controls before the full-screen link and close button, e.g. a "New chat" icon button. */
89
+ actions?: React.ReactNode;
83
90
  /** Empty state heading. Default "How can I help?". */
84
91
  emptyTitle?: React.ReactNode;
85
92
  /** Empty state line under the heading. */
@@ -94,11 +101,16 @@ export interface AssistantDockProps extends Omit<React.ComponentProps<"section">
94
101
  launcher?: boolean;
95
102
  /** Label of the launcher. Default `Ask ${title}`. */
96
103
  launcherLabel?: string;
104
+ /**
105
+ * Allow sending while the assistant works, so the app can queue a follow-up. The new send aborts the running
106
+ * one's `signal` with reason "superseded", not "stop". Stop stays available. Default false.
107
+ */
108
+ queue?: boolean;
97
109
  }
98
110
  /**
99
111
  * The assistant as a floating panel over the page (it never reflows the page): header, empty state with
100
112
  * suggestions, message list, AskBox. A soft launcher button opens it. Escape closes it. Needs AssistantProvider.
101
113
  * Sits above AppNav's phone tab bar.
102
114
  */
103
- declare function AssistantDock({ title, icon, fullScreenHref, emptyTitle, emptyDescription, suggestions, placeholder, dictation, launcher, launcherLabel, className, ...props }: AssistantDockProps): React.JSX.Element | null;
115
+ declare function AssistantDock({ title, icon, fullScreenHref, actions, emptyTitle, emptyDescription, suggestions, placeholder, dictation, launcher, launcherLabel, queue, className, ...props }: AssistantDockProps): React.JSX.Element | null;
104
116
  export { AssistantDock, AssistantProvider };
@@ -43,7 +43,7 @@ function AssistantProvider({ onSend, initialMessages = [], defaultOpen = false,
43
43
  }, []);
44
44
  const send = React.useCallback(async (text, options) => {
45
45
  const before = messagesRef.current;
46
- runRef.current?.abort();
46
+ runRef.current?.abort("superseded");
47
47
  const run = new AbortController();
48
48
  runRef.current = run;
49
49
  append({
@@ -81,7 +81,7 @@ function AssistantProvider({ onSend, initialMessages = [], defaultOpen = false,
81
81
  }
82
82
  }, [append, update]);
83
83
  const stop = React.useCallback(() => {
84
- runRef.current?.abort();
84
+ runRef.current?.abort("stop");
85
85
  runRef.current = null;
86
86
  setPending(false);
87
87
  }, []);
@@ -144,7 +144,7 @@ function useAssistantPage(page) {
144
144
  * suggestions, message list, AskBox. A soft launcher button opens it. Escape closes it. Needs AssistantProvider.
145
145
  * Sits above AppNav's phone tab bar.
146
146
  */
147
- function AssistantDock({ title = "Assistant", icon = /* @__PURE__ */ jsx(SparklesIcon, {}), fullScreenHref, emptyTitle = "How can I help?", emptyDescription, suggestions, placeholder, dictation, launcher = true, launcherLabel, className, ...props }) {
147
+ function AssistantDock({ title = "Assistant", icon = /* @__PURE__ */ jsx(SparklesIcon, {}), fullScreenHref, actions, emptyTitle = "How can I help?", emptyDescription, suggestions, placeholder, dictation, launcher = true, launcherLabel, queue, className, ...props }) {
148
148
  const { open, setOpen, messages, pending, page, send, stop } = useAssistant();
149
149
  const [draft, setDraft] = React.useState("");
150
150
  const titleId = React.useId();
@@ -158,7 +158,7 @@ function AssistantDock({ title = "Assistant", icon = /* @__PURE__ */ jsx(Sparkle
158
158
  wasOpen.current = open;
159
159
  }, [open]);
160
160
  const submit = (text) => {
161
- if (pending) return;
161
+ if (pending && !queue) return;
162
162
  setDraft("");
163
163
  panelRef.current?.querySelector("textarea")?.focus();
164
164
  send(text);
@@ -200,6 +200,7 @@ function AssistantDock({ title = "Assistant", icon = /* @__PURE__ */ jsx(Sparkle
200
200
  className: "min-w-0 flex-1 truncate text-[15px] font-semibold",
201
201
  children: title
202
202
  }),
203
+ actions,
203
204
  fullScreenHref && /* @__PURE__ */ jsx(Button, {
204
205
  asChild: true,
205
206
  variant: "ghost",
@@ -250,6 +251,7 @@ function AssistantDock({ title = "Assistant", icon = /* @__PURE__ */ jsx(Sparkle
250
251
  suggestions: messages.length === 0 ? pills : void 0,
251
252
  dictation,
252
253
  pending,
254
+ queue,
253
255
  onStop: stop
254
256
  })
255
257
  })
@@ -59,7 +59,7 @@ function ChatMessage({ role, avatar, actions, variant = "muted", className, chil
59
59
  className: cn("flex items-start gap-4", className),
60
60
  ...props,
61
61
  children: [avatar === false ? null : avatar ?? /* @__PURE__ */ jsx(AssistantAvatar, {}), /* @__PURE__ */ jsxs("div", {
62
- className: "flex min-w-0 flex-1 flex-col gap-3.5",
62
+ className: "flex min-w-0 flex-1 flex-col gap-3.5 wrap-anywhere",
63
63
  children: [children, actions]
64
64
  })]
65
65
  });
@@ -6,8 +6,15 @@ export interface PromptInputProps extends Omit<React.ComponentProps<"form">, "on
6
6
  /** Called with the trimmed text on Enter or the send button. Shift+Enter inserts a newline. */
7
7
  onSubmit: (value: string) => void;
8
8
  placeholder?: string;
9
- /** While true, the send button becomes a stop button. */
9
+ /** The text field's accessible name. Default the placeholder. */
10
+ label?: string;
11
+ /** While true, the send button becomes a stop button (with `queue`, send stays beside it). */
10
12
  loading?: boolean;
13
+ /**
14
+ * Allow sending while `loading`, so the app can queue a follow-up. Enter submits and a send button shows beside
15
+ * Stop. Default false: sending waits until `loading` ends.
16
+ */
17
+ queue?: boolean;
11
18
  onStop?: () => void;
12
19
  /** Shows a paperclip button. */
13
20
  onAttach?: () => void;
@@ -18,8 +25,8 @@ export interface PromptInputProps extends Omit<React.ComponentProps<"form">, "on
18
25
  disabled?: boolean;
19
26
  autoFocus?: boolean;
20
27
  }
21
- /** Chat composer: auto-growing textarea with attach, tool chips, and send/stop. */
22
- declare function PromptInput({ value, onValueChange, onSubmit, placeholder, loading, onStop, onAttach, tools, footer, disabled, autoFocus, className, ...props }: PromptInputProps): React.JSX.Element;
28
+ /** Chat composer: auto-growing textarea with attach, tool chips, and send/stop. With `queue`, follow-ups can be sent while loading. */
29
+ declare function PromptInput({ value, onValueChange, onSubmit, placeholder, label, loading, queue, onStop, onAttach, tools, footer, disabled, autoFocus, className, ...props }: PromptInputProps): React.JSX.Element;
23
30
  export interface PromptInputChipProps extends React.ComponentProps<typeof Button> {
24
31
  active?: boolean;
25
32
  /** "warning" outlines the chip in the warning color, e.g. for a risky mode that is on. */
@@ -6,12 +6,12 @@ import * as React from "react";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
7
  import { ArrowUpIcon, ChevronDownIcon, PaperclipIcon, SquareIcon } from "lucide-react";
8
8
  // src/components/ai/prompt-input.tsx
9
- /** Chat composer: auto-growing textarea with attach, tool chips, and send/stop. */
10
- function PromptInput({ value, onValueChange, onSubmit, placeholder = "Send a message…", loading, onStop, onAttach, tools, footer, disabled, autoFocus, className, ...props }) {
9
+ /** Chat composer: auto-growing textarea with attach, tool chips, and send/stop. With `queue`, follow-ups can be sent while loading. */
10
+ function PromptInput({ value, onValueChange, onSubmit, placeholder = "Send a message…", label = placeholder, loading, queue, onStop, onAttach, tools, footer, disabled, autoFocus, className, ...props }) {
11
11
  const id = React.useId();
12
12
  const canSend = value.trim().length > 0 && !disabled;
13
13
  const submit = () => {
14
- if (canSend && !loading) onSubmit(value.trim());
14
+ if (canSend && (!loading || queue)) onSubmit(value.trim());
15
15
  };
16
16
  return /* @__PURE__ */ jsxs("form", {
17
17
  "data-slot": "prompt-input",
@@ -27,7 +27,7 @@ function PromptInput({ value, onValueChange, onSubmit, placeholder = "Send a mes
27
27
  /* @__PURE__ */ jsx("label", {
28
28
  htmlFor: id,
29
29
  className: "sr-only",
30
- children: "Message"
30
+ children: label
31
31
  }),
32
32
  /* @__PURE__ */ jsx("textarea", {
33
33
  id,
@@ -59,7 +59,7 @@ function PromptInput({ value, onValueChange, onSubmit, placeholder = "Send a mes
59
59
  }),
60
60
  tools,
61
61
  /* @__PURE__ */ jsx("div", { className: "flex-1" }),
62
- loading && onStop ? /* @__PURE__ */ jsx(Button, {
62
+ loading && onStop && /* @__PURE__ */ jsx(Button, {
63
63
  type: "button",
64
64
  size: "icon",
65
65
  shape: "pill",
@@ -67,13 +67,14 @@ function PromptInput({ value, onValueChange, onSubmit, placeholder = "Send a mes
67
67
  "aria-label": "Stop generating",
68
68
  onClick: onStop,
69
69
  children: /* @__PURE__ */ jsx(SquareIcon, { className: "fill-current" })
70
- }) : /* @__PURE__ */ jsx(Button, {
70
+ }),
71
+ (!loading || !onStop || queue) && /* @__PURE__ */ jsx(Button, {
71
72
  type: "submit",
72
73
  size: "icon",
73
74
  shape: "pill",
74
75
  className: "size-9",
75
76
  "aria-label": "Send message",
76
- disabled: !canSend || loading,
77
+ disabled: !canSend || loading && !queue,
77
78
  children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
78
79
  })
79
80
  ]
@@ -24,9 +24,9 @@ declare function ToolGroup({ title, status, meta, autoOpen, open: openProp, onOp
24
24
  export interface ToolStepProps extends Omit<React.ComponentProps<"div">, "title"> {
25
25
  /** Tool icon, e.g. <TerminalIcon />. */
26
26
  icon?: React.ReactNode;
27
- /** Short verb column, e.g. "Run", "Read", "Edit". */
27
+ /** Short verb column, e.g. "Run", "Read", "Edit". With an empty `detail` it is the whole label, e.g. "Added milk". */
28
28
  kind: React.ReactNode;
29
- /** What it acted on, shown monospace: the command or path. */
29
+ /** What it acted on, shown monospace: the command or path. Empty for a plain-language step. */
30
30
  detail: string;
31
31
  status: ToolStatus;
32
32
  /** Trailing text. Default derived from status ("running", "failed"…). */
@@ -52,7 +52,7 @@ function ToolGroup({ title, status, meta, autoOpen, open: openProp, onOpenChange
52
52
  children: [
53
53
  /* @__PURE__ */ jsx(ChevronRightIcon, { className: cn("size-4 shrink-0 text-muted-foreground transition-transform", open && "rotate-90") }),
54
54
  /* @__PURE__ */ jsx("span", {
55
- className: "min-w-0 flex-1 truncate font-medium",
55
+ className: "min-w-0 flex-1 font-medium wrap-anywhere",
56
56
  children: title
57
57
  }),
58
58
  meta !== void 0 && /* @__PURE__ */ jsx("span", {
@@ -85,11 +85,12 @@ function ToolStep({ icon, kind, detail, status, meta, output, autoOpen, classNam
85
85
  children: icon
86
86
  }),
87
87
  /* @__PURE__ */ jsx("span", {
88
- className: "w-18 shrink-0 truncate text-[13px] text-muted-foreground",
88
+ "data-slot": "tool-step-kind",
89
+ className: cn("text-[13px] text-muted-foreground wrap-anywhere", detail ? "w-18 shrink-0" : "min-w-0 flex-1 text-foreground"),
89
90
  children: kind
90
91
  }),
91
- /* @__PURE__ */ jsx("span", {
92
- className: "min-w-0 flex-1 truncate font-mono text-[12.5px]",
92
+ detail && /* @__PURE__ */ jsx("span", {
93
+ className: "min-w-0 flex-1 font-mono text-[12.5px] wrap-anywhere",
93
94
  children: detail
94
95
  }),
95
96
  /* @__PURE__ */ jsx("span", {
@@ -21,7 +21,7 @@ function AvatarImage({ className, ...props }) {
21
21
  function AvatarFallback({ className, ...props }) {
22
22
  return /* @__PURE__ */ jsx(Avatar$1.Fallback, {
23
23
  "data-slot": "avatar-fallback",
24
- className: cn("flex size-full items-center justify-center rounded-full bg-primary-soft text-[13px] font-semibold text-primary group-data-[size=sm]/avatar:text-xs group-data-[size=lg]/avatar:text-lg", className),
24
+ className: cn("flex size-full items-center justify-center rounded-full bg-primary-soft text-[13px] font-semibold text-primary-soft-foreground group-data-[size=sm]/avatar:text-xs group-data-[size=lg]/avatar:text-lg", className),
25
25
  ...props
26
26
  });
27
27
  }
@@ -9,7 +9,7 @@ const badgeVariants = cva("group/badge inline-flex h-6 w-fit shrink-0 items-cent
9
9
  variant: {
10
10
  default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
11
11
  secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
12
- soft: "bg-primary-soft text-primary",
12
+ soft: "bg-primary-soft text-primary-soft-foreground",
13
13
  success: "bg-success/12 text-success",
14
14
  warning: "bg-warning/12 text-warning",
15
15
  destructive: "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
@@ -12,7 +12,7 @@ const buttonVariants = cva("group/button relative inline-flex shrink-0 items-cen
12
12
  secondary: "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklab,var(--secondary),var(--foreground)_6%)] aria-expanded:bg-[color-mix(in_oklab,var(--secondary),var(--foreground)_6%)]",
13
13
  outline: "border-border bg-card text-foreground hover:bg-muted aria-expanded:bg-muted",
14
14
  ghost: "text-muted-foreground hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground",
15
- soft: "bg-primary-soft text-primary hover:bg-[color-mix(in_oklab,var(--primary-soft),var(--primary)_8%)]",
15
+ soft: "bg-primary-soft text-primary-soft-foreground hover:bg-[color-mix(in_oklab,var(--primary-soft),var(--primary)_8%)]",
16
16
  dashed: "border-dashed border-border bg-transparent text-muted-foreground hover:border-muted-foreground hover:text-foreground",
17
17
  destructive: "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30",
18
18
  link: "text-primary underline-offset-4 hover:underline"
@@ -5,7 +5,7 @@ declare function DialogTrigger({ ...props }: React.ComponentProps<typeof Dialog$
5
5
  declare function DialogPortal({ ...props }: React.ComponentProps<typeof Dialog$1.Portal>): React.JSX.Element;
6
6
  declare function DialogClose({ ...props }: React.ComponentProps<typeof Dialog$1.Close>): React.JSX.Element;
7
7
  declare function DialogOverlay({ className, ...props }: React.ComponentProps<typeof Dialog$1.Overlay>): React.JSX.Element;
8
- declare function DialogContent({ className, children, showCloseButton, ...props }: React.ComponentProps<typeof Dialog$1.Content> & {
8
+ declare function DialogContent({ className, children, showCloseButton, onInteractOutside, ...props }: React.ComponentProps<typeof Dialog$1.Content> & {
9
9
  showCloseButton?: boolean;
10
10
  }): React.JSX.Element;
11
11
  declare function DialogHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ import { keepOpenOnToast } from "../../lib/internal/toast-clearance.js";
2
3
  import { cn } from "../../lib/utils.js";
3
4
  import { Button } from "./button.js";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -36,11 +37,12 @@ function DialogOverlay({ className, ...props }) {
36
37
  ...props
37
38
  });
38
39
  }
39
- function DialogContent({ className, children, showCloseButton = true, ...props }) {
40
+ function DialogContent({ className, children, showCloseButton = true, onInteractOutside, ...props }) {
40
41
  return /* @__PURE__ */ jsxs(DialogPortal, { children: [/* @__PURE__ */ jsx(DialogOverlay, {}), /* @__PURE__ */ jsxs(Dialog$1.Content, {
41
42
  "data-slot": "dialog-content",
42
43
  className: cn("fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className),
43
44
  ...props,
45
+ onInteractOutside: keepOpenOnToast(onInteractOutside),
44
46
  children: [children, showCloseButton && /* @__PURE__ */ jsx(Dialog$1.Close, {
45
47
  "data-slot": "dialog-close",
46
48
  asChild: true,
@@ -5,7 +5,7 @@ declare function DrawerTrigger({ ...props }: React.ComponentProps<typeof Drawer$
5
5
  declare function DrawerPortal({ ...props }: React.ComponentProps<typeof Drawer$1.Portal>): React.JSX.Element;
6
6
  declare function DrawerClose({ ...props }: React.ComponentProps<typeof Drawer$1.Close>): React.JSX.Element;
7
7
  declare function DrawerOverlay({ className, ...props }: React.ComponentProps<typeof Drawer$1.Overlay>): React.JSX.Element;
8
- declare function DrawerContent({ className, children, ...props }: React.ComponentProps<typeof Drawer$1.Content>): React.JSX.Element;
8
+ declare function DrawerContent({ className, children, onInteractOutside, ...props }: React.ComponentProps<typeof Drawer$1.Content>): React.JSX.Element;
9
9
  declare function DrawerHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
10
10
  declare function DrawerFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
11
11
  declare function DrawerTitle({ className, ...props }: React.ComponentProps<typeof Drawer$1.Title>): React.JSX.Element;
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ import { keepOpenOnToast } from "../../lib/internal/toast-clearance.js";
2
3
  import { cn } from "../../lib/utils.js";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
  import { Drawer as Drawer$1 } from "vaul";
@@ -34,13 +35,14 @@ function DrawerOverlay({ className, ...props }) {
34
35
  ...props
35
36
  });
36
37
  }
37
- function DrawerContent({ className, children, ...props }) {
38
+ function DrawerContent({ className, children, onInteractOutside, ...props }) {
38
39
  return /* @__PURE__ */ jsxs(DrawerPortal, {
39
40
  "data-slot": "drawer-portal",
40
41
  children: [/* @__PURE__ */ jsx(DrawerOverlay, {}), /* @__PURE__ */ jsxs(Drawer$1.Content, {
41
42
  "data-slot": "drawer-content",
42
43
  className: cn("group/drawer-content fixed z-50 flex h-auto flex-col bg-popover text-sm text-popover-foreground data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-xl data-[vaul-drawer-direction=bottom]:border-t data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:rounded-r-xl data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:rounded-l-xl data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-xl data-[vaul-drawer-direction=top]:border-b data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm", className),
43
44
  ...props,
45
+ onInteractOutside: keepOpenOnToast(onInteractOutside),
44
46
  children: [/* @__PURE__ */ jsx("div", { className: "mx-auto mt-4 hidden h-1 w-[100px] shrink-0 rounded-full bg-muted group-data-[vaul-drawer-direction=bottom]/drawer-content:block" }), children]
45
47
  })]
46
48
  });
@@ -3,7 +3,7 @@ import { Dialog } from "radix-ui";
3
3
  declare function Sheet({ ...props }: React.ComponentProps<typeof Dialog.Root>): React.JSX.Element;
4
4
  declare function SheetTrigger({ ...props }: React.ComponentProps<typeof Dialog.Trigger>): React.JSX.Element;
5
5
  declare function SheetClose({ ...props }: React.ComponentProps<typeof Dialog.Close>): React.JSX.Element;
6
- declare function SheetContent({ className, children, side, showCloseButton, ...props }: React.ComponentProps<typeof Dialog.Content> & {
6
+ declare function SheetContent({ className, children, side, showCloseButton, onInteractOutside, ...props }: React.ComponentProps<typeof Dialog.Content> & {
7
7
  side?: "top" | "right" | "bottom" | "left";
8
8
  showCloseButton?: boolean;
9
9
  }): React.JSX.Element;
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ import { keepOpenOnToast } from "../../lib/internal/toast-clearance.js";
2
3
  import { cn } from "../../lib/utils.js";
3
4
  import { Button } from "./button.js";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -36,12 +37,13 @@ function SheetOverlay({ className, ...props }) {
36
37
  ...props
37
38
  });
38
39
  }
39
- function SheetContent({ className, children, side = "right", showCloseButton = true, ...props }) {
40
+ function SheetContent({ className, children, side = "right", showCloseButton = true, onInteractOutside, ...props }) {
40
41
  return /* @__PURE__ */ jsxs(SheetPortal, { children: [/* @__PURE__ */ jsx(SheetOverlay, {}), /* @__PURE__ */ jsxs(Dialog.Content, {
41
42
  "data-slot": "sheet-content",
42
43
  "data-side": side,
43
44
  className: cn("fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10", className),
44
45
  ...props,
46
+ onInteractOutside: keepOpenOnToast(onInteractOutside),
45
47
  children: [children, showCloseButton && /* @__PURE__ */ jsx(Dialog.Close, {
46
48
  "data-slot": "sheet-close",
47
49
  asChild: true,
@@ -20,11 +20,12 @@ const Toaster = ({ ...props }) => {
20
20
  "--normal-bg": "var(--popover)",
21
21
  "--normal-text": "var(--popover-foreground)",
22
22
  "--normal-border": "var(--border)",
23
- "--border-radius": "var(--radius)"
23
+ "--border-radius": "var(--radius)",
24
+ pointerEvents: "auto"
24
25
  },
25
26
  toastOptions: { classNames: {
26
27
  toast: "cn-toast",
27
- actionButton: "bg-primary-soft! text-primary! font-medium! h-8! px-3! rounded-md!"
28
+ actionButton: "bg-primary-soft! text-primary-soft-foreground! font-medium! h-8! px-3! rounded-md!"
28
29
  } },
29
30
  ...props
30
31
  });
@@ -30,7 +30,7 @@ function TraceIcon({ className, ...props }) {
30
30
  function TraceTitle({ className, ...props }) {
31
31
  return /* @__PURE__ */ jsx("span", {
32
32
  "data-slot": "trace-title",
33
- className: cn("min-w-0 truncate motion-reduce:shimmer-none", className),
33
+ className: cn("min-w-0 wrap-anywhere motion-reduce:shimmer-none", className),
34
34
  ...props
35
35
  });
36
36
  }
@@ -2,6 +2,16 @@
2
2
  import * as React from "react";
3
3
  // src/lib/internal/toast-clearance.ts
4
4
  const counts = /* @__PURE__ */ new Map();
5
+ /**
6
+ * For a modal's `onInteractOutside`: a press or focus inside the Toaster (e.g. a toast's Undo) does not dismiss the
7
+ * modal. The Toaster opts out of the modal's `pointer-events: none` on <body> (src/components/ui/sonner.tsx).
8
+ */
9
+ function keepOpenOnToast(handler) {
10
+ return (event) => {
11
+ handler?.(event);
12
+ if (event.target instanceof Element && event.target.closest("[data-sonner-toaster]")) event.preventDefault();
13
+ };
14
+ }
5
15
  function useToastClearance(name, value, enabled = true) {
6
16
  React.useEffect(() => {
7
17
  if (!enabled || typeof document === "undefined") return;
@@ -19,4 +29,4 @@ function useToastClearance(name, value, enabled = true) {
19
29
  enabled
20
30
  ]);
21
31
  }
22
- export { useToastClearance };
32
+ export { keepOpenOnToast, useToastClearance };
package/dist/styles.css CHANGED
@@ -34,6 +34,7 @@
34
34
  --color-primary: var(--primary);
35
35
  --color-primary-foreground: var(--primary-foreground);
36
36
  --color-primary-soft: var(--primary-soft);
37
+ --color-primary-soft-foreground: var(--primary-soft-foreground);
37
38
  --color-secondary: var(--secondary);
38
39
  --color-secondary-foreground: var(--secondary-foreground);
39
40
  --color-muted: var(--muted);
@@ -96,6 +97,7 @@
96
97
  --primary: var(--brand);
97
98
  --primary-foreground: #ffffff;
98
99
  --primary-soft: color-mix(in oklab, var(--primary) 10%, var(--card));
100
+ --primary-soft-foreground: var(--primary);
99
101
  --secondary: #f1f1f3;
100
102
  --secondary-foreground: #18181b;
101
103
  --muted: #f1f1f3;
@@ -148,6 +150,8 @@
148
150
  --primary: color-mix(in oklab, var(--brand), white 26%);
149
151
  --primary-foreground: #0b1020;
150
152
  --primary-soft: color-mix(in oklab, var(--primary) 15%, var(--card));
153
+ /* Text on primary-soft: plain primary is 4.35:1 here with the default brand; this is 6.6:1. */
154
+ --primary-soft-foreground: color-mix(in oklab, var(--primary), white 30%);
151
155
  --secondary: #232326;
152
156
  --secondary-foreground: #ededef;
153
157
  --muted: #232326;
@@ -319,22 +323,37 @@
319
323
  /* Toasts clear floating controls. AppNav (tab bar, action button) and AssistantDock (launcher) set --hb-toast-* on
320
324
  <html> while mounted (src/lib/internal/toast-clearance.ts); this adds them to Sonner's bottom offset. Unlayered and
321
325
  !important because Sonner injects its own unlayered rule at runtime. Breakpoints: AppNav's tab bar shows under
322
- 768px; Sonner switches to full-width mobile toasts under 600px. */
326
+ 768px; Sonner switches to full-width mobile toasts under 600px.
327
+ An open DetailSheet sets --hb-toast-sheet (desktop side sheet: its width plus gap) or --hb-toast-drawer (phone
328
+ full-screen drawer: its footer height). The sheet moves toasts left of it and narrows them to fit; the drawer
329
+ replaces the other bottom offsets, since it covers the tab bar, action button and launcher. */
323
330
  [data-sonner-toaster][data-y-position="bottom"] {
324
331
  bottom: calc(var(--offset-bottom) + var(--hb-toast-launcher, 0px)) !important;
325
332
  }
333
+ @media (min-width: 768px) {
334
+ [data-sonner-toaster] {
335
+ --width: min(356px, 100vw - var(--hb-toast-sheet, 0px) - 2 * var(--offset-right)) !important;
336
+ }
337
+ [data-sonner-toaster][data-x-position="right"] {
338
+ right: calc(var(--offset-right) + var(--hb-toast-sheet, 0px)) !important;
339
+ }
340
+ [data-sonner-toaster][data-x-position="center"] {
341
+ transform: translateX(calc(-50% - var(--hb-toast-sheet, 0px) / 2)) !important;
342
+ }
343
+ }
326
344
  @media (max-width: 767px) {
327
345
  [data-sonner-toaster][data-y-position="bottom"] {
328
346
  bottom: calc(
329
- var(--offset-bottom) + var(--hb-toast-nav, 0px) + var(--hb-toast-fab, 0px) + var(--hb-toast-launcher, 0px)
347
+ var(--offset-bottom) +
348
+ var(--hb-toast-drawer, var(--hb-toast-nav, 0px) + var(--hb-toast-fab, 0px) + var(--hb-toast-launcher, 0px))
330
349
  ) !important;
331
350
  }
332
351
  }
333
352
  @media (max-width: 600px) {
334
353
  [data-sonner-toaster][data-y-position="bottom"] {
335
354
  bottom: calc(
336
- var(--mobile-offset-bottom) + var(--hb-toast-nav, 0px) + var(--hb-toast-fab, 0px) +
337
- var(--hb-toast-launcher, 0px)
355
+ var(--mobile-offset-bottom) +
356
+ var(--hb-toast-drawer, var(--hb-toast-nav, 0px) + var(--hb-toast-fab, 0px) + var(--hb-toast-launcher, 0px))
338
357
  ) !important;
339
358
  }
340
359
  }
@@ -373,7 +373,7 @@ Searchable IANA time zone field built on `Combobox`, listing `Intl.supportedValu
373
373
 
374
374
  ### `DetailSheet`
375
375
 
376
- A record's details, one API for both sizes: a floating side sheet from 768px up, a full-screen drawer with a close button on phones. Props: `open`, `onOpenChange`, `title` (heading and accessible name), `description?`, `footer?` (pinned under the scrolling body, e.g. Delete), `children` (the fields). `data-layout` is `"sheet"` or `"drawer"`.
376
+ A record's details, one API for both sizes: a floating side sheet from 768px up, a full-screen drawer with a close button on phones. Props: `open`, `onOpenChange`, `title` (heading and accessible name), `description?`, `footer?` (pinned under the scrolling body, e.g. Delete), `children` (the fields). `data-layout` is `"sheet"` or `"drawer"`. While open, toasts (e.g. `toastUndo`) stay off it: on desktop they move to the left of the sheet and narrow to fit; on phones they sit above the footer (one row of buttons) or the bottom safe area, in place of the hidden tab bar's space.
377
377
 
378
378
  ```tsx
379
379
  ;<DetailSheet
@@ -651,7 +651,7 @@ Scrollable log that sticks to the bottom as content streams in (unless the user
651
651
 
652
652
  ### `ChatMessage`
653
653
 
654
- One turn. Props: `role: "user" | "assistant"` (matches AI SDK message roles), `avatar?` (assistant only, defaults to `AssistantAvatar`; `false` for none and full-width content, for agent threads where one turn is many blocks), `actions?` (assistant only, under the content), `variant?` (user bubble: `"muted"` default or `"soft"` accent tint), `children`. Long words wrap.
654
+ One turn. Props: `role: "user" | "assistant"` (matches AI SDK message roles), `avatar?` (assistant only, defaults to `AssistantAvatar`; `false` for none and full-width content, for agent threads where one turn is many blocks), `actions?` (assistant only, under the content), `variant?` (user bubble: `"muted"` default or `"soft"` accent tint), `children`. Long words wrap, in assistant content too.
655
655
  User turns render as a right-aligned bubble with preserved newlines. Assistant turns render free-flowing: put `Prose`, `CodeBlock`, `ToolCall`, `TypingIndicator` inside.
656
656
 
657
657
  ```tsx
@@ -689,7 +689,7 @@ Row of ghost icon buttons. Props: `copyText?` (shows copy), `onRegenerate?`, `on
689
689
 
690
690
  ### `PromptInput`, `PromptInputChip`
691
691
 
692
- Composer. Props: `value`, `onValueChange`, `onSubmit(text)` (Enter sends trimmed text, Shift+Enter is a newline), `placeholder?`, `loading?` + `onStop?` (send becomes stop), `onAttach?` (shows paperclip), `tools?` (chips), `footer?` (disclaimer), `disabled?`, `autoFocus?`.
692
+ Composer. Props: `value`, `onValueChange`, `onSubmit(text)` (Enter sends trimmed text, Shift+Enter is a newline), `placeholder?`, `label?` (the field's accessible name, default the placeholder), `loading?` + `onStop?` (send becomes stop), `queue?` (Enter and send still work while `loading`, beside Stop, so the app can queue a follow-up), `onAttach?` (shows paperclip), `tools?` (chips), `footer?` (disclaimer), `disabled?`, `autoFocus?`.
693
693
  `PromptInputChip` is a pill toggle for the tools slot: `active`, `tone?` (`"warning"`), `onClick`, children (icon + label).
694
694
 
695
695
  ### `PromptInputSelect`
@@ -722,9 +722,9 @@ Three pulsing dots. Props: `label?` (text after the dots, e.g. "Working"), `vari
722
722
 
723
723
  ### `ToolGroup`, `ToolStep`, `ToolStatusIcon`
724
724
 
725
- Agent tool calls as a bordered, collapsible card. `ToolStatus = "queued" | "waiting" | "running" | "done" | "error" | "stopped"`.
725
+ Agent tool calls as a bordered, collapsible card. Titles, labels and details wrap instead of cutting off. `ToolStatus = "queued" | "waiting" | "running" | "done" | "error" | "stopped"`.
726
726
  `ToolGroup` props: `title`, `status`, `meta?` (e.g. time), `autoOpen?` (default open while active, until the user toggles), `open?`/`onOpenChange?`.
727
- `ToolStep` props: `icon?`, `kind` (verb column), `detail` (monospace command or path), `status`, `meta?` (default from status), `output?` (monospace, shown when expanded; omit for a fixed row), `autoOpen?` (default open while running).
727
+ `ToolStep` props: `icon?`, `kind` (verb column; with an empty `detail` it is the whole label, for plain-language steps such as "Added milk"), `detail` (monospace command or path, or `""`), `status`, `meta?` (default from status), `output?` (monospace, shown when expanded; omit for a fixed row), `autoOpen?` (default open while running).
728
728
  `ToolStatusIcon`: spinner while running, else a toned dot. Lighter alternative: `Trace`.
729
729
 
730
730
  ```tsx
@@ -736,19 +736,19 @@ Agent tool calls as a bordered, collapsible card. `ToolStatus = "queued" | "wait
736
736
  ### `ApprovalCard`, `ApprovalResult`
737
737
 
738
738
  `ApprovalCard`: warning-ringed card for anything the agent waits on the user for. Props: `title`, `description?`, `icon?` (default shield, `null` for none), `reason?` (shown first after a bold "Why:"), `code?` + `codeLabel?` (wrapped code block with copy), `actions?` (buttons), `footer?`, `children` (more body).
739
- `ApprovalResult`: one-line record in the thread. Props: `decision: "approved" | "denied"`, `time?`, `children`.
739
+ `ApprovalResult`: short record of a decision in the thread; wraps when long. Props: `decision: "approved" | "denied"`, `time?`, `children`.
740
740
 
741
741
  ### `AssistantProvider`, `useAssistant`, `useAssistantPage`
742
742
 
743
- One assistant conversation across pages. `AssistantProvider` props: `onSend(text, { page, messages, append, update, signal })` (return the reply, or `append`/`update` messages yourself for streaming and receipts; a thrown error becomes the reply; `signal` aborts when the user presses Stop, so pass it to `fetch` or listen for `abort` to tell your server), `initialMessages?`, `defaultOpen?`. `useAssistant()` returns `{ open, setOpen, toggle, messages, pending, reply, page, setPage, send, stop, append, update, clear }`: `send(text, { open: false }?)` skips opening the dock, `stop()` aborts the running send, ends `pending` and drops its late reply, `reply` is the assistant messages after the latest user message (null until there is one), for `AskBox`. A page calls `useAssistantPage({ id, title?, data?, suggestions? })` to become the context while mounted.
743
+ One assistant conversation across pages. `AssistantProvider` props: `onSend(text, { page, messages, append, update, signal })` (return the reply, or `append`/`update` messages yourself for streaming and receipts; a thrown error becomes the reply; `signal` aborts with `signal.reason` `"stop"` when the user presses Stop and `"superseded"` when the next send starts, so pass it to `fetch`, or listen for `abort` and tell your server to stop only on `"stop"`; after either abort that send's return value and later `append`/`update` calls are dropped, so on `"superseded"` show the rest of the run through the new send), `initialMessages?`, `defaultOpen?`. `useAssistant()` returns `{ open, setOpen, toggle, messages, pending, reply, page, setPage, send, stop, append, update, clear }`: `send(text, { open: false }?)` skips opening the dock, `stop()` aborts the running send (reason `"stop"`), ends `pending` and drops its late reply, `reply` is the assistant messages after the latest user message (null until there is one), for `AskBox`. A page calls `useAssistantPage({ id, title?, data?, suggestions? })` to become the context while mounted.
744
744
 
745
745
  ### `AssistantDock`
746
746
 
747
- The assistant as a floating panel over the page (never reflows it): header (icon, title, full-screen link, close), empty state with suggestions, thread, `AskBox` (Stop while the assistant works, wired to `useAssistant().stop`). Closed, it is a soft launcher button. Escape closes and returns focus to the launcher. Props: `title?` (default "Assistant"), `icon?`, `fullScreenHref?`, `emptyTitle?`, `emptyDescription?`, `suggestions?` (default: the page's), `placeholder?`, `dictation?`, `launcher?` (false when another control opens it), `launcherLabel?`.
747
+ The assistant as a floating panel over the page (never reflows it): header (icon, title, your `actions`, full-screen link, close), empty state with suggestions, thread, `AskBox` (Stop while the assistant works, wired to `useAssistant().stop`). Closed, it is a soft launcher button. Escape closes and returns focus to the launcher. Props: `title?` (default "Assistant"), `icon?`, `fullScreenHref?`, `actions?` (header controls before the full-screen link, e.g. a "New chat" icon button with an `aria-label`), `emptyTitle?`, `emptyDescription?`, `suggestions?` (default: the page's), `placeholder?`, `dictation?`, `launcher?` (false when another control opens it), `launcherLabel?`, `queue?` (send follow-ups while the assistant works; the running send's `signal` aborts with reason `"superseded"` and its later output is dropped, so the new send shows the rest; Stop stays).
748
748
 
749
749
  ### `AskBox`
750
750
 
751
- Large rounded composer: auto-growing text, `chips` shown while there is text, suggestion pills, `DictationButton`. The focus ring is on the box (`focus-box`). Props: `value`, `onValueChange`, `onSubmit(text)`, `placeholder?` (also the label, default "Ask anything"), `chips?`, `suggestions?`, `onSuggestion?` (default submits), `dictation?` (`SpeechSource`, or `false` to hide the mic), `lang?`, `disabled?`, `pending?` (blocks sending; with `onStop?` the send button becomes Stop), `reply?` (the answer under the box; passing it, even `null`, shows "Working" while `pending`), `chatHref?` (an "Open chat" link under the reply).
751
+ Large rounded composer: auto-growing text, `chips` shown while there is text, suggestion pills, `DictationButton`. The focus ring is on the box (`focus-box`). Props: `value`, `onValueChange`, `onSubmit(text)`, `placeholder?` (also the label, default "Ask anything"), `chips?`, `suggestions?`, `onSuggestion?` (default submits), `dictation?` (`SpeechSource`, or `false` to hide the mic), `lang?`, `disabled?`, `pending?` (blocks sending; with `onStop?` the send button becomes Stop), `queue?` (sending works while `pending`, with send beside Stop, so the app can queue a follow-up), `reply?` (the answer under the box; passing it, even `null`, shows "Working" while `pending`), `chatHref?` (an "Open chat" link under the reply).
752
752
 
753
753
  ```tsx
754
754
  // Inline answer under the box (no dock), e.g. a Today page
@@ -774,7 +774,7 @@ const { send, pending, stop, reply } = useAssistant()
774
774
 
775
775
  ### `ActionReceipt`
776
776
 
777
- What the assistant did, as a compact card: round check, `title` (past tense), `meta?`, Undo (`onUndo?`, `undoLabel?`). `undone` strikes it through and shows "Undone".
777
+ What the assistant did, as a compact card: round check, `title` (past tense), `meta?` (wraps when long), Undo (`onUndo?`, `undoLabel?`). `undone` strikes it through and shows "Undone".
778
778
 
779
779
  ### `CodeSnippet`, `InlineCode`, `Prose`, `ProseBlockquote`
780
780
 
@@ -884,20 +884,22 @@ Reset or first password: "New password" + "Confirm password" (a mismatch is bloc
884
884
 
885
885
  Use these as Tailwind utilities (`bg-*`, `text-*`, `border-*`, `ring-*`, `fill-*`, `stroke-*`):
886
886
 
887
- | Token | Use |
888
- | ----------------------------------- | ------------------------------------------------------------------------------------------------------- |
889
- | `background` / `foreground` | Page background and body text. |
890
- | `card` / `card-foreground` | Raised surfaces (cards, active nav item, popovers via `popover`). |
891
- | `muted` / `muted-foreground` | Inputs, segmented controls, quiet panels; secondary text and labels. |
892
- | `primary` / `primary-foreground` | Brand accent (from `--brand`). Primary buttons, links, chart line. |
893
- | `primary-soft` | Tinted primary background for selected rows, soft badges, icon circles. Pair with `text-primary`. |
894
- | `secondary`, `accent` | Same as muted; kept for shadcn compatibility. |
895
- | `success`, `warning`, `destructive` | Status colors. Use `bg-success/12 text-success` for soft status fills. |
896
- | `border`, `input`, `ring` | Hairlines, input borders, focus rings. |
897
- | `chart-1` … `chart-5`, `chart-grid` | Data series and gridlines. |
898
- | `category-1` … `category-6` | Members, lists, calendars: blue, teal, violet, rose, slate, cyan, each with a dark value. Never status. |
899
- | `category-foreground` | Text and icons on a category fill (initials, a swatch's check). |
900
- | `sidebar-*` | Sidebar surfaces (default to the page tokens). |
887
+ | Token | Use |
888
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
889
+ | `background` / `foreground` | Page background and body text. |
890
+ | `card` / `card-foreground` | Raised surfaces (cards, active nav item, popovers via `popover`). |
891
+ | `muted` / `muted-foreground` | Inputs, segmented controls, quiet panels; secondary text and labels. |
892
+ | `primary` / `primary-foreground` | Brand accent (from `--brand`). Primary buttons, links, chart line. |
893
+ | `primary-soft` / `primary-soft-foreground` | Tinted primary background for selected rows, soft badges, icon circles, and the text that goes on it. |
894
+ | `secondary`, `accent` | Same as muted; kept for shadcn compatibility. |
895
+ | `success`, `warning`, `destructive` | Status colors. Use `bg-success/12 text-success` for soft status fills. |
896
+ | `border`, `input`, `ring` | Hairlines, input borders, focus rings. |
897
+ | `chart-1` … `chart-5`, `chart-grid` | Data series and gridlines. |
898
+ | `category-1` … `category-6` | Members, lists, calendars: blue, teal, violet, rose, slate, cyan, each with a dark value. Never status. |
899
+ | `category-foreground` | Text and icons on a category fill (initials, a swatch's check). |
900
+ | `sidebar-*` | Sidebar surfaces (default to the page tokens). |
901
+
902
+ Put text on `bg-primary-soft` in `text-primary-soft-foreground`, not `text-primary`. In dark mode plain primary on the soft tint is 4.35:1 with the default brand, under axe's 4.5:1; the soft foreground is primary mixed with 30% white there (6.6:1) and plain primary in light mode. The soft Button and Badge, Avatar initials and the toast action use it. Icons alone need only 3:1, so `text-primary` is fine for them.
901
903
 
902
904
  Other theme values: `rounded-sm/md/lg/xl/2xl` scale from `--radius` (default 14px; cards use `xl`, buttons and inputs `md`), `shadow-card` (the one elevation), `font-sans` (Geist), `font-mono` (Geist Mono), `animate-typing`.
903
905
 
package/docs/ROADMAP.md CHANGED
@@ -48,7 +48,7 @@ Calendar views and SmartInput follow when a second app needs them.
48
48
  | AssistantDock (built) | Floating panel over the page (never reflows it) with header, empty state, thread, receipts and composer; a soft launcher button when closed; a link to full screen. Same on every page; pages pass suggestions only. | Extends ChatShell. |
49
49
  | AskBox (built) | Large rounded input with chips that appear as you type, suggestion pills under it, and the dictation button. Focus ring on the box, not the field. | One placeholder per app ("Ask HomeBot anything"). |
50
50
  | DictationButton, `useDictation` (built) | One button that is a microphone when empty, a stop button while listening (with listening bars and a live region), and send once there is text. The speech source plugs in (browser speech recognition, a server model). | Off-animation under reduced motion. |
51
- | ActionReceipt (built) | What the assistant did, as a compact card: round check, title, one meta line, Undo. One per action. | The core of "safe actions run on their own". |
51
+ | ActionReceipt (built) | What the assistant did, as a compact card: round check, title, meta, Undo. One per action. | The core of "safe actions run on their own". |
52
52
  | WhatIDid | Collapsed list of plain past-tense steps under an answer. | A plain-language variant of ToolSteps. |
53
53
  | ProgressCard | A long-running job: title, status word, progress bar, time left, an action when finished. | Downloads, imports, syncs. |
54
54
  | ChatsHome | Recent chats grouped Today, This week, Earlier, with search and a row menu (Rename, Delete with inline confirm). | Extends ConversationList. |
@@ -68,16 +68,16 @@ Use CSS rather than props when the first paint must be right (SSR apps): `ThemeP
68
68
 
69
69
  The full token list, with what each is for, is in the [component catalog](../COMPONENTS.md#tokens) (`npx harebear-ui docs Tokens`). The ones you will use most:
70
70
 
71
- | Token | Tailwind utilities |
72
- | ----------------------------------- | --------------------------------------------------------- |
73
- | `background`, `foreground` | `bg-background`, `text-foreground` |
74
- | `card` | `bg-card` (raised surfaces) |
75
- | `muted`, `muted-foreground` | `bg-muted` (inputs, quiet panels), secondary text |
76
- | `primary`, `primary-soft` | `bg-primary`, `bg-primary-soft text-primary` |
77
- | `success`, `warning`, `destructive` | `text-success`, `bg-success/12`, and so on |
78
- | `category-1` … `category-6` | `bg-category-2` (members, lists, calendars; never status) |
79
- | `border` | `border-border` (the default border color) |
80
- | elevation | `shadow-card` (the one shadow) |
71
+ | Token | Tailwind utilities |
72
+ | ----------------------------------- | ------------------------------------------------------------ |
73
+ | `background`, `foreground` | `bg-background`, `text-foreground` |
74
+ | `card` | `bg-card` (raised surfaces) |
75
+ | `muted`, `muted-foreground` | `bg-muted` (inputs, quiet panels), secondary text |
76
+ | `primary`, `primary-soft` | `bg-primary`, `bg-primary-soft text-primary-soft-foreground` |
77
+ | `success`, `warning`, `destructive` | `text-success`, `bg-success/12`, and so on |
78
+ | `category-1` … `category-6` | `bg-category-2` (members, lists, calendars; never status) |
79
+ | `border` | `border-border` (the default border color) |
80
+ | elevation | `shadow-card` (the one shadow) |
81
81
 
82
82
  ## Rules that keep a themed app consistent
83
83
 
package/llms.txt CHANGED
@@ -12,9 +12,9 @@
12
12
  ## Rules
13
13
 
14
14
  - Use harebear-ui components before writing new UI. Primitives keep shadcn/ui names and props.
15
- - Style with theme tokens only: bg-background, bg-card, bg-muted, text-muted-foreground, bg-primary, bg-primary-soft, text-success, text-warning, text-destructive, border-border, shadow-card, rounded-md/lg/xl. No hex colors or Tailwind palette colors.
15
+ - Style with theme tokens only: bg-background, bg-card, bg-muted, text-muted-foreground, bg-primary, bg-primary-soft (text on it: text-primary-soft-foreground), text-success, text-warning, text-destructive, border-border, shadow-card, rounded-md/lg/xl. No hex colors or Tailwind palette colors.
16
16
  - Status is a tone: success | warning | danger | info | neutral. Members, lists and calendars use category-1..6 (bg-category-3, or `color={3}` on MemberAvatar, CategoryDot, ColorSwatchPicker), never status colors.
17
- - Agent-first apps (assistant on every page, undoable actions): AppNav + Page/PageHeader/PageSection + AssistantProvider/AssistantDock + SectionedList + toastUndo + FocusLayout/SettingsSection. A page's own AskBox answers inline with `reply` from useAssistant and stops with `pending` + `onStop={stop}`. Recipe: `npx harebear-ui docs "Agent-first apps"`.
17
+ - Agent-first apps (assistant on every page, undoable actions): AppNav + Page/PageHeader/PageSection + AssistantProvider/AssistantDock + SectionedList + toastUndo + FocusLayout/SettingsSection. A page's own AskBox answers inline with `reply` from useAssistant and stops with `pending` + `onStop={stop}`. To queue follow-ups while it works, pass `queue` (AskBox, PromptInput, AssistantDock); a superseded send's `signal.reason` is "superseded", a Stop's is "stop"; after either, that send's later output is dropped, so the new send shows the rest. Recipe: `npx harebear-ui docs "Agent-first apps"`.
18
18
  - Icons: lucide-react elements, e.g. `<SearchIcon />`. Icon-only buttons need aria-label.
19
19
  - Touch: Button, Toggle and Input already meet 44px under `pointer: coarse` (buttons via an invisible ::after, which overflow-hidden ancestors clip: there, size the button itself with pointer-coarse:size-11).
20
20
  - Change the brand with `<ThemeProvider accent="#hex" radius={14}>` or `--brand` / `--radius` in CSS.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harebear-ui",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Opinionated, pre-themed shadcn/ui component library for React 19 + Tailwind CSS v4. Admin, data, and AI chat building blocks, ready for agents.",
5
5
  "license": "MIT",
6
6
  "author": "Harrison Milbradt",
@@ -18,7 +18,7 @@ This project uses `harebear-ui` for all UI. Its theme, tokens, and components ar
18
18
 
19
19
  - Import from the package root: `import { Button, Card, DataTable } from "harebear-ui"`. Never copy component source into the app.
20
20
  - Primitives keep shadcn/ui names, parts, and props (Dialog, DropdownMenu, Select, Sidebar, Tabs...).
21
- - Style only with theme tokens: `bg-background`, `bg-card`, `bg-muted`, `text-foreground`, `text-muted-foreground`, `bg-primary`, `bg-primary-soft text-primary`, `text-success`, `text-warning`, `text-destructive`, `border-border`, `shadow-card`, `rounded-md` (controls), `rounded-xl` (cards). No hex colors, no Tailwind palette colors like `bg-blue-500`, no inline color styles.
21
+ - Style only with theme tokens: `bg-background`, `bg-card`, `bg-muted`, `text-foreground`, `text-muted-foreground`, `bg-primary`, `bg-primary-soft text-primary-soft-foreground`, `text-success`, `text-warning`, `text-destructive`, `border-border`, `shadow-card`, `rounded-md` (controls), `rounded-xl` (cards). No hex colors, no Tailwind palette colors like `bg-blue-500`, no inline color styles.
22
22
  - Status uses a tone prop: `success | warning | danger | info | neutral` (StatusBadge, StatusDot, Timeline, StatusBars, LinkTile).
23
23
  - Icons are lucide-react elements passed as nodes: `icon={<UsersIcon />}`. Icon-only buttons need `aria-label`.
24
24
  - Touch: `Button`, `Toggle` and `Input` already meet 44px under `pointer: coarse` (buttons via an invisible `::after`, which `overflow-hidden` ancestors clip: there, size the button itself with `pointer-coarse:size-11`); don't override their size.
@@ -26,8 +26,8 @@ This project uses `harebear-ui` for all UI. Its theme, tokens, and components ar
26
26
  - Lay out pages as columns of `Card`s with `gap-5`, page sections with `gap-7`. Use `PageHeader` for page titles and `AppShell` for sidebar apps. Every shell (`AppShell`, `ChatShell`) is built on `SidebarLayout`, which gives the collapsible icon rail; use it directly for other sidebar layouts instead of the raw Sidebar primitives.
27
27
  - DataTable columns: define with `createDataTableColumnHelper<Row>()` at module scope, pass `getRowId`.
28
28
  - Chat apps: `ChatShell` (sidebar: `AppBrand`, `NewConversationButton`, `ConversationList` + `groupByDate`, `UserMenu`; optional `details` panel) around a `ChatPanel` (title, actions, `notice`, `composer`). Don't hand-build the sidebar, rail or panel.
29
- - Chat content: `ChatMessage role="user" | "assistant"`; render model output inside `Prose`; composer is `PromptInput` (+ `PromptInputChip`, `PromptInputSelect`); code is `CodeSnippet`; agent tool calls are `ToolGroup`/`ToolStep` (or the lighter `Trace`); permission prompts are `ApprovalCard`/`ApprovalResult`; working state is `TypingIndicator label`; clarifying questions are `Questionnaire`.
30
- - Agent-first and household apps: `AppNav` (sidebar on desktop, tab bar on phones; `fill` for a chat page, minus the round action button's space when `action` is set) around pages, each a `Page` with a `PageHeader` (`UserMenu compact className="md:hidden"` in its actions, hidden from md up) and `PageSection`s (h2; `Heading level={3}` inside cards); `AssistantProvider` + `AssistantDock` (pages call `useAssistantPage`); `AskBox` with `DictationButton` (inline answers: `AskBox` `reply={reply}` from `useAssistant()` plus your own `chatHref`, `send(text, { open: false })`; Stop: `pending` + `onStop={stop}`); `ActionReceipt` for each action the agent took; `toastUndo` after undoable changes; to-dos are `SectionedList` / `TaskRow` / `CheckCircle` (`lingerMs` = the Undo window); a week agenda is `DayStrip sticky` + one `SectionedList` with a section per day and `AgendaRow` rows via `renderItem`; settings are `FocusLayout` + `SettingsSection` + `SettingsRow`. Recipe: `npx harebear-ui docs "Agent-first apps"`. Categories (members, lists) use `category-1..6`. People are `MemberAvatar` (photo or initials on their `color`), `AvatarStack` and `VisibilityBadge`; list dots are `CategoryDot` (`locked` for private); profile settings use `AvatarUpload` and `ColorSwatchPicker`.
29
+ - Chat content: `ChatMessage role="user" | "assistant"`; render model output inside `Prose`; composer is `PromptInput` (+ `PromptInputChip`, `PromptInputSelect`; `label` names its field, default the placeholder); code is `CodeSnippet`; agent tool calls are `ToolGroup`/`ToolStep` (or the lighter `Trace`); permission prompts are `ApprovalCard`/`ApprovalResult`; working state is `TypingIndicator label`; clarifying questions are `Questionnaire`.
30
+ - Agent-first and household apps: `AppNav` (sidebar on desktop, tab bar on phones; `fill` for a chat page, minus the round action button's space when `action` is set) around pages, each a `Page` with a `PageHeader` (`UserMenu compact className="md:hidden"` in its actions, hidden from md up) and `PageSection`s (h2; `Heading level={3}` inside cards); `AssistantProvider` + `AssistantDock` (pages call `useAssistantPage`); `AskBox` with `DictationButton` (inline answers: `AskBox` `reply={reply}` from `useAssistant()` plus your own `chatHref`, `send(text, { open: false })`; Stop: `pending` + `onStop={stop}`; follow-ups while it works: `queue` on `AskBox`/`PromptInput`/`AssistantDock`, and in `onSend` tell the server to stop only when `signal.reason === "stop"`, not `"superseded"`; a superseded send's later `append`/`update` calls and return value are dropped, so the new send shows the rest); `ActionReceipt` for each action the agent took; `toastUndo` after undoable changes; to-dos are `SectionedList` / `TaskRow` / `CheckCircle` (`lingerMs` = the Undo window); a week agenda is `DayStrip sticky` + one `SectionedList` with a section per day and `AgendaRow` rows via `renderItem`; settings are `FocusLayout` + `SettingsSection` + `SettingsRow`. Recipe: `npx harebear-ui docs "Agent-first apps"`. Categories (members, lists) use `category-1..6`. People are `MemberAvatar` (photo or initials on their `color`), `AvatarStack` and `VisibilityBadge`; list dots are `CategoryDot` (`locked` for private); profile settings use `AvatarUpload` and `ColorSwatchPicker`.
31
31
  - Notices: `Alert variant="warning" | "success" | "info" | "destructive"`. Confirmations: `ConfirmDialog`; one line of text (New, Rename): `PromptDialog`. Toasts: `toast` / `toast.success` / `toast.error` from harebear-ui. Copyable IDs: `CopyField`. Live status: `StatusDot`/`StatusBadge` with `pulse`.
32
32
  - If you need a className to fix spacing, color, wrapping or borders on a harebear component, that is a library gap: report it instead of overriding.
33
33
  - Sign-in and sign-up pages: `AuthLayout` + `LoginForm` / `SignupForm` (extra fields as `children`) / `NewPasswordForm`; other signed-out steps use `AuthForm`. Don't hand-build auth forms.