harebear-ui 0.11.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.
@@ -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)
@@ -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"];
@@ -96,11 +101,16 @@ export interface AssistantDockProps extends Omit<React.ComponentProps<"section">
96
101
  launcher?: boolean;
97
102
  /** Label of the launcher. Default `Ask ${title}`. */
98
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;
99
109
  }
100
110
  /**
101
111
  * The assistant as a floating panel over the page (it never reflows the page): header, empty state with
102
112
  * suggestions, message list, AskBox. A soft launcher button opens it. Escape closes it. Needs AssistantProvider.
103
113
  * Sits above AppNav's phone tab bar.
104
114
  */
105
- declare function AssistantDock({ title, icon, fullScreenHref, actions, 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;
106
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, actions, 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);
@@ -251,6 +251,7 @@ function AssistantDock({ title = "Assistant", icon = /* @__PURE__ */ jsx(Sparkle
251
251
  suggestions: messages.length === 0 ? pills : void 0,
252
252
  dictation,
253
253
  pending,
254
+ queue,
254
255
  onStop: stop
255
256
  })
256
257
  })
@@ -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
  ]
@@ -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"
@@ -25,7 +25,7 @@ const Toaster = ({ ...props }) => {
25
25
  },
26
26
  toastOptions: { classNames: {
27
27
  toast: "cn-toast",
28
- 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!"
29
29
  } },
30
30
  ...props
31
31
  });
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;
@@ -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`
@@ -740,15 +740,15 @@ Agent tool calls as a bordered, collapsible card. Titles, labels and details wra
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, 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?`.
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
@@ -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
 
@@ -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.11.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.