harebear-ui 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/AGENTS.md +15 -12
  2. package/README.md +8 -7
  3. package/dist/components/admin/app-nav.d.ts +57 -0
  4. package/dist/components/admin/app-nav.js +175 -0
  5. package/dist/components/admin/focus-layout.d.ts +14 -0
  6. package/dist/components/admin/focus-layout.js +36 -0
  7. package/dist/components/admin/settings.d.ts +31 -0
  8. package/dist/components/admin/settings.js +78 -0
  9. package/dist/components/ai/action-receipt.d.ts +16 -0
  10. package/dist/components/ai/action-receipt.js +47 -0
  11. package/dist/components/ai/ask-box.d.ts +27 -0
  12. package/dist/components/ai/ask-box.js +85 -0
  13. package/dist/components/ai/assistant.d.ts +94 -0
  14. package/dist/components/ai/assistant.js +231 -0
  15. package/dist/components/ai/dictation.d.ts +57 -0
  16. package/dist/components/ai/dictation.js +148 -0
  17. package/dist/components/data/sectioned-list.d.ts +37 -0
  18. package/dist/components/data/sectioned-list.js +139 -0
  19. package/dist/components/data/task-row.d.ts +22 -0
  20. package/dist/components/data/task-row.js +60 -0
  21. package/dist/components/layout/collapse.d.ts +14 -0
  22. package/dist/components/layout/collapse.js +24 -0
  23. package/dist/components/ui/check-circle.d.ts +16 -0
  24. package/dist/components/ui/check-circle.js +39 -0
  25. package/dist/components/ui/sonner.d.ts +15 -2
  26. package/dist/components/ui/sonner.js +20 -3
  27. package/dist/index.d.ts +13 -2
  28. package/dist/index.js +13 -2
  29. package/dist/lib/toast-clearance.js +22 -0
  30. package/dist/styles.css +98 -0
  31. package/docs/COMPONENTS.md +136 -21
  32. package/docs/ROADMAP.md +95 -0
  33. package/docs/guides/theming.md +10 -9
  34. package/llms.txt +7 -4
  35. package/package.json +1 -1
  36. package/skills/harebear-ui/SKILL.md +1 -0
@@ -0,0 +1,94 @@
1
+ import { SpeechSource } from "./dictation.js";
2
+ import * as React from "react";
3
+ export interface AssistantMessage {
4
+ id: string;
5
+ role: "user" | "assistant";
6
+ /** Text or nodes: Prose, ActionReceipts, ToolSteps. */
7
+ content: React.ReactNode;
8
+ }
9
+ /** What the open page tells the assistant: use it as context, and its suggestions fill the dock's empty state. */
10
+ export interface AssistantPage {
11
+ /** Stable key for the page, e.g. the route. */
12
+ id: string;
13
+ title?: string;
14
+ /** Anything the send handler needs: the open list, the selected record. */
15
+ data?: unknown;
16
+ suggestions?: string[];
17
+ }
18
+ export interface AssistantSendContext {
19
+ page: AssistantPage | null;
20
+ /** The conversation before this message. */
21
+ messages: AssistantMessage[];
22
+ /** Add a message (a streamed reply, a receipt) while the handler runs. Returns its id. */
23
+ append: (message: Omit<AssistantMessage, "id"> & {
24
+ id?: string;
25
+ }) => string;
26
+ /** Replace a message's content, e.g. while a reply streams in. */
27
+ update: (id: string, content: React.ReactNode) => void;
28
+ }
29
+ export interface AssistantContextValue {
30
+ open: boolean;
31
+ setOpen: (open: boolean) => void;
32
+ toggle: () => void;
33
+ messages: AssistantMessage[];
34
+ /** True while the send handler runs. */
35
+ pending: boolean;
36
+ page: AssistantPage | null;
37
+ setPage: (page: AssistantPage | null) => void;
38
+ /** Adds the user message, opens the dock and runs the app's onSend. */
39
+ send: (text: string) => Promise<void>;
40
+ append: AssistantSendContext["append"];
41
+ update: AssistantSendContext["update"];
42
+ clear: () => void;
43
+ }
44
+ export interface AssistantProviderProps {
45
+ /**
46
+ * The app's send handler. Return the reply (text or nodes) to append it, or call `append` / `update` yourself
47
+ * (for streaming or receipts) and return nothing. A thrown error is shown as the reply.
48
+ */
49
+ onSend: (text: string, context: AssistantSendContext) => React.ReactNode | Promise<React.ReactNode>;
50
+ initialMessages?: AssistantMessage[];
51
+ defaultOpen?: boolean;
52
+ children?: React.ReactNode;
53
+ }
54
+ /**
55
+ * Keeps one assistant conversation across pages: open state, messages, the open page as context, and the app's
56
+ * send handler. Mount it above the router outlet; read it with useAssistant().
57
+ */
58
+ declare function AssistantProvider({ onSend, initialMessages, defaultOpen, children }: AssistantProviderProps): React.JSX.Element;
59
+ /** The assistant state from AssistantProvider. Throws outside one. */
60
+ export declare function useAssistant(): AssistantContextValue;
61
+ /**
62
+ * Tells the assistant which page is open, for as long as the calling component is mounted. The page is replaced
63
+ * when `page.id` changes; other fields are read at send time.
64
+ */
65
+ export declare function useAssistantPage(page: AssistantPage): void;
66
+ export interface AssistantDockProps extends Omit<React.ComponentProps<"section">, "title"> {
67
+ /** Assistant name in the header and launcher, e.g. "HomeBot". Default "Assistant". */
68
+ title?: string;
69
+ /** Header and launcher icon. Default a sparkle. */
70
+ icon?: React.ReactNode;
71
+ /** Link to the full-screen chat. Shows an expand button in the header. */
72
+ fullScreenHref?: string;
73
+ /** Empty state heading. Default "How can I help?". */
74
+ emptyTitle?: React.ReactNode;
75
+ /** Empty state line under the heading. */
76
+ emptyDescription?: React.ReactNode;
77
+ /** Suggestion pills in the empty state. Default: the open page's suggestions. */
78
+ suggestions?: string[];
79
+ /** Composer placeholder. Default `Ask ${title}`. */
80
+ placeholder?: string;
81
+ /** Speech source for the mic. Default the browser's; `false` hides the mic. */
82
+ dictation?: SpeechSource | false;
83
+ /** Show the floating launcher button while closed. Default true. Turn off when another control opens it. */
84
+ launcher?: boolean;
85
+ /** Label of the launcher. Default `Ask ${title}`. */
86
+ launcherLabel?: string;
87
+ }
88
+ /**
89
+ * The assistant as a floating panel over the page (it never reflows the page): header, empty state with
90
+ * suggestions, message list, AskBox. A soft launcher button opens it. Escape closes it. Needs AssistantProvider.
91
+ * Sits above AppNav's phone tab bar.
92
+ */
93
+ declare function AssistantDock({ title, icon, fullScreenHref, emptyTitle, emptyDescription, suggestions, placeholder, dictation, launcher, launcherLabel, className, ...props }: AssistantDockProps): React.JSX.Element | null;
94
+ export { AssistantDock, AssistantProvider };
@@ -0,0 +1,231 @@
1
+ "use client";
2
+ import { AskBox } from "./ask-box.js";
3
+ import { AssistantAvatar, ChatMessage, ChatThread, TypingIndicator } from "./chat.js";
4
+ import { Button } from "../ui/button.js";
5
+ import { useToastClearance } from "../../lib/toast-clearance.js";
6
+ import { cn } from "../../lib/utils.js";
7
+ import * as React from "react";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { Maximize2Icon, SparklesIcon, XIcon } from "lucide-react";
10
+ // src/components/ai/assistant.tsx
11
+ const AssistantContext = React.createContext(null);
12
+ let nextId = 0;
13
+ const newId = () => `m${Date.now().toString(36)}${(nextId++).toString(36)}`;
14
+ /**
15
+ * Keeps one assistant conversation across pages: open state, messages, the open page as context, and the app's
16
+ * send handler. Mount it above the router outlet; read it with useAssistant().
17
+ */
18
+ function AssistantProvider({ onSend, initialMessages = [], defaultOpen = false, children }) {
19
+ const [open, setOpen] = React.useState(defaultOpen);
20
+ const [messages, setMessages] = React.useState(initialMessages);
21
+ const [pending, setPending] = React.useState(false);
22
+ const [page, setPage] = React.useState(null);
23
+ const messagesRef = React.useRef(messages);
24
+ messagesRef.current = messages;
25
+ const pageRef = React.useRef(page);
26
+ pageRef.current = page;
27
+ const onSendRef = React.useRef(onSend);
28
+ onSendRef.current = onSend;
29
+ const append = React.useCallback((m) => {
30
+ const id = m.id ?? newId();
31
+ setMessages((list) => [...list, {
32
+ ...m,
33
+ id
34
+ }]);
35
+ return id;
36
+ }, []);
37
+ const update = React.useCallback((id, content) => {
38
+ setMessages((list) => list.map((m) => m.id === id ? {
39
+ ...m,
40
+ content
41
+ } : m));
42
+ }, []);
43
+ const send = React.useCallback(async (text) => {
44
+ const before = messagesRef.current;
45
+ append({
46
+ role: "user",
47
+ content: text
48
+ });
49
+ setOpen(true);
50
+ setPending(true);
51
+ try {
52
+ const reply = await onSendRef.current(text, {
53
+ page: pageRef.current,
54
+ messages: before,
55
+ append,
56
+ update
57
+ });
58
+ if (reply !== void 0 && reply !== null && reply !== false) append({
59
+ role: "assistant",
60
+ content: reply
61
+ });
62
+ } catch (e) {
63
+ append({
64
+ role: "assistant",
65
+ content: e instanceof Error ? e.message : "Something went wrong."
66
+ });
67
+ } finally {
68
+ setPending(false);
69
+ }
70
+ }, [append, update]);
71
+ const value = React.useMemo(() => ({
72
+ open,
73
+ setOpen,
74
+ toggle: () => setOpen((v) => !v),
75
+ messages,
76
+ pending,
77
+ page,
78
+ setPage,
79
+ send,
80
+ append,
81
+ update,
82
+ clear: () => setMessages([])
83
+ }), [
84
+ open,
85
+ messages,
86
+ pending,
87
+ page,
88
+ send,
89
+ append,
90
+ update
91
+ ]);
92
+ return /* @__PURE__ */ jsx(AssistantContext.Provider, {
93
+ value,
94
+ children
95
+ });
96
+ }
97
+ /** The assistant state from AssistantProvider. Throws outside one. */
98
+ function useAssistant() {
99
+ const ctx = React.useContext(AssistantContext);
100
+ if (!ctx) throw new Error("useAssistant must be used inside <AssistantProvider>");
101
+ return ctx;
102
+ }
103
+ /**
104
+ * Tells the assistant which page is open, for as long as the calling component is mounted. The page is replaced
105
+ * when `page.id` changes; other fields are read at send time.
106
+ */
107
+ function useAssistantPage(page) {
108
+ const { setPage } = useAssistant();
109
+ const latest = React.useRef(page);
110
+ latest.current = page;
111
+ React.useEffect(() => {
112
+ setPage(latest.current);
113
+ return () => setPage(null);
114
+ }, [page.id, setPage]);
115
+ }
116
+ /**
117
+ * The assistant as a floating panel over the page (it never reflows the page): header, empty state with
118
+ * suggestions, message list, AskBox. A soft launcher button opens it. Escape closes it. Needs AssistantProvider.
119
+ * Sits above AppNav's phone tab bar.
120
+ */
121
+ function AssistantDock({ title = "Assistant", icon = /* @__PURE__ */ jsx(SparklesIcon, {}), fullScreenHref, emptyTitle = "How can I help?", emptyDescription, suggestions, placeholder, dictation, launcher = true, launcherLabel, className, ...props }) {
122
+ const { open, setOpen, messages, pending, page, send } = useAssistant();
123
+ const [draft, setDraft] = React.useState("");
124
+ const titleId = React.useId();
125
+ const panelRef = React.useRef(null);
126
+ const launcherRef = React.useRef(null);
127
+ const wasOpen = React.useRef(open);
128
+ const pills = suggestions ?? page?.suggestions ?? [];
129
+ React.useEffect(() => {
130
+ if (open && !wasOpen.current) panelRef.current?.querySelector("textarea")?.focus();
131
+ if (!open && wasOpen.current) launcherRef.current?.focus();
132
+ wasOpen.current = open;
133
+ }, [open]);
134
+ const submit = (text) => {
135
+ if (pending) return;
136
+ setDraft("");
137
+ panelRef.current?.querySelector("textarea")?.focus();
138
+ send(text);
139
+ };
140
+ useToastClearance("--hb-toast-launcher", "3.5rem", !open && launcher);
141
+ if (!open) {
142
+ if (!launcher) return null;
143
+ return /* @__PURE__ */ jsxs("button", {
144
+ ref: launcherRef,
145
+ type: "button",
146
+ "data-slot": "assistant-launcher",
147
+ "aria-expanded": false,
148
+ onClick: () => setOpen(true),
149
+ className: "fixed right-4 bottom-[calc(var(--hb-bottom-inset,0px)+var(--hb-fab-inset,0px)+1rem)] z-40 flex h-12 items-center gap-2 rounded-full border border-border bg-card pr-5 pl-2 text-sm font-medium text-foreground shadow-card outline-none hover:bg-muted focus-visible:ring-4 focus-visible:ring-ring/20 md:right-6 md:bottom-6",
150
+ children: [/* @__PURE__ */ jsx(AssistantAvatar, { icon }), launcherLabel ?? `Ask ${title}`]
151
+ });
152
+ }
153
+ return /* @__PURE__ */ jsxs("section", {
154
+ ref: panelRef,
155
+ "data-slot": "assistant-dock",
156
+ role: "dialog",
157
+ "aria-modal": "false",
158
+ "aria-labelledby": titleId,
159
+ onKeyDown: (e) => {
160
+ if (e.key === "Escape") {
161
+ e.stopPropagation();
162
+ setOpen(false);
163
+ }
164
+ },
165
+ className: cn("fixed inset-x-2 bottom-[calc(var(--hb-bottom-inset,0px)+0.5rem)] z-40 flex h-[min(40rem,calc(100dvh-var(--hb-bottom-inset,0px)-1rem))] flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-card duration-200 animate-in fade-in slide-in-from-bottom-4 motion-reduce:animate-none sm:inset-x-auto sm:right-4 sm:w-104 md:right-6 md:bottom-6 md:h-[min(40rem,calc(100dvh-3rem))]", className),
166
+ ...props,
167
+ children: [
168
+ /* @__PURE__ */ jsxs("header", {
169
+ className: "flex items-center gap-2.5 py-2.5 pr-2.5 pl-4",
170
+ children: [
171
+ /* @__PURE__ */ jsx(AssistantAvatar, { icon }),
172
+ /* @__PURE__ */ jsx("h2", {
173
+ id: titleId,
174
+ className: "min-w-0 flex-1 truncate text-[15px] font-semibold",
175
+ children: title
176
+ }),
177
+ fullScreenHref && /* @__PURE__ */ jsx(Button, {
178
+ asChild: true,
179
+ variant: "ghost",
180
+ size: "icon",
181
+ shape: "pill",
182
+ children: /* @__PURE__ */ jsx("a", {
183
+ href: fullScreenHref,
184
+ "aria-label": "Open full screen",
185
+ children: /* @__PURE__ */ jsx(Maximize2Icon, {})
186
+ })
187
+ }),
188
+ /* @__PURE__ */ jsx(Button, {
189
+ variant: "ghost",
190
+ size: "icon",
191
+ shape: "pill",
192
+ "aria-label": `Close ${title}`,
193
+ onClick: () => setOpen(false),
194
+ children: /* @__PURE__ */ jsx(XIcon, {})
195
+ })
196
+ ]
197
+ }),
198
+ messages.length === 0 ? /* @__PURE__ */ jsxs("div", {
199
+ "data-slot": "assistant-dock-empty",
200
+ className: "flex flex-1 flex-col justify-end gap-1 overflow-y-auto px-5 pb-3",
201
+ children: [/* @__PURE__ */ jsx("p", {
202
+ className: "text-lg font-semibold",
203
+ children: emptyTitle
204
+ }), emptyDescription && /* @__PURE__ */ jsx("p", {
205
+ className: "text-sm text-muted-foreground",
206
+ children: emptyDescription
207
+ })]
208
+ }) : /* @__PURE__ */ jsxs(ChatThread, {
209
+ className: "px-4 pt-2",
210
+ contentClassName: "gap-5",
211
+ children: [messages.map((m) => /* @__PURE__ */ jsx(ChatMessage, {
212
+ role: m.role,
213
+ avatar: false,
214
+ children: m.content
215
+ }, m.id)), pending && /* @__PURE__ */ jsx(TypingIndicator, { label: "Working" })]
216
+ }),
217
+ /* @__PURE__ */ jsx("div", {
218
+ className: "px-3 pt-2 pb-3",
219
+ children: /* @__PURE__ */ jsx(AskBox, {
220
+ value: draft,
221
+ onValueChange: setDraft,
222
+ onSubmit: submit,
223
+ placeholder: placeholder ?? `Ask ${title}`,
224
+ suggestions: messages.length === 0 ? pills : void 0,
225
+ dictation
226
+ })
227
+ })
228
+ ]
229
+ });
230
+ }
231
+ export { AssistantDock, AssistantProvider, useAssistant, useAssistantPage };
@@ -0,0 +1,57 @@
1
+ import * as React from "react";
2
+ export interface SpeechSourceHandlers {
3
+ /** Recognised text. `final` is false for interim guesses that may still change. */
4
+ onResult: (text: string, final: boolean) => void;
5
+ /** Listening stopped (silence, stop(), or an error). */
6
+ onEnd: () => void;
7
+ onError: (error: string) => void;
8
+ }
9
+ /**
10
+ * Where dictation comes from. The default is `browserSpeechSource` (the Web Speech API); pass your own to use a
11
+ * server model: start recording in `start`, call the handlers, and return a function that stops.
12
+ */
13
+ export interface SpeechSource {
14
+ isSupported: () => boolean;
15
+ start: (handlers: SpeechSourceHandlers, options: {
16
+ lang?: string;
17
+ }) => () => void;
18
+ }
19
+ /** SpeechSource on the browser's SpeechRecognition API. Unsupported (Firefox, most WebViews) means no mic button. */
20
+ export declare const browserSpeechSource: SpeechSource;
21
+ export interface UseDictationOptions {
22
+ /** Default browserSpeechSource. */
23
+ source?: SpeechSource;
24
+ /** BCP 47 language, e.g. "en-CA". Default: the browser's. */
25
+ lang?: string;
26
+ /** Called with each final transcript. */
27
+ onTranscript: (text: string) => void;
28
+ }
29
+ /** Dictation state for a custom control: `{ supported, listening, interim, error, start, stop }`. */
30
+ export declare function useDictation({ source, lang, onTranscript }: UseDictationOptions): {
31
+ supported: boolean;
32
+ listening: boolean;
33
+ interim: string;
34
+ error: string | null;
35
+ start: () => void;
36
+ stop: () => void;
37
+ };
38
+ export interface DictationButtonProps extends Omit<React.ComponentProps<"div">, "onSubmit"> {
39
+ /** The composer's current text. Empty shows the mic; text shows send. */
40
+ value: string;
41
+ /** Final transcripts, to append to the text. */
42
+ onTranscript: (text: string) => void;
43
+ /** Send. Without it, the send button is a submit button for the surrounding form. */
44
+ onSend?: () => void;
45
+ source?: SpeechSource;
46
+ lang?: string;
47
+ disabled?: boolean;
48
+ /** Default "Send message". */
49
+ sendLabel?: string;
50
+ }
51
+ /**
52
+ * One round button for a composer: a mic when empty, stop (with listening bars) while listening, send once there is
53
+ * text. Announces "Listening" in a polite live region. Without a supported speech source the mic is hidden and the
54
+ * button is just send.
55
+ */
56
+ declare function DictationButton({ value, onTranscript, onSend, source, lang, disabled, sendLabel, className, ...props }: DictationButtonProps): React.JSX.Element;
57
+ export { DictationButton };
@@ -0,0 +1,148 @@
1
+ "use client";
2
+ import { Button } from "../ui/button.js";
3
+ import { cn } from "../../lib/utils.js";
4
+ import * as React from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { ArrowUpIcon, MicIcon } from "lucide-react";
7
+ // src/components/ai/dictation.tsx
8
+ function recognitionCtor() {
9
+ if (typeof window === "undefined") return void 0;
10
+ const w = window;
11
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition;
12
+ }
13
+ /** SpeechSource on the browser's SpeechRecognition API. Unsupported (Firefox, most WebViews) means no mic button. */
14
+ const browserSpeechSource = {
15
+ isSupported: () => recognitionCtor() !== void 0,
16
+ start(handlers, { lang }) {
17
+ const Ctor = recognitionCtor();
18
+ if (!Ctor) {
19
+ handlers.onError("not-supported");
20
+ handlers.onEnd();
21
+ return () => {};
22
+ }
23
+ const rec = new Ctor();
24
+ if (lang) rec.lang = lang;
25
+ rec.interimResults = true;
26
+ rec.continuous = false;
27
+ rec.onresult = (e) => {
28
+ for (let i = e.resultIndex; i < e.results.length; i++) {
29
+ const r = e.results[i];
30
+ if (r) handlers.onResult(r[0].transcript, r.isFinal);
31
+ }
32
+ };
33
+ rec.onerror = (e) => handlers.onError(e.error);
34
+ rec.onend = () => handlers.onEnd();
35
+ rec.start();
36
+ return () => rec.stop();
37
+ }
38
+ };
39
+ const noopSubscribe = () => () => {};
40
+ /** Dictation state for a custom control: `{ supported, listening, interim, error, start, stop }`. */
41
+ function useDictation({ source = browserSpeechSource, lang, onTranscript }) {
42
+ const supported = React.useSyncExternalStore(noopSubscribe, source.isSupported, () => false);
43
+ const [listening, setListening] = React.useState(false);
44
+ const [interim, setInterim] = React.useState("");
45
+ const [error, setError] = React.useState(null);
46
+ const stopRef = React.useRef(null);
47
+ const onTranscriptRef = React.useRef(onTranscript);
48
+ onTranscriptRef.current = onTranscript;
49
+ const stop = React.useCallback(() => {
50
+ stopRef.current?.();
51
+ }, []);
52
+ const start = React.useCallback(() => {
53
+ if (stopRef.current) return;
54
+ setError(null);
55
+ setInterim("");
56
+ setListening(true);
57
+ stopRef.current = source.start({
58
+ onResult: (text, final) => {
59
+ if (final) {
60
+ setInterim("");
61
+ onTranscriptRef.current(text.trim());
62
+ } else setInterim(text);
63
+ },
64
+ onError: (e) => setError(e),
65
+ onEnd: () => {
66
+ stopRef.current = null;
67
+ setInterim("");
68
+ setListening(false);
69
+ }
70
+ }, { lang });
71
+ }, [source, lang]);
72
+ React.useEffect(() => () => stopRef.current?.(), []);
73
+ return {
74
+ supported,
75
+ listening,
76
+ interim,
77
+ error,
78
+ start,
79
+ stop
80
+ };
81
+ }
82
+ /**
83
+ * One round button for a composer: a mic when empty, stop (with listening bars) while listening, send once there is
84
+ * text. Announces "Listening" in a polite live region. Without a supported speech source the mic is hidden and the
85
+ * button is just send.
86
+ */
87
+ function DictationButton({ value, onTranscript, onSend, source, lang, disabled, sendLabel = "Send message", className, ...props }) {
88
+ const d = useDictation({
89
+ source,
90
+ lang,
91
+ onTranscript
92
+ });
93
+ const hasText = value.trim().length > 0;
94
+ return /* @__PURE__ */ jsxs("div", {
95
+ "data-slot": "dictation-button",
96
+ className: cn("flex shrink-0 items-center", className),
97
+ ...props,
98
+ children: [/* @__PURE__ */ jsx("span", {
99
+ role: "status",
100
+ "aria-live": "polite",
101
+ className: "sr-only",
102
+ children: d.listening ? "Listening" : ""
103
+ }), d.listening ? /* @__PURE__ */ jsx(Button, {
104
+ type: "button",
105
+ size: "icon",
106
+ shape: "pill",
107
+ className: "size-10 gap-0.5",
108
+ "aria-label": "Stop dictation",
109
+ onClick: d.stop,
110
+ children: /* @__PURE__ */ jsx(ListeningBars, {})
111
+ }) : !hasText && d.supported ? /* @__PURE__ */ jsx(Button, {
112
+ type: "button",
113
+ variant: "ghost",
114
+ size: "icon",
115
+ shape: "pill",
116
+ className: "size-10",
117
+ "aria-label": "Dictate",
118
+ disabled,
119
+ onClick: d.start,
120
+ children: /* @__PURE__ */ jsx(MicIcon, {})
121
+ }) : /* @__PURE__ */ jsx(Button, {
122
+ type: onSend ? "button" : "submit",
123
+ size: "icon",
124
+ shape: "pill",
125
+ className: "size-10",
126
+ "aria-label": sendLabel,
127
+ disabled: disabled || !hasText,
128
+ onClick: onSend,
129
+ children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
130
+ })]
131
+ });
132
+ }
133
+ function ListeningBars() {
134
+ return /* @__PURE__ */ jsx("span", {
135
+ "aria-hidden": "true",
136
+ className: "flex h-4 items-center gap-0.5",
137
+ children: [
138
+ 0,
139
+ 150,
140
+ 300,
141
+ 450
142
+ ].map((delay) => /* @__PURE__ */ jsx("span", {
143
+ className: "h-full w-0.75 origin-center animate-listening rounded-full bg-current motion-reduce:scale-y-75",
144
+ style: { animationDelay: `${delay}ms` }
145
+ }, delay))
146
+ });
147
+ }
148
+ export { DictationButton, browserSpeechSource, useDictation };
@@ -0,0 +1,37 @@
1
+ import { TaskRowAssignee } from "./task-row.js";
2
+ import * as React from "react";
3
+ export interface SectionedListItem {
4
+ id: string;
5
+ title: React.ReactNode;
6
+ meta?: React.ReactNode;
7
+ done?: boolean;
8
+ important?: boolean;
9
+ assignee?: TaskRowAssignee;
10
+ }
11
+ export interface SectionedListSection {
12
+ id: string;
13
+ /** Section heading, e.g. "Today" or "Groceries". Omit for a section without one. */
14
+ title?: React.ReactNode;
15
+ items: SectionedListItem[];
16
+ }
17
+ export interface SectionedListProps extends Omit<React.ComponentProps<"div">, "onToggle"> {
18
+ sections: SectionedListSection[];
19
+ /** Called when an item's check changes. Update `done` in your data; the row folds away by itself. */
20
+ onToggle: (id: string, done: boolean) => void;
21
+ /** Makes item titles buttons, e.g. to open a DetailSheet. */
22
+ onOpen?: (id: string) => void;
23
+ /** Shows one add box at the top. Called with the trimmed text on Enter. */
24
+ onAdd?: (text: string) => void;
25
+ /** Add box placeholder and label. Default "Add a to-do". */
26
+ addPlaceholder?: string;
27
+ /** Shown when no item is open, e.g. <Empty /> or a line of text. */
28
+ empty?: React.ReactNode;
29
+ /** Start with the done items shown. Default false. */
30
+ defaultShowDone?: boolean;
31
+ }
32
+ /**
33
+ * A to-do list: one add box, sections with headings, done items folded into a "Show N done" toggle, an empty
34
+ * state. Rows fold in and out with Collapse, so nothing jumps.
35
+ */
36
+ declare function SectionedList({ sections, onToggle, onOpen, onAdd, addPlaceholder, empty, defaultShowDone, className, ...props }: SectionedListProps): React.JSX.Element;
37
+ export { SectionedList };