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.
- package/dist/components/admin/detail-sheet.js +3 -0
- package/dist/components/ai/action-receipt.d.ts +1 -1
- package/dist/components/ai/action-receipt.js +1 -1
- package/dist/components/ai/approval.d.ts +1 -1
- package/dist/components/ai/approval.js +2 -2
- package/dist/components/ai/ask-box.d.ts +8 -3
- package/dist/components/ai/ask-box.js +8 -7
- package/dist/components/ai/assistant.d.ts +15 -3
- package/dist/components/ai/assistant.js +6 -4
- package/dist/components/ai/chat.js +1 -1
- package/dist/components/ai/prompt-input.d.ts +10 -3
- package/dist/components/ai/prompt-input.js +8 -7
- package/dist/components/ai/tool-steps.d.ts +2 -2
- package/dist/components/ai/tool-steps.js +5 -4
- 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/dialog.d.ts +1 -1
- package/dist/components/ui/dialog.js +3 -1
- package/dist/components/ui/drawer.d.ts +1 -1
- package/dist/components/ui/drawer.js +3 -1
- package/dist/components/ui/sheet.d.ts +1 -1
- package/dist/components/ui/sheet.js +3 -1
- package/dist/components/ui/sonner.js +3 -2
- package/dist/components/ui/trace.js +1 -1
- package/dist/lib/internal/toast-clearance.js +11 -1
- package/dist/styles.css +23 -4
- package/docs/COMPONENTS.md +26 -24
- package/docs/ROADMAP.md +1 -1
- 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
|
@@ -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
|
-
/**
|
|
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: "
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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)
|
|
@@ -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
|
-
/**
|
|
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"];
|
|
@@ -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
|
-
/**
|
|
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
|
]
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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) +
|
|
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) +
|
|
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
|
}
|
package/docs/COMPONENTS.md
CHANGED
|
@@ -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`:
|
|
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
|
|
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
|
|
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
|
|
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/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,
|
|
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. |
|
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.
|