@schlenkr/quassel 0.6.0 → 0.7.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/index.js CHANGED
@@ -42,6 +42,11 @@ var englishTexts = {
42
42
  send: "Send",
43
43
  sendIntoRun: "Feed into the running turn",
44
44
  stop: "Stop work",
45
+ microphone: "Speak",
46
+ microphoneHint: "Hold to speak or click to start recording",
47
+ microphonePreparing: "Connecting microphone ...",
48
+ microphoneRecording: "Finish recording",
49
+ microphoneTranscribing: "Transcribing ...",
45
50
  placeholder: "Write a message ...",
46
51
  steeringPlaceholder: "Interject ...",
47
52
  steered: "Fed into the running turn",
@@ -112,6 +117,11 @@ var germanTexts = {
112
117
  send: "Senden",
113
118
  sendIntoRun: "In die laufende Antwort einreihen",
114
119
  stop: "Bearbeitung stoppen",
120
+ microphone: "Sprechen",
121
+ microphoneHint: "Zum Sprechen halten oder zum Aufnehmen antippen",
122
+ microphonePreparing: "Mikrofon wird verbunden ...",
123
+ microphoneRecording: "Aufnahme beenden",
124
+ microphoneTranscribing: "Wird transkribiert ...",
115
125
  placeholder: "Nachricht schreiben ...",
116
126
  steeringPlaceholder: "Nachsteuern ...",
117
127
  steered: "In die laufende Antwort eingereiht",
@@ -1860,18 +1870,20 @@ function MessageAttachments({ message, texts }) {
1860
1870
  return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1861
1871
  /* @__PURE__ */ jsx13("ul", { className: "qsl:mt-2.5 qsl:flex qsl:flex-wrap qsl:gap-2", "aria-label": texts.attachments, children: message.attachments.map((attachment, index) => {
1862
1872
  const isImage = attachment.mediaType.startsWith("image/");
1863
- return /* @__PURE__ */ jsxs6("li", { className: cn("qsl:relative qsl:flex qsl:max-w-full qsl:min-w-0 qsl:flex-col qsl:overflow-hidden qsl:rounded-lg qsl:border qsl:border-border qsl:text-small", isImage ? "qsl:w-[320px]" : "qsl:w-[190px]"), children: [
1864
- isImage && /* @__PURE__ */ jsx13(
1873
+ if (isImage)
1874
+ return /* @__PURE__ */ jsx13("li", { className: "qsl:relative qsl:overflow-hidden qsl:rounded-lg qsl:border qsl:border-border", children: /* @__PURE__ */ jsx13(
1865
1875
  "button",
1866
1876
  {
1867
1877
  type: "button",
1868
- className: "qsl:block qsl:w-full qsl:cursor-zoom-in qsl:bg-secondary",
1878
+ className: "qsl:block qsl:cursor-zoom-in qsl:bg-secondary",
1869
1879
  "aria-label": fillText(texts.enlargeImage ?? defaultTexts.enlargeImage, { name: attachment.name }),
1880
+ title: attachment.name,
1870
1881
  onClick: () => setEnlarged(attachment),
1871
1882
  "data-chat": "attachment-image",
1872
- children: /* @__PURE__ */ jsx13("img", { className: "qsl:mx-auto qsl:block qsl:max-h-[240px] qsl:max-w-full qsl:object-contain", src: attachment.url, alt: attachment.name, loading: "lazy" })
1883
+ children: /* @__PURE__ */ jsx13("img", { className: "qsl:block qsl:size-24 qsl:object-cover", src: attachment.url, alt: attachment.name, loading: "lazy" })
1873
1884
  }
1874
- ),
1885
+ ) }, `${attachment.url}-${index}`);
1886
+ return /* @__PURE__ */ jsxs6("li", { className: "qsl:relative qsl:flex qsl:w-[190px] qsl:max-w-full qsl:min-w-0 qsl:flex-col qsl:overflow-hidden qsl:rounded-lg qsl:border qsl:border-border qsl:text-small", children: [
1875
1887
  attachment.mediaType.startsWith("video/") && /* @__PURE__ */ jsx13("video", { className: "qsl:block qsl:h-[112px] qsl:w-full qsl:bg-secondary qsl:object-contain", src: attachment.url, controls: true, preload: "metadata", "aria-label": attachment.name }),
1876
1888
  /* @__PURE__ */ jsxs6("div", { className: "qsl:flex qsl:flex-col qsl:gap-1 qsl:p-2 qsl:[overflow-wrap:anywhere]", children: [
1877
1889
  /* @__PURE__ */ jsx13("a", { className: "qsl:font-medium", href: attachmentDownloadUrl(attachment.url), download: attachment.name, children: attachment.name }),
@@ -1986,7 +1998,7 @@ function ChatPanel({
1986
1998
  }
1987
1999
 
1988
2000
  // packages/quassel/src/ChatInputToolbar.tsx
1989
- import { PaperclipIcon, SendIcon, XIcon as XIcon3 } from "lucide-react";
2001
+ import { PaperclipIcon, SendIcon as SendIcon2, XIcon as XIcon3 } from "lucide-react";
1990
2002
 
1991
2003
  // packages/quassel/src/ui/useFileInput.ts
1992
2004
  import { useRef as useRef5, useState as useState6 } from "react";
@@ -2037,9 +2049,214 @@ function useFileInput({ disabled, onFiles, onPasteText }) {
2037
2049
  }
2038
2050
 
2039
2051
  // packages/quassel/src/ChatInputToolbar.tsx
2040
- import { useCallback as useCallback3, useContext as useContext4, useEffect as useEffect5, useImperativeHandle, useLayoutEffect as useLayoutEffect3, useRef as useRef6, useState as useState7 } from "react";
2052
+ import { useCallback as useCallback3, useContext as useContext4, useEffect as useEffect6, useImperativeHandle, useLayoutEffect as useLayoutEffect5, useRef as useRef8, useState as useState8 } from "react";
2053
+ import { createPortal as createPortal3 } from "react-dom";
2054
+
2055
+ // packages/quassel/src/ComposerPrimaryAction.tsx
2056
+ import { LoaderCircleIcon, MicIcon, SendIcon, SquareIcon as SquareIcon2 } from "lucide-react";
2057
+ import { useEffect as useEffect5, useLayoutEffect as useLayoutEffect4, useRef as useRef7, useState as useState7 } from "react";
2058
+
2059
+ // packages/quassel/src/RecordingAura.tsx
2060
+ import { useLayoutEffect as useLayoutEffect3, useRef as useRef6 } from "react";
2041
2061
  import { createPortal as createPortal2 } from "react-dom";
2042
- import { Fragment as Fragment3, jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
2062
+ import { jsx as jsx15 } from "react/jsx-runtime";
2063
+ function RecordingAura({ anchor, level = 0 }) {
2064
+ const aura = useRef6(null);
2065
+ const volume = Number.isFinite(level) ? Math.max(0, Math.min(1, level)) : 0;
2066
+ useLayoutEffect3(() => {
2067
+ const element = aura.current;
2068
+ const view = anchor.ownerDocument.defaultView;
2069
+ if (!element || !view) return;
2070
+ let frame = 0;
2071
+ let visible = true;
2072
+ const observer = new view.IntersectionObserver(([entry]) => {
2073
+ visible = entry.isIntersecting;
2074
+ });
2075
+ observer.observe(anchor);
2076
+ const update = () => {
2077
+ const rect = anchor.getBoundingClientRect();
2078
+ const style = view.getComputedStyle(anchor);
2079
+ element.style.left = `${rect.left}px`;
2080
+ element.style.top = `${rect.top}px`;
2081
+ element.style.width = `${rect.width}px`;
2082
+ element.style.height = `${rect.height}px`;
2083
+ element.style.visibility = visible && rect.width && rect.height && anchor.checkVisibility() ? "visible" : "hidden";
2084
+ element.style.setProperty("--qsl-pill-aura-color", style.getPropertyValue("--qsl-pill-aura-color") || style.getPropertyValue("--qsl-destructive"));
2085
+ element.style.setProperty("--qsl-pill-aura-ring-color", style.getPropertyValue("--qsl-pill-aura-ring-color") || style.getPropertyValue("--qsl-pill-aura-color") || style.getPropertyValue("--qsl-destructive"));
2086
+ element.style.zIndex = style.getPropertyValue("--qsl-pill-aura-z-index") || "1000";
2087
+ frame = view.requestAnimationFrame(update);
2088
+ };
2089
+ update();
2090
+ return () => {
2091
+ view.cancelAnimationFrame(frame);
2092
+ observer.disconnect();
2093
+ };
2094
+ }, [anchor]);
2095
+ return createPortal2(/* @__PURE__ */ jsx15(
2096
+ "span",
2097
+ {
2098
+ "aria-hidden": "true",
2099
+ className: "qsl:recording-aura",
2100
+ "data-quassel": "",
2101
+ "data-recording-aura": "",
2102
+ ref: aura,
2103
+ style: { transform: `scale(${1.2 + volume * 0.65})`, opacity: 0.4 + volume * 0.4 }
2104
+ }
2105
+ ), anchor.ownerDocument.body);
2106
+ }
2107
+
2108
+ // packages/quassel/src/ComposerPrimaryAction.tsx
2109
+ import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs8 } from "react/jsx-runtime";
2110
+ function ComposerPrimaryAction({ hasContent, running: running2, sending, disabled, sendDisabled, microphone, texts, onSend, onStop }) {
2111
+ const state = microphone && microphone.state !== "idle" ? microphone.state : hasContent ? "send" : running2 ? "stop" : microphone && !sending ? "microphone" : "send";
2112
+ const voice = state === "microphone" || state === "preparing" || state === "recording";
2113
+ const busy = state === "preparing" || state === "transcribing";
2114
+ const blocked = disabled || state === "transcribing" || state === "send" && (sendDisabled || sending || !hasContent) || state === "stop" && !onStop;
2115
+ const label = state === "microphone" ? microphone?.label ?? texts.microphone : state === "preparing" ? texts.microphonePreparing : state === "recording" ? texts.microphoneRecording : state === "transcribing" ? texts.microphoneTranscribing : state === "stop" ? texts.stop : running2 ? texts.sendIntoRun : texts.send;
2116
+ const [button, setButton] = useState7(null);
2117
+ const press = useRef7(null);
2118
+ const suppressClick = useRef7(false);
2119
+ const clickReset = useRef7(void 0);
2120
+ const currentMicrophone = useRef7(microphone);
2121
+ const resetClick = () => {
2122
+ clearTimeout(clickReset.current);
2123
+ clickReset.current = setTimeout(() => {
2124
+ suppressClick.current = false;
2125
+ }, 0);
2126
+ };
2127
+ const cancel = () => {
2128
+ const active = press.current;
2129
+ press.current = null;
2130
+ if (active) resetClick();
2131
+ const current = currentMicrophone.current;
2132
+ if (active) active.microphone.onCancel();
2133
+ else if (current?.state === "recording" || current?.state === "preparing") current.onCancel();
2134
+ };
2135
+ useLayoutEffect4(() => {
2136
+ currentMicrophone.current = microphone;
2137
+ }, [microphone]);
2138
+ useEffect5(() => {
2139
+ if (disabled) cancel();
2140
+ }, [disabled]);
2141
+ useEffect5(() => {
2142
+ if (!button) return;
2143
+ const document2 = button.ownerDocument;
2144
+ const view = document2.defaultView;
2145
+ const onVisibility = () => {
2146
+ if (document2.hidden) cancel();
2147
+ };
2148
+ const onEscape = (event) => {
2149
+ if (event.key === "Escape" && (press.current || currentMicrophone.current?.state === "recording" || currentMicrophone.current?.state === "preparing")) {
2150
+ event.preventDefault();
2151
+ cancel();
2152
+ }
2153
+ };
2154
+ view.addEventListener("blur", cancel);
2155
+ document2.addEventListener("visibilitychange", onVisibility);
2156
+ document2.addEventListener("keydown", onEscape);
2157
+ return () => {
2158
+ view.removeEventListener("blur", cancel);
2159
+ document2.removeEventListener("visibilitychange", onVisibility);
2160
+ document2.removeEventListener("keydown", onEscape);
2161
+ cancel();
2162
+ clearTimeout(clickReset.current);
2163
+ };
2164
+ }, [button]);
2165
+ const start = (source, timeStamp, pointerId, key) => {
2166
+ if (!voice || state === "preparing" || blocked || !microphone || press.current) return;
2167
+ press.current = { source, pointerId, key, microphone };
2168
+ clearTimeout(clickReset.current);
2169
+ suppressClick.current = true;
2170
+ microphone.onPressStart({ source, timeStamp });
2171
+ };
2172
+ const end = (timeStamp) => {
2173
+ const active = press.current;
2174
+ press.current = null;
2175
+ resetClick();
2176
+ active?.microphone.onPressEnd({ source: active.source, timeStamp });
2177
+ };
2178
+ const activationKey = (event) => (event.key === " " || event.key === "Enter") && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229;
2179
+ return /* @__PURE__ */ jsxs8(Fragment3, { children: [
2180
+ state === "recording" && button && /* @__PURE__ */ jsx16(RecordingAura, { anchor: button, level: microphone?.level }),
2181
+ /* @__PURE__ */ jsxs8(
2182
+ "button",
2183
+ {
2184
+ "aria-busy": busy || void 0,
2185
+ "aria-label": label,
2186
+ "aria-pressed": voice ? state === "recording" : void 0,
2187
+ className: "qsl:primary-action",
2188
+ "data-primary-action": state,
2189
+ disabled: blocked,
2190
+ onBlur: () => {
2191
+ if (press.current?.source === "keyboard") cancel();
2192
+ },
2193
+ onClick: (event) => {
2194
+ if (suppressClick.current) {
2195
+ suppressClick.current = false;
2196
+ return;
2197
+ }
2198
+ if (voice && event.detail === 0) {
2199
+ start("virtual", event.timeStamp);
2200
+ end(event.timeStamp);
2201
+ suppressClick.current = false;
2202
+ } else if (state === "send") onSend();
2203
+ else if (state === "stop") onStop?.();
2204
+ },
2205
+ onContextMenu: (event) => {
2206
+ if (voice) event.preventDefault();
2207
+ },
2208
+ onKeyDown: (event) => {
2209
+ if (activationKey(event) && event.repeat) {
2210
+ event.preventDefault();
2211
+ return;
2212
+ }
2213
+ if (!voice) {
2214
+ suppressClick.current = false;
2215
+ return;
2216
+ }
2217
+ if (activationKey(event)) {
2218
+ event.preventDefault();
2219
+ start("keyboard", event.timeStamp, void 0, event.key);
2220
+ }
2221
+ },
2222
+ onKeyUp: (event) => {
2223
+ if (activationKey(event) && (voice || press.current?.source === "keyboard")) {
2224
+ event.preventDefault();
2225
+ if (press.current?.source === "keyboard" && press.current.key === event.key) end(event.timeStamp);
2226
+ }
2227
+ },
2228
+ onLostPointerCapture: (event) => {
2229
+ if (press.current?.pointerId === event.pointerId) cancel();
2230
+ },
2231
+ onPointerCancel: (event) => {
2232
+ if (press.current?.pointerId === event.pointerId) cancel();
2233
+ },
2234
+ onPointerDown: (event) => {
2235
+ suppressClick.current = false;
2236
+ if (event.button !== 0 || event.isPrimary === false) return;
2237
+ start("pointer", event.timeStamp, event.pointerId);
2238
+ if (press.current?.pointerId === event.pointerId) event.currentTarget.setPointerCapture?.(event.pointerId);
2239
+ },
2240
+ onPointerUp: (event) => {
2241
+ if (press.current?.source === "pointer" && press.current.pointerId === event.pointerId) end(event.timeStamp);
2242
+ },
2243
+ ref: setButton,
2244
+ title: state === "microphone" ? texts.microphoneHint : label,
2245
+ type: "button",
2246
+ children: [
2247
+ /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", className: "qsl:primary-icon", "data-active": state === "microphone" || state === "recording", children: /* @__PURE__ */ jsx16(MicIcon, {}) }),
2248
+ /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", className: "qsl:primary-icon", "data-active": state === "send", children: /* @__PURE__ */ jsx16(SendIcon, {}) }),
2249
+ /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", className: "qsl:primary-icon", "data-active": state === "stop", children: /* @__PURE__ */ jsx16(SquareIcon2, { fill: "currentColor" }) }),
2250
+ /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", className: "qsl:primary-icon", "data-active": busy, children: /* @__PURE__ */ jsx16(LoaderCircleIcon, { className: "qsl:motion-safe:animate-spin" }) }),
2251
+ state === "recording" && /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", className: "qsl:recording-dot" })
2252
+ ]
2253
+ }
2254
+ )
2255
+ ] });
2256
+ }
2257
+
2258
+ // packages/quassel/src/ChatInputToolbar.tsx
2259
+ import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
2043
2260
  var COMPACT_WIDTH_PX = 480;
2044
2261
  var errorClasses = "qsl:px-3 qsl:py-2 qsl:text-small qsl:text-destructive";
2045
2262
  var attachmentItemClasses = "qsl:relative qsl:flex qsl:w-[190px] qsl:max-w-full qsl:min-w-0 qsl:flex-col qsl:overflow-hidden qsl:rounded-lg qsl:border qsl:border-border qsl:text-small";
@@ -2059,6 +2276,8 @@ function ChatInputToolbar({
2059
2276
  actions,
2060
2277
  toolbarLeft,
2061
2278
  toolbarRight,
2279
+ microphone,
2280
+ replaceInput,
2062
2281
  texts,
2063
2282
  handleRef,
2064
2283
  attachments: attachmentsEnabled = true,
@@ -2074,21 +2293,21 @@ function ChatInputToolbar({
2074
2293
  const sendScope = useContext4(ChatSendContext);
2075
2294
  const allTexts = { ...defaultTexts, ...texts };
2076
2295
  const { Button: Button2, StopButton: StopButton2, Toggle: Toggle2 } = useQuasselComponents();
2077
- const [draft, setDraft] = useState7(initialValue);
2078
- const [sending, setSending] = useState7(false);
2079
- const [sendError, setSendError] = useState7();
2080
- const [failedSubmissions, setFailedSubmissions] = useState7([]);
2081
- const nextFailedId = useRef6(0);
2082
- const pendingSend = useRef6(false);
2083
- const generation = useRef6(0);
2084
- useLayoutEffect3(() => () => {
2296
+ const [draft, setDraft] = useState8(initialValue);
2297
+ const [sending, setSending] = useState8(false);
2298
+ const [sendError, setSendError] = useState8();
2299
+ const [failedSubmissions, setFailedSubmissions] = useState8([]);
2300
+ const nextFailedId = useRef8(0);
2301
+ const pendingSend = useRef8(false);
2302
+ const generation = useRef8(0);
2303
+ useLayoutEffect5(() => () => {
2085
2304
  generation.current += 1;
2086
2305
  }, []);
2087
- const draftRevision = useRef6(0);
2088
- const [attachments, setAttachments] = useState7([]);
2089
- const attachmentsRef = useRef6([]);
2090
- const nextAttachmentId = useRef6(0);
2091
- const [attachmentError, setAttachmentError] = useState7();
2306
+ const draftRevision = useRef8(0);
2307
+ const [attachments, setAttachments] = useState8([]);
2308
+ const attachmentsRef = useRef8([]);
2309
+ const nextAttachmentId = useRef8(0);
2310
+ const [attachmentError, setAttachmentError] = useState8();
2092
2311
  const updateAttachments = (update) => {
2093
2312
  attachmentsRef.current = update(attachmentsRef.current);
2094
2313
  setAttachments(attachmentsRef.current);
@@ -2119,10 +2338,11 @@ function ChatInputToolbar({
2119
2338
  );
2120
2339
  }
2121
2340
  };
2122
- const rootRef = useRef6(null);
2123
- const textareaRef = useRef6(null);
2124
- const autoFocusSettled = useRef6(false);
2125
- useEffect5(() => {
2341
+ const rootRef = useRef8(null);
2342
+ const textareaRef = useRef8(null);
2343
+ const composing = useRef8(false);
2344
+ const autoFocusSettled = useRef8(false);
2345
+ useEffect6(() => {
2126
2346
  if (!autoFocus || autoFocusSettled.current) return;
2127
2347
  const input2 = textareaRef.current;
2128
2348
  if (!input2) return;
@@ -2167,8 +2387,8 @@ function ChatInputToolbar({
2167
2387
  window.removeEventListener("blur", settle);
2168
2388
  };
2169
2389
  }, [autoFocus, disabled, onAutoFocusSettled]);
2170
- const [compact, setCompact] = useState7(false);
2171
- const pendingFocus = useRef6(false);
2390
+ const [compact, setCompact] = useState8(false);
2391
+ const pendingFocus = useRef8(false);
2172
2392
  const fileInput = useFileInput({
2173
2393
  disabled: disabled || sending,
2174
2394
  onFiles: addFiles,
@@ -2184,17 +2404,21 @@ function ChatInputToolbar({
2184
2404
  });
2185
2405
  const toolbar = layout === "toolbar";
2186
2406
  const inline = layout === "inline";
2187
- const inputRows = inline || toolbar ? 1 : rows;
2188
- const inputMaxRows = toolbar ? 1 : inline ? maxRows ?? 1 : maxRows;
2407
+ const pill = layout === "pill";
2408
+ const inputRows = inline || toolbar || pill ? 1 : rows;
2409
+ const inputMaxRows = toolbar ? 1 : pill ? maxRows ?? 6 : inline ? maxRows ?? 1 : maxRows;
2410
+ const microphoneBusy = pill && !!microphone && microphone.state !== "idle";
2411
+ const replacingInput = pill && replaceInput !== void 0 && replaceInput !== null && typeof replaceInput !== "boolean";
2189
2412
  const rootClasses = cn(
2190
- "qsl:flex qsl:shrink-0 qsl:flex-col qsl:overflow-hidden qsl:rounded-[var(--qsl-input-card-radius,--theme(--radius-xl))] qsl:border qsl:border-border-strong qsl:bg-background qsl:shadow-bar qsl:transition-colors qsl:focus-within:border-primary",
2191
- "qsl:in-data-[tone=material]:rounded-xl qsl:in-data-[tone=material]:border-glass-edge/50 qsl:in-data-[tone=material]:bg-background",
2413
+ "qsl:flex qsl:shrink-0 qsl:flex-col",
2414
+ pill ? "qsl:min-w-0" : "qsl:overflow-hidden qsl:rounded-[var(--qsl-input-card-radius,--theme(--radius-xl))] qsl:border qsl:border-border-strong qsl:bg-background qsl:shadow-bar qsl:transition-colors qsl:focus-within:border-primary qsl:in-data-[tone=material]:rounded-xl qsl:in-data-[tone=material]:border-glass-edge/50 qsl:in-data-[tone=material]:bg-background",
2192
2415
  toolbar && "qsl:h-full qsl:min-w-0 qsl:flex-row qsl:items-center qsl:rounded-lg qsl:data-[working=true]:animate-working-pulse qsl:data-[working=true]:border-primary qsl:motion-reduce:data-[working=true]:animate-none",
2193
2416
  fileInput.dragging && "qsl:outline-2 qsl:outline-offset-[3px] qsl:outline-dashed qsl:outline-primary"
2194
2417
  );
2195
2418
  const textareaClasses = cn(
2196
- "qsl:resize-none qsl:overscroll-contain qsl:border-0 qsl:bg-transparent qsl:text-body qsl:text-foreground qsl:placeholder:text-muted-foreground qsl:focus:outline-none",
2197
- inline ? "qsl:min-w-0 qsl:flex-1 qsl:px-2.5 qsl:py-2" : toolbar ? "qsl:w-full qsl:min-w-0 qsl:flex-auto qsl:px-2 qsl:py-[3px]" : "qsl:px-3 qsl:pt-3 qsl:pb-2"
2419
+ "qsl:resize-none qsl:overscroll-contain qsl:border-0 qsl:bg-transparent qsl:text-foreground qsl:placeholder:text-muted-foreground",
2420
+ !pill && "qsl:text-body qsl:focus:outline-none",
2421
+ pill ? "qsl:pill-input" : inline ? "qsl:min-w-0 qsl:flex-1 qsl:px-2.5 qsl:py-2" : toolbar ? "qsl:w-full qsl:min-w-0 qsl:flex-auto qsl:px-2 qsl:py-[3px]" : "qsl:px-3 qsl:pt-3 qsl:pb-2"
2198
2422
  );
2199
2423
  const toolbarRowClasses = cn(
2200
2424
  "qsl:flex qsl:items-center qsl:gap-2 qsl:border-t qsl:border-border-soft qsl:px-2 qsl:py-1.5 qsl:in-data-[tone=overseer-details]:flex-nowrap",
@@ -2229,10 +2453,10 @@ function ChatInputToolbar({
2229
2453
  fileInput.clearDragging();
2230
2454
  }
2231
2455
  }));
2232
- useEffect5(() => {
2456
+ useEffect6(() => {
2233
2457
  onDraftChange?.(draft);
2234
2458
  }, [draft, onDraftChange]);
2235
- useEffect5(() => {
2459
+ useEffect6(() => {
2236
2460
  if (!pendingFocus.current) {
2237
2461
  return;
2238
2462
  }
@@ -2257,10 +2481,10 @@ function ChatInputToolbar({
2257
2481
  element.style.height = `${Math.min(contentHeight, maximum)}px`;
2258
2482
  element.style.overflowY = contentHeight > maximum ? "auto" : "hidden";
2259
2483
  }, [inputMaxRows, inputRows]);
2260
- useEffect5(() => {
2484
+ useEffect6(() => {
2261
2485
  applyAutoGrow();
2262
- }, [draft, applyAutoGrow]);
2263
- useEffect5(() => {
2486
+ }, [draft, replacingInput, applyAutoGrow]);
2487
+ useEffect6(() => {
2264
2488
  const element = textareaRef.current;
2265
2489
  if (!element || inputMaxRows === void 0) {
2266
2490
  return;
@@ -2274,8 +2498,8 @@ function ChatInputToolbar({
2274
2498
  });
2275
2499
  observer.observe(element);
2276
2500
  return () => observer.disconnect();
2277
- }, [inputMaxRows, applyAutoGrow]);
2278
- useEffect5(() => {
2501
+ }, [inputMaxRows, replacingInput, applyAutoGrow]);
2502
+ useEffect6(() => {
2279
2503
  const element = rootRef.current;
2280
2504
  if (!element) {
2281
2505
  return;
@@ -2287,7 +2511,7 @@ function ChatInputToolbar({
2287
2511
  return () => observer.disconnect();
2288
2512
  }, []);
2289
2513
  const hasAttachments = attachments.length > 0;
2290
- useEffect5(() => {
2514
+ useEffect6(() => {
2291
2515
  onAttachmentsChange?.(hasAttachments);
2292
2516
  }, [hasAttachments, onAttachmentsChange]);
2293
2517
  const capabilityError = hasAttachments && attachmentCapabilitiesError ? attachmentCapabilitiesError : attachmentCapabilities ? attachmentCapabilityError(attachments.map((entry) => ({
@@ -2298,13 +2522,13 @@ function ChatInputToolbar({
2298
2522
  const preparing = attachments.some((entry) => entry.status === "reading");
2299
2523
  const attachmentBlocked = attachments.some((entry) => entry.status !== "ready");
2300
2524
  const currentError = sendError ?? attachmentError ?? capabilityError ?? attachments.find((entry) => entry.status === "error")?.error;
2301
- useEffect5(() => {
2525
+ useEffect6(() => {
2302
2526
  onErrorChange?.(currentError);
2303
2527
  }, [currentError, onErrorChange]);
2304
2528
  const send = async (onSubmit = onSend, allowEmpty = false) => {
2305
2529
  const text = draft.trim();
2306
2530
  const snapshot = attachmentsRef.current;
2307
- if (!allowEmpty && !text && !snapshot.length || disabled || sendDisabled || pendingSend.current || capabilityError || snapshot.some((entry) => entry.status !== "ready")) {
2531
+ if (!allowEmpty && !text && !snapshot.length || disabled || sendDisabled || microphoneBusy || replacingInput || pendingSend.current || capabilityError || snapshot.some((entry) => entry.status !== "ready")) {
2308
2532
  return;
2309
2533
  }
2310
2534
  pendingSend.current = true;
@@ -2349,21 +2573,21 @@ function ChatInputToolbar({
2349
2573
  }
2350
2574
  };
2351
2575
  const onKeyDown = (event) => {
2352
- if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229 && (sendShortcut === "enter" || event.metaKey || event.ctrlKey)) {
2576
+ if (event.key === "Enter" && !event.shiftKey && !composing.current && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229 && (sendShortcut === "enter" || event.metaKey || event.ctrlKey)) {
2353
2577
  event.preventDefault();
2354
2578
  if (!event.repeat) void send();
2355
2579
  }
2356
2580
  };
2357
- const attachmentPreviews = attachments.length > 0 && /* @__PURE__ */ jsx15("ul", { className: "qsl:flex qsl:max-h-[240px] qsl:flex-wrap qsl:gap-2 qsl:overflow-y-auto qsl:overscroll-contain qsl:px-3 qsl:pt-2.5 qsl:in-data-[tone=overseer-details]:max-h-40 qsl:in-data-[tone=overseer-details]:overflow-y-auto", "aria-label": allTexts.attachments, children: attachments.map((entry) => /* @__PURE__ */ jsxs8("li", { className: attachmentItemClasses, children: [
2358
- entry.status === "ready" && entry.input.mediaType.startsWith("image/") && /* @__PURE__ */ jsx15("img", { className: attachmentMediaClasses, src: `data:${entry.input.mediaType};base64,${entry.input.data}`, alt: entry.file.name }),
2359
- entry.status === "ready" && entry.input.mediaType.startsWith("video/") && /* @__PURE__ */ jsx15("video", { className: attachmentMediaClasses, controls: true, preload: "metadata", "aria-label": entry.file.name, src: `data:${entry.input.mediaType};base64,${entry.input.data}` }),
2360
- /* @__PURE__ */ jsxs8("div", { className: "qsl:flex qsl:flex-col qsl:gap-1 qsl:p-2 qsl:pr-8 qsl:[overflow-wrap:anywhere]", children: [
2361
- /* @__PURE__ */ jsx15("span", { className: "qsl:font-medium", title: entry.file.name, children: entry.file.name }),
2362
- /* @__PURE__ */ jsx15("small", { children: formatAttachmentSize(entry.file.size) }),
2363
- entry.status === "reading" && /* @__PURE__ */ jsx15("small", { role: "status", children: allTexts.attachmentPreparing }),
2364
- entry.status === "error" && /* @__PURE__ */ jsx15("small", { role: "alert", children: entry.error })
2581
+ const attachmentPreviews = attachments.length > 0 && /* @__PURE__ */ jsx17("ul", { className: "qsl:flex qsl:max-h-[240px] qsl:flex-wrap qsl:gap-2 qsl:overflow-y-auto qsl:overscroll-contain qsl:px-3 qsl:pt-2.5 qsl:in-data-[tone=overseer-details]:max-h-40 qsl:in-data-[tone=overseer-details]:overflow-y-auto", "aria-label": allTexts.attachments, children: attachments.map((entry) => /* @__PURE__ */ jsxs9("li", { className: attachmentItemClasses, children: [
2582
+ entry.status === "ready" && entry.input.mediaType.startsWith("image/") && /* @__PURE__ */ jsx17("img", { className: attachmentMediaClasses, src: `data:${entry.input.mediaType};base64,${entry.input.data}`, alt: entry.file.name }),
2583
+ entry.status === "ready" && entry.input.mediaType.startsWith("video/") && /* @__PURE__ */ jsx17("video", { className: attachmentMediaClasses, controls: true, preload: "metadata", "aria-label": entry.file.name, src: `data:${entry.input.mediaType};base64,${entry.input.data}` }),
2584
+ /* @__PURE__ */ jsxs9("div", { className: "qsl:flex qsl:flex-col qsl:gap-1 qsl:p-2 qsl:pr-8 qsl:[overflow-wrap:anywhere]", children: [
2585
+ /* @__PURE__ */ jsx17("span", { className: "qsl:font-medium", title: entry.file.name, children: entry.file.name }),
2586
+ /* @__PURE__ */ jsx17("small", { children: formatAttachmentSize(entry.file.size) }),
2587
+ entry.status === "reading" && /* @__PURE__ */ jsx17("small", { role: "status", children: allTexts.attachmentPreparing }),
2588
+ entry.status === "error" && /* @__PURE__ */ jsx17("small", { role: "alert", children: entry.error })
2365
2589
  ] }),
2366
- /* @__PURE__ */ jsx15(
2590
+ /* @__PURE__ */ jsx17(
2367
2591
  Button2,
2368
2592
  {
2369
2593
  "aria-label": fillText(allTexts.removeAttachment, { name: entry.file.name }),
@@ -2377,16 +2601,16 @@ function ChatInputToolbar({
2377
2601
  size: "icon-sm",
2378
2602
  title: fillText(allTexts.removeAttachment, { name: entry.file.name }),
2379
2603
  variant: "outline",
2380
- children: /* @__PURE__ */ jsx15(XIcon3, {})
2604
+ children: /* @__PURE__ */ jsx17(XIcon3, {})
2381
2605
  }
2382
2606
  )
2383
2607
  ] }, entry.id)) });
2384
- const errors = /* @__PURE__ */ jsxs8(Fragment3, { children: [
2385
- capabilityError && /* @__PURE__ */ jsx15("p", { className: errorClasses, role: "alert", children: capabilityError }),
2386
- attachmentError && /* @__PURE__ */ jsx15("p", { className: errorClasses, role: "alert", children: attachmentError }),
2387
- sendError && /* @__PURE__ */ jsx15("p", { className: errorClasses, role: "alert", children: sendError }),
2388
- failedSubmissions.map((failed) => /* @__PURE__ */ jsxs8("div", { className: cn(errorClasses, "qsl:flex qsl:flex-wrap qsl:items-center qsl:gap-2"), role: "status", children: [
2389
- /* @__PURE__ */ jsxs8("span", { children: [
2608
+ const errors = /* @__PURE__ */ jsxs9(Fragment4, { children: [
2609
+ capabilityError && /* @__PURE__ */ jsx17("p", { className: errorClasses, role: "alert", children: capabilityError }),
2610
+ attachmentError && /* @__PURE__ */ jsx17("p", { className: errorClasses, role: "alert", children: attachmentError }),
2611
+ sendError && /* @__PURE__ */ jsx17("p", { className: errorClasses, role: "alert", children: sendError }),
2612
+ failedSubmissions.map((failed) => /* @__PURE__ */ jsxs9("div", { className: cn(errorClasses, "qsl:flex qsl:flex-wrap qsl:items-center qsl:gap-2"), role: "status", children: [
2613
+ /* @__PURE__ */ jsxs9("span", { children: [
2390
2614
  allTexts.notSent,
2391
2615
  " ",
2392
2616
  failed.draft.slice(0, 100),
@@ -2394,7 +2618,7 @@ function ChatInputToolbar({
2394
2618
  failed.attachments.length > 0 ? ` ${fillText(allTexts.notSentAttachments, { count: failed.attachments.length })}` : "",
2395
2619
  " "
2396
2620
  ] }),
2397
- /* @__PURE__ */ jsx15(Button2, { disabled: disabled || sending, size: "sm", variant: "outline", onClick: () => {
2621
+ /* @__PURE__ */ jsx17(Button2, { disabled: disabled || sending, size: "sm", variant: "outline", onClick: () => {
2398
2622
  try {
2399
2623
  validateAttachmentSelection(attachmentsRef.current.map((entry) => entry.file), failed.attachments.map((entry) => entry.file), allTexts);
2400
2624
  } catch (error) {
@@ -2410,9 +2634,9 @@ function ChatInputToolbar({
2410
2634
  }, children: allTexts.insertUnsent })
2411
2635
  ] }, failed.id))
2412
2636
  ] });
2413
- const sendButtons = /* @__PURE__ */ jsxs8(Fragment3, { children: [
2414
- running2 && onStop && !hasContent && /* @__PURE__ */ jsx15(StopButton2, { className: "qsl:flex-none", label: allTexts.stop, onClick: onStop, size: "icon-sm" }),
2415
- (!running2 || !onStop || hasContent) && /* @__PURE__ */ jsx15(
2637
+ const sendButtons = /* @__PURE__ */ jsxs9(Fragment4, { children: [
2638
+ running2 && onStop && !hasContent && /* @__PURE__ */ jsx17(StopButton2, { className: "qsl:flex-none", label: allTexts.stop, onClick: onStop, size: "icon-sm" }),
2639
+ (!running2 || !onStop || hasContent) && /* @__PURE__ */ jsx17(
2416
2640
  Button2,
2417
2641
  {
2418
2642
  "aria-label": allTexts.send,
@@ -2423,53 +2647,62 @@ function ChatInputToolbar({
2423
2647
  },
2424
2648
  size: "icon-sm",
2425
2649
  title: running2 ? allTexts.sendIntoRun : allTexts.send,
2426
- children: /* @__PURE__ */ jsx15(SendIcon, { size: 14 })
2650
+ children: /* @__PURE__ */ jsx17(SendIcon2, { size: 14 })
2427
2651
  }
2428
2652
  )
2429
2653
  ] });
2430
- const detailToolbar = /* @__PURE__ */ jsxs8("div", { className: toolbarRowClasses, "data-input": "controls", children: [
2431
- /* @__PURE__ */ jsxs8("div", { className: cn(toolbarSideClasses, "qsl:in-data-[tone=overseer-details]:flex-none", inline || toolbar ? "qsl:flex-none" : "qsl:flex-auto"), children: [
2432
- attachmentsEnabled && /* @__PURE__ */ jsx15(
2433
- Button2,
2434
- {
2435
- "aria-label": allTexts.attachFiles,
2436
- disabled: disabled || sending,
2437
- onClick: () => fileInput.inputRef.current?.click(),
2438
- size: "icon-sm",
2439
- title: fillText(allTexts.attachFilesHint, { count: MAX_CHAT_ATTACHMENTS, size: maxAttachmentSizeLabel }),
2440
- variant: "ghost",
2441
- children: /* @__PURE__ */ jsx15(PaperclipIcon, {})
2442
- }
2443
- ),
2444
- preparing && /* @__PURE__ */ jsx15("span", { className: cn("qsl:text-small qsl:text-muted-foreground", inline && "qsl:sr-only"), role: "status", children: allTexts.attachmentsPreparing }),
2445
- actions?.map((action, index) => action.active === void 0 ? /* @__PURE__ */ jsxs8(Button2, { disabled: action.disabled, onClick: action.onClick, size: "sm", title: action.title ?? action.label, variant: "ghost", children: [
2446
- action.icon,
2447
- action.label && /* @__PURE__ */ jsx15("span", { className: action.icon ? "qsl:in-data-[compact=true]:hidden" : void 0, children: action.label })
2448
- ] }, index) : /* @__PURE__ */ jsxs8(Toggle2, { disabled: action.disabled, onPressedChange: () => action.onClick(), pressed: action.active, size: "sm", title: action.title ?? action.label, children: [
2449
- action.icon,
2450
- action.label && /* @__PURE__ */ jsx15("span", { className: action.icon ? "qsl:in-data-[compact=true]:hidden" : void 0, children: action.label })
2451
- ] }, index)),
2654
+ const actionButtons = actions?.map((action, index) => action.active === void 0 ? /* @__PURE__ */ jsxs9(Button2, { "aria-label": action.label ?? action.title, disabled: action.disabled, onClick: action.onClick, size: "sm", title: action.title ?? action.label, variant: "ghost", children: [
2655
+ action.icon,
2656
+ action.label && /* @__PURE__ */ jsx17("span", { className: action.icon ? "qsl:in-data-[compact=true]:hidden" : void 0, children: action.label })
2657
+ ] }, index) : /* @__PURE__ */ jsxs9(Toggle2, { "aria-label": action.label ?? action.title, disabled: action.disabled, onPressedChange: () => action.onClick(), pressed: action.active, size: "sm", title: action.title ?? action.label, children: [
2658
+ action.icon,
2659
+ action.label && /* @__PURE__ */ jsx17("span", { className: action.icon ? "qsl:in-data-[compact=true]:hidden" : void 0, children: action.label })
2660
+ ] }, index));
2661
+ const attachButton = attachmentsEnabled && /* @__PURE__ */ jsx17(
2662
+ Button2,
2663
+ {
2664
+ "aria-label": allTexts.attachFiles,
2665
+ disabled: disabled || sending,
2666
+ onClick: () => fileInput.inputRef.current?.click(),
2667
+ size: "icon-sm",
2668
+ title: fillText(allTexts.attachFilesHint, { count: MAX_CHAT_ATTACHMENTS, size: maxAttachmentSizeLabel }),
2669
+ variant: "ghost",
2670
+ children: /* @__PURE__ */ jsx17(PaperclipIcon, {})
2671
+ }
2672
+ );
2673
+ const detailToolbar = /* @__PURE__ */ jsxs9("div", { className: toolbarRowClasses, "data-input": "controls", children: [
2674
+ /* @__PURE__ */ jsxs9("div", { className: cn(toolbarSideClasses, "qsl:in-data-[tone=overseer-details]:flex-none", inline || toolbar ? "qsl:flex-none" : "qsl:flex-auto"), children: [
2675
+ attachButton,
2676
+ preparing && /* @__PURE__ */ jsx17("span", { className: cn("qsl:text-small qsl:text-muted-foreground", inline && "qsl:sr-only"), role: "status", children: allTexts.attachmentsPreparing }),
2677
+ actionButtons,
2452
2678
  toolbarLeft
2453
2679
  ] }),
2454
- /* @__PURE__ */ jsxs8("div", { className: cn(toolbarSideClasses, "qsl:in-data-[tone=overseer-details]:flex-1", inline || toolbar ? "qsl:flex-none" : "qsl:flex-[0_1_auto]"), children: [
2680
+ /* @__PURE__ */ jsxs9("div", { className: cn(toolbarSideClasses, "qsl:in-data-[tone=overseer-details]:flex-1", inline || toolbar ? "qsl:flex-none" : "qsl:flex-[0_1_auto]"), children: [
2455
2681
  toolbarRight,
2456
2682
  !toolbar && sendButtons
2457
2683
  ] })
2458
2684
  ] });
2459
- const details = /* @__PURE__ */ jsxs8("div", { className: "qsl:min-w-0", "data-compact": compact, "data-quassel": "", children: [
2685
+ const details = /* @__PURE__ */ jsxs9("div", { className: "qsl:min-w-0", "data-compact": compact, "data-quassel": "", children: [
2460
2686
  attachmentPreviews,
2461
2687
  errors,
2462
2688
  detailToolbar
2463
2689
  ] });
2464
- const input = /* @__PURE__ */ jsx15(
2690
+ const input = /* @__PURE__ */ jsx17(
2465
2691
  "textarea",
2466
2692
  {
2467
2693
  "aria-controls": inputAriaControls,
2468
2694
  className: textareaClasses,
2469
2695
  "aria-label": allTexts.placeholder,
2470
- disabled: !toolbar && disabled,
2696
+ disabled: !toolbar && (disabled || microphoneBusy),
2697
+ hidden: replacingInput,
2471
2698
  readOnly: toolbar && disabled,
2472
2699
  onChange: (event) => changeDraft(event.target.value),
2700
+ onCompositionStart: () => {
2701
+ composing.current = true;
2702
+ },
2703
+ onCompositionEnd: () => {
2704
+ composing.current = false;
2705
+ },
2473
2706
  onKeyDown,
2474
2707
  placeholder: running2 ? allTexts.steeringPlaceholder : allTexts.placeholder,
2475
2708
  ref: textareaRef,
@@ -2477,7 +2710,7 @@ function ChatInputToolbar({
2477
2710
  value: draft
2478
2711
  }
2479
2712
  );
2480
- return /* @__PURE__ */ jsxs8(
2713
+ return /* @__PURE__ */ jsxs9(
2481
2714
  "div",
2482
2715
  {
2483
2716
  className: rootClasses,
@@ -2488,7 +2721,7 @@ function ChatInputToolbar({
2488
2721
  ref: rootRef,
2489
2722
  ...attachmentsEnabled ? fileInput.dropProps : {},
2490
2723
  children: [
2491
- attachmentsEnabled && /* @__PURE__ */ jsx15(
2724
+ attachmentsEnabled && /* @__PURE__ */ jsx17(
2492
2725
  "input",
2493
2726
  {
2494
2727
  "aria-label": allTexts.attachFiles,
@@ -2499,17 +2732,48 @@ function ChatInputToolbar({
2499
2732
  }
2500
2733
  ),
2501
2734
  !toolbar && attachmentPreviews,
2502
- inline ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2735
+ pill ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
2503
2736
  errors,
2504
- /* @__PURE__ */ jsxs8("div", { className: "qsl:flex qsl:min-w-0 qsl:items-end", children: [
2737
+ preparing && /* @__PURE__ */ jsx17("span", { className: "qsl:sr-only", role: "status", children: allTexts.attachmentsPreparing }),
2738
+ /* @__PURE__ */ jsxs9("div", { className: "qsl:pill-row", "data-input": "pill", children: [
2739
+ (actionButtons?.length || toolbarLeft) && /* @__PURE__ */ jsxs9("div", { className: "qsl:pill-slot", "data-input": "left", children: [
2740
+ actionButtons,
2741
+ toolbarLeft
2742
+ ] }),
2743
+ input,
2744
+ replacingInput && /* @__PURE__ */ jsx17("div", { className: "qsl:pill-replacement", "data-input": "replacement", children: replaceInput }),
2745
+ (toolbarRight || attachmentsEnabled) && /* @__PURE__ */ jsxs9("div", { className: "qsl:pill-slot", "data-input": "right", children: [
2746
+ toolbarRight,
2747
+ attachButton
2748
+ ] }),
2749
+ /* @__PURE__ */ jsx17(
2750
+ ComposerPrimaryAction,
2751
+ {
2752
+ disabled,
2753
+ hasContent,
2754
+ microphone,
2755
+ onSend: () => {
2756
+ void send();
2757
+ },
2758
+ onStop,
2759
+ running: running2,
2760
+ sendDisabled: sendDisabled || attachmentBlocked || !!capabilityError || replacingInput,
2761
+ sending,
2762
+ texts: allTexts
2763
+ }
2764
+ )
2765
+ ] })
2766
+ ] }) : inline ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
2767
+ errors,
2768
+ /* @__PURE__ */ jsxs9("div", { className: "qsl:flex qsl:min-w-0 qsl:items-end", children: [
2505
2769
  input,
2506
2770
  detailToolbar
2507
2771
  ] })
2508
2772
  ] }) : input,
2509
- toolbar ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2510
- /* @__PURE__ */ jsx15("div", { className: "qsl:flex qsl:flex-none qsl:items-center qsl:gap-1 qsl:p-1", children: sendButtons }),
2511
- detailsContainer && createPortal2(details, detailsContainer)
2512
- ] }) : !inline && /* @__PURE__ */ jsxs8(Fragment3, { children: [
2773
+ toolbar ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
2774
+ /* @__PURE__ */ jsx17("div", { className: "qsl:flex qsl:flex-none qsl:items-center qsl:gap-1 qsl:p-1", children: sendButtons }),
2775
+ detailsContainer && createPortal3(details, detailsContainer)
2776
+ ] }) : !inline && !pill && /* @__PURE__ */ jsxs9(Fragment4, { children: [
2513
2777
  errors,
2514
2778
  detailToolbar
2515
2779
  ] })
@@ -2520,7 +2784,7 @@ function ChatInputToolbar({
2520
2784
 
2521
2785
  // packages/quassel/src/DetailModeSwitch.tsx
2522
2786
  import { CircleDotIcon, EyeOffIcon, ListCollapseIcon, ListIcon, ShapesIcon, TagsIcon, TextAlignJustifyIcon } from "lucide-react";
2523
- import { jsx as jsx16 } from "react/jsx-runtime";
2787
+ import { jsx as jsx18 } from "react/jsx-runtime";
2524
2788
  var DETAIL_MODES = ["off", "current", "icons", "chips", "grouped", "compact", "full"];
2525
2789
  var TEXT_KEYS = {
2526
2790
  off: "detailModeOff",
@@ -2554,18 +2818,18 @@ function DetailModeSwitch({
2554
2818
  const next2 = modes[(current < 0 ? 0 : current + 1) % modes.length];
2555
2819
  const Icon = ICONS[mode];
2556
2820
  const title = `${allTexts.detailModeTitle}: ${detailModeLabel(mode, texts)}`;
2557
- return /* @__PURE__ */ jsx16(Button2, { "aria-label": title, className, onClick: () => onChange(next2), size: "icon-sm", title, variant: "ghost", children: /* @__PURE__ */ jsx16(Icon, {}) });
2821
+ return /* @__PURE__ */ jsx18(Button2, { "aria-label": title, className, onClick: () => onChange(next2), size: "icon-sm", title, variant: "ghost", children: /* @__PURE__ */ jsx18(Icon, {}) });
2558
2822
  }
2559
2823
 
2560
2824
  // packages/quassel/src/TranscriptModeSwitch.tsx
2561
2825
  import { MessagesSquareIcon, MessageSquareIcon } from "lucide-react";
2562
- import { jsx as jsx17 } from "react/jsx-runtime";
2826
+ import { jsx as jsx19 } from "react/jsx-runtime";
2563
2827
  function TranscriptModeSwitch({ mode, onChange, texts, className }) {
2564
2828
  const labels = { ...defaultTexts, ...texts };
2565
2829
  const { Button: Button2 } = useQuasselComponents();
2566
2830
  const latest = mode === "latest";
2567
2831
  const title = latest ? labels.showAllMessages : labels.showLatestExchange;
2568
- return /* @__PURE__ */ jsx17(
2832
+ return /* @__PURE__ */ jsx19(
2569
2833
  Button2,
2570
2834
  {
2571
2835
  "aria-label": title,
@@ -2575,19 +2839,19 @@ function TranscriptModeSwitch({ mode, onChange, texts, className }) {
2575
2839
  size: "icon-sm",
2576
2840
  title,
2577
2841
  variant: "ghost",
2578
- children: latest ? /* @__PURE__ */ jsx17(MessageSquareIcon, {}) : /* @__PURE__ */ jsx17(MessagesSquareIcon, {})
2842
+ children: latest ? /* @__PURE__ */ jsx19(MessageSquareIcon, {}) : /* @__PURE__ */ jsx19(MessagesSquareIcon, {})
2579
2843
  }
2580
2844
  );
2581
2845
  }
2582
2846
 
2583
2847
  // packages/quassel/src/TimestampSwitch.tsx
2584
2848
  import { ClockFadingIcon, ClockIcon } from "lucide-react";
2585
- import { jsx as jsx18 } from "react/jsx-runtime";
2849
+ import { jsx as jsx20 } from "react/jsx-runtime";
2586
2850
  function TimestampSwitch({ showTimestamps, onChange, texts, className }) {
2587
2851
  const labels = { ...defaultTexts, ...texts };
2588
2852
  const { Button: Button2 } = useQuasselComponents();
2589
2853
  const title = showTimestamps ? labels.hideTimestamps : labels.showTimestamps;
2590
- return /* @__PURE__ */ jsx18(
2854
+ return /* @__PURE__ */ jsx20(
2591
2855
  Button2,
2592
2856
  {
2593
2857
  "aria-label": title,
@@ -2597,7 +2861,7 @@ function TimestampSwitch({ showTimestamps, onChange, texts, className }) {
2597
2861
  size: "icon-sm",
2598
2862
  title,
2599
2863
  variant: "ghost",
2600
- children: showTimestamps ? /* @__PURE__ */ jsx18(ClockIcon, {}) : /* @__PURE__ */ jsx18(ClockFadingIcon, {})
2864
+ children: showTimestamps ? /* @__PURE__ */ jsx20(ClockIcon, {}) : /* @__PURE__ */ jsx20(ClockFadingIcon, {})
2601
2865
  }
2602
2866
  );
2603
2867
  }