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.
- package/dist/components/ai/ask-box.d.ts +8 -3
- package/dist/components/ai/ask-box.js +7 -6
- package/dist/components/ai/assistant.d.ts +13 -3
- package/dist/components/ai/assistant.js +5 -4
- package/dist/components/ai/prompt-input.d.ts +10 -3
- package/dist/components/ai/prompt-input.js +8 -7
- package/dist/components/ui/avatar.js +1 -1
- package/dist/components/ui/badge.js +1 -1
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/sonner.js +1 -1
- package/dist/styles.css +4 -0
- package/docs/COMPONENTS.md +20 -18
- package/docs/guides/theming.md +10 -10
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/skills/harebear-ui/SKILL.md +3 -3
|
@@ -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
|
|
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
|
-
})
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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:
|
|
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
|
|
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
|
-
})
|
|
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;
|
package/docs/COMPONENTS.md
CHANGED
|
@@ -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
|
|
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
|
|
888
|
-
|
|
|
889
|
-
| `background` / `foreground`
|
|
890
|
-
| `card` / `card-foreground`
|
|
891
|
-
| `muted` / `muted-foreground`
|
|
892
|
-
| `primary` / `primary-foreground`
|
|
893
|
-
| `primary-soft`
|
|
894
|
-
| `secondary`, `accent`
|
|
895
|
-
| `success`, `warning`, `destructive`
|
|
896
|
-
| `border`, `input`, `ring`
|
|
897
|
-
| `chart-1` … `chart-5`, `chart-grid`
|
|
898
|
-
| `category-1` … `category-6`
|
|
899
|
-
| `category-foreground`
|
|
900
|
-
| `sidebar-*`
|
|
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/guides/theming.md
CHANGED
|
@@ -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.
|
|
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.
|