@burdenoff/fe-libs 2026.1029.10 → 2026.1030.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 (43) hide show
  1. package/dist/shared/assistant/i18nManifest.d.ts +22 -0
  2. package/dist/shared/assistant/i18nManifest.js +22 -0
  3. package/dist/shared/assistant/index.d.ts +6 -0
  4. package/dist/shared/assistant/ui/AIAssistantPanel.d.ts +26 -0
  5. package/dist/shared/assistant/ui/AIAssistantPanel.js +197 -160
  6. package/dist/shared/assistant/ui/AssistantChatArea.d.ts +33 -0
  7. package/dist/shared/assistant/ui/AssistantChatArea.js +1463 -1397
  8. package/dist/shared/assistant/ui/AssistantLanguageMenu.d.ts +9 -1
  9. package/dist/shared/assistant/ui/AssistantLanguageMenu.js +22 -22
  10. package/dist/shared/assistant/ui/AssistantOverlay.d.ts +91 -3
  11. package/dist/shared/assistant/ui/AssistantOverlay.js +347 -127
  12. package/dist/shared/assistant/ui/assistantOverlayState.d.ts +224 -0
  13. package/dist/shared/assistant/ui/assistantOverlayState.js +116 -29
  14. package/dist/shared/assistant/ui/compact/AssistantConversationMenu.js +53 -52
  15. package/dist/shared/assistant/ui/compact/AssistantDiscardDraftDialog.js +3 -4
  16. package/dist/shared/assistant/ui/compact/AssistantLanguagePill.d.ts +29 -0
  17. package/dist/shared/assistant/ui/compact/AssistantLanguagePill.js +98 -0
  18. package/dist/shared/assistant/ui/compact/AssistantVoiceReady.d.ts +31 -0
  19. package/dist/shared/assistant/ui/compact/AssistantVoiceReady.js +41 -0
  20. package/dist/shared/assistant/ui/compact/AssistantWaitingChangeRow.d.ts +6 -0
  21. package/dist/shared/assistant/ui/compact/AssistantWaitingChangeRow.js +5 -5
  22. package/dist/shared/assistant/ui/compact/AssistantWindowHeader.d.ts +36 -0
  23. package/dist/shared/assistant/ui/compact/AssistantWindowHeader.js +68 -0
  24. package/dist/shared/assistant/ui/compact/windowView.d.ts +50 -0
  25. package/dist/shared/assistant/ui/compact/windowView.js +31 -0
  26. package/dist/shared/assistant/ui/composer/AssistantAttachmentButton.d.ts +8 -0
  27. package/dist/shared/assistant/ui/composer/AssistantAttachmentButton.js +34 -34
  28. package/dist/shared/assistant/ui/composer/AssistantComposer.d.ts +6 -0
  29. package/dist/shared/assistant/ui/composer/AssistantComposer.js +92 -90
  30. package/dist/shared/assistant/ui/composer/AssistantModeMenu.d.ts +6 -0
  31. package/dist/shared/assistant/ui/composer/AssistantModeMenu.js +83 -83
  32. package/dist/shared/assistant/ui/useAssistantVisualViewport.d.ts +3 -0
  33. package/dist/shared/assistant/ui/useAssistantVisualViewport.js +24 -0
  34. package/dist/shared/assistant/ui/voice/AssistantVoiceSession.d.ts +31 -0
  35. package/dist/shared/assistant/ui/voice/AssistantVoiceSession.js +270 -166
  36. package/dist/shared/assistant/ui/voice/VoiceStage.d.ts +8 -0
  37. package/dist/shared/assistant/ui/voice/VoiceStage.js +10 -10
  38. package/dist/shared/assistant/voice/voiceStartedCue.d.ts +11 -0
  39. package/dist/shared/assistant/voice/voiceStartedCue.js +7 -0
  40. package/dist/shared-assistant.js +266 -260
  41. package/dist/ui/modal-shell.d.ts +6 -1
  42. package/dist/ui/modal-shell.js +7 -6
  43. package/package.json +1 -1
@@ -0,0 +1,98 @@
1
+ import { useI18n as e } from "../../../providers/shell/I18nProvider.js";
2
+ import { EscapeLayer as t } from "../../../components/EscapeLayer.js";
3
+ import { useAssistantTr as n } from "../../i18n.js";
4
+ import { useAssistantStore as r } from "../../store.js";
5
+ import { replyFollowsInput as i, resolveLanguagePair as a } from "../../voice/languagePair.js";
6
+ import { AssistantLanguageMenu as o } from "../AssistantLanguageMenu.js";
7
+ import { useEffect as s, useId as c, useMemo as l, useRef as u, useState as d } from "react";
8
+ import { ChevronDown as f, Languages as p } from "lucide-react";
9
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
10
+ //#region src/shared/assistant/ui/compact/AssistantLanguagePill.tsx
11
+ var g = ({ voiceLive: g }) => {
12
+ let _ = n(), { locale: v } = e(), y = r((e) => e.assistantLanguage), b = r((e) => e.replyLanguage), x = l(() => a(y, b, v), [
13
+ y,
14
+ b,
15
+ v
16
+ ]), [S, C] = d(!1), w = u(null), T = u(null), E = c(), D = c();
17
+ S && g && C(!1);
18
+ let O = u(S);
19
+ s(() => {
20
+ O.current = S;
21
+ });
22
+ let k = u(y);
23
+ s(() => {
24
+ k.current !== y && (k.current = y, O.current && (C(!1), T.current?.focus({ preventScroll: !0 })));
25
+ }, [y]), s(() => {
26
+ if (!S) return;
27
+ let e = (e) => {
28
+ let t = e.target;
29
+ t instanceof Node && w.current?.contains(t) || C(!1);
30
+ };
31
+ return document.addEventListener("pointerdown", e), () => document.removeEventListener("pointerdown", e);
32
+ }, [S]);
33
+ let A = _("window.language", "Language"), j = _("window.languageLocked", "Change the language after you end voice"), M = !i(b) && x.reply.code !== x.input.code ? x.reply.label : null, N = (e) => {
34
+ C(!1), e && T.current?.focus({ preventScroll: !0 });
35
+ };
36
+ return /* @__PURE__ */ h("div", {
37
+ ref: w,
38
+ className: "relative min-w-0 flex-1",
39
+ "data-testid": "assistant-window-language-root",
40
+ children: [
41
+ /* @__PURE__ */ h("button", {
42
+ ref: T,
43
+ type: "button",
44
+ onClick: () => {
45
+ g || C((e) => !e);
46
+ },
47
+ className: `flex h-10 min-w-0 max-w-full items-center gap-1.5 rounded-full border border-border-default px-3 text-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] ${g ? "cursor-default text-text-muted" : "text-text-primary hover:bg-bg-sunken"}`,
48
+ "aria-expanded": S,
49
+ "aria-controls": S ? E : void 0,
50
+ "aria-disabled": g ? "true" : void 0,
51
+ "aria-describedby": g ? D : void 0,
52
+ "aria-label": `${A}: ${x.input.label}`,
53
+ title: g ? j : `${A}: ${x.input.label}`,
54
+ "data-testid": "assistant-window-language",
55
+ children: [
56
+ /* @__PURE__ */ m(p, {
57
+ className: "size-4 shrink-0",
58
+ "aria-hidden": "true"
59
+ }),
60
+ /* @__PURE__ */ m("span", {
61
+ className: "min-w-0 truncate",
62
+ "data-testid": "assistant-window-language-name",
63
+ children: x.input.label
64
+ }),
65
+ /* @__PURE__ */ m(f, {
66
+ className: "size-4 shrink-0",
67
+ "aria-hidden": "true"
68
+ })
69
+ ]
70
+ }),
71
+ g ? /* @__PURE__ */ m("span", {
72
+ id: D,
73
+ className: "sr-only",
74
+ "data-testid": "assistant-window-language-reason",
75
+ children: j
76
+ }) : null,
77
+ S ? /* @__PURE__ */ m(t, {
78
+ onDismiss: () => {
79
+ N(!0);
80
+ },
81
+ children: /* @__PURE__ */ h("div", {
82
+ id: E,
83
+ role: "group",
84
+ "aria-label": A,
85
+ className: "absolute start-0 top-full z-30 mt-1 w-72 max-w-[calc(100vw-2rem)] overflow-hidden rounded-card border border-border-default bg-bg-elevated py-1 shadow-[var(--shadow-overlay)]",
86
+ "data-testid": "assistant-window-language-menu",
87
+ children: [/* @__PURE__ */ m(o, { inputOnly: !0 }), M === null ? null : /* @__PURE__ */ m("p", {
88
+ className: "px-3 py-2 text-xs text-text-muted",
89
+ "data-testid": "assistant-window-language-pinned",
90
+ children: _("window.languagePinned", "Replies in {{language}} — change in full assistant", { language: M })
91
+ })]
92
+ })
93
+ }) : null
94
+ ]
95
+ });
96
+ };
97
+ //#endregion
98
+ export { g as AssistantLanguagePill };
@@ -0,0 +1,31 @@
1
+ import { FC } from 'react';
2
+ export interface AssistantVoiceReadyProps {
3
+ /**
4
+ * Why a voice conversation cannot start here and now (`voiceStartBlocker`),
5
+ * already worded. Null: it can, and Start voice is offered.
6
+ */
7
+ blockedReason: string | null;
8
+ /** Start the voice conversation: the chat area's own `startVoiceWithPrewarm`. */
9
+ onStart: () => void;
10
+ }
11
+ /**
12
+ * The Quick assistant window's Voice view, ready: not started (BOFF-10180, C2).
13
+ *
14
+ * ★ Nothing here listens. The brand orb is a still picture, with no ring, halo or
15
+ * motion, so it cannot read as "the microphone is open"; the status line says
16
+ * when it opens ("only when you press Start voice"); opening the window, or
17
+ * switching to this view, starts neither the microphone nor an avatar. The one
18
+ * thing that does is the person's press on Start voice, which is the composer's
19
+ * own start (`startVoiceWithPrewarm`).
20
+ *
21
+ * ★ Start voice is never auto-focused. Focus lands in the message box when the
22
+ * window opens, so a stray Enter cannot open the microphone.
23
+ *
24
+ * ★ Where voice cannot start (the mode takes none, the browser has no speech,
25
+ * the conversation is read-only, or it is busy) the reason takes the button's
26
+ * place, as a polite status, and there is no Start voice to press.
27
+ *
28
+ * ★ The orb is the brand's. Face stills belong to the avatar seam, which offers
29
+ * no accessor for the chosen face, and the window never starts an avatar.
30
+ */
31
+ export declare const AssistantVoiceReady: FC<AssistantVoiceReadyProps>;
@@ -0,0 +1,41 @@
1
+ import { useAssistantTr as e } from "../../i18n.js";
2
+ import { Mic as t } from "lucide-react";
3
+ import { jsx as n, jsxs as r } from "react/jsx-runtime";
4
+ //#region src/shared/assistant/ui/compact/AssistantVoiceReady.tsx
5
+ var i = ({ blockedReason: i, onStart: a }) => {
6
+ let o = e();
7
+ return /* @__PURE__ */ r("div", {
8
+ className: "flex min-h-0 flex-1 flex-col items-center justify-center gap-4 overflow-y-auto px-6 py-4 text-center",
9
+ "data-testid": "assistant-window-voice-ready",
10
+ children: [
11
+ /* @__PURE__ */ n("span", {
12
+ className: "size-28 shrink-0 rounded-full bg-gradient-to-br from-action-primary-bg/80 to-action-primary-bg/20 shadow-[var(--shadow-elevation-3)]",
13
+ "aria-hidden": "true",
14
+ "data-testid": "assistant-window-orb"
15
+ }),
16
+ /* @__PURE__ */ n("h2", {
17
+ className: "text-lg font-semibold text-text-primary",
18
+ children: o("window.voiceReadyTitle", "Talk to the assistant")
19
+ }),
20
+ /* @__PURE__ */ n("p", {
21
+ role: "status",
22
+ "aria-live": "polite",
23
+ className: "max-w-[16rem] text-sm text-text-secondary",
24
+ "data-testid": "assistant-window-voice-status",
25
+ children: i ?? o("window.voiceReadyStatus", "The microphone starts only when you press Start voice.")
26
+ }),
27
+ i === null ? /* @__PURE__ */ r("button", {
28
+ type: "button",
29
+ onClick: a,
30
+ className: "flex h-12 items-center gap-2 rounded-full bg-action-primary-bg px-6 text-base font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-bg-elevated)]",
31
+ "data-testid": "assistant-window-start-voice",
32
+ children: [/* @__PURE__ */ n(t, {
33
+ className: "size-5",
34
+ "aria-hidden": "true"
35
+ }), o("window.startVoice", "Start voice")]
36
+ }) : null
37
+ ]
38
+ });
39
+ };
40
+ //#endregion
41
+ export { i as AssistantVoiceReady };
@@ -4,6 +4,12 @@ export interface AssistantWaitingChangeRowProps {
4
4
  count: number;
5
5
  /** Scroll to the first waiting change's card and focus it. */
6
6
  onReview: () => void;
7
+ /**
8
+ * BOFF-10180 (the Quick assistant window): the card is in the full assistant,
9
+ * so the row's one button says so ("Review in full assistant") and `onReview`
10
+ * opens the full assistant. Still a pointer, never a second way to approve.
11
+ */
12
+ opensFull?: boolean;
7
13
  }
8
14
  /**
9
15
  * The pinned "change waiting" row above the composer (N6, 2026-10-29), in the
@@ -2,8 +2,8 @@ import { useAssistantTr as e } from "../../i18n.js";
2
2
  import { ShieldAlert as t } from "lucide-react";
3
3
  import { jsx as n, jsxs as r } from "react/jsx-runtime";
4
4
  //#region src/shared/assistant/ui/compact/AssistantWaitingChangeRow.tsx
5
- var i = ({ count: i, onReview: a }) => {
6
- let o = e();
5
+ var i = ({ count: i, onReview: a, opensFull: o = !1 }) => {
6
+ let s = e();
7
7
  return /* @__PURE__ */ r("div", {
8
8
  className: "mb-2 flex min-h-11 items-center gap-2 rounded-xl border border-status-warning bg-status-warning/10 px-3 py-1",
9
9
  "data-testid": "assistant-waiting-change",
@@ -17,15 +17,15 @@ var i = ({ count: i, onReview: a }) => {
17
17
  "aria-live": "polite",
18
18
  className: "min-w-0 flex-1 text-sm text-text-primary",
19
19
  "data-testid": "assistant-waiting-change-status",
20
- children: o("waitingChange.status", "{{count}} change(s) waiting for your approval", { count: i })
20
+ children: s("waitingChange.status", "{{count}} change(s) waiting for your approval", { count: i })
21
21
  }),
22
22
  /* @__PURE__ */ n("button", {
23
23
  type: "button",
24
24
  onClick: a,
25
25
  className: "min-h-9 shrink-0 rounded-button px-3 text-sm font-medium text-action-primary-bg transition-colors hover:bg-bg-sunken focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
26
- "aria-label": o("waitingChange.reviewLabel", "Review the change waiting for your approval"),
26
+ "aria-label": o ? void 0 : s("waitingChange.reviewLabel", "Review the change waiting for your approval"),
27
27
  "data-testid": "assistant-waiting-change-review",
28
- children: o("waitingChange.review", "Review")
28
+ children: o ? s("waitingChange.reviewInFull", "Review in full assistant") : s("waitingChange.review", "Review")
29
29
  })
30
30
  ]
31
31
  });
@@ -0,0 +1,36 @@
1
+ import { FC } from 'react';
2
+ import { AssistantWindowView } from '../assistantOverlayState';
3
+ export interface AssistantWindowHeaderProps {
4
+ /** The view the window shows. */
5
+ view: AssistantWindowView;
6
+ /** Chat or Voice was chosen. Presentation only: a live voice conversation carries on. */
7
+ onSelectView: (view: AssistantWindowView) => void;
8
+ /** A voice conversation is live: Minimize ends it, and the language waits. */
9
+ voiceLive: boolean;
10
+ /** Switch the same instance to the full assistant. Nothing is re-sent. */
11
+ onOpenFull: () => void;
12
+ /** Hide the window and keep everything in it (CW-M). */
13
+ onMinimize: () => void;
14
+ }
15
+ /**
16
+ * The Quick assistant window's header strip (BOFF-10178 Slice 3, BOFF-10180
17
+ * Slice 5):
18
+ *
19
+ * `[Chat | Voice] [文A English ▾] ………… [Open full assistant] [Minimize]`
20
+ *
21
+ * Four controls, 40 px each (C2). The pilot name is the panel's accessible name,
22
+ * so the strip spends its width on the controls.
23
+ *
24
+ * ★ The view toggle is a two-option segmented control with `aria-pressed`: each
25
+ * option names the view it SHOWS, and the one showing is pressed. It is
26
+ * presentation only: it starts nothing and ends nothing.
27
+ *
28
+ * ★ No Close (CW-M): the window is hidden, never closed, so a draft, its
29
+ * attachments and the conversation stay exactly where they are. Closing is the
30
+ * full panel's. While voice is live Minimize says it ENDS voice (VD): the hidden
31
+ * window keeps everything but the microphone, and nothing listens in the
32
+ * background.
33
+ *
34
+ * ★ Every control keeps one NAME (WCAG 2.5.3): the tooltip is the same words.
35
+ */
36
+ export declare const AssistantWindowHeader: FC<AssistantWindowHeaderProps>;
@@ -0,0 +1,68 @@
1
+ import { useAssistantTr as e } from "../../i18n.js";
2
+ import { AssistantLanguagePill as t } from "./AssistantLanguagePill.js";
3
+ import { AudioLines as n, MessageSquare as r, Minus as i, PanelRight as a } from "lucide-react";
4
+ import { jsx as o, jsxs as s } from "react/jsx-runtime";
5
+ //#region src/shared/assistant/ui/compact/AssistantWindowHeader.tsx
6
+ var c = "flex size-10 shrink-0 items-center justify-center rounded-button transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]", l = "text-text-secondary hover:bg-bg-sunken hover:text-text-primary", u = ({ view: u, onSelectView: d, voiceLive: f, onOpenFull: p, onMinimize: m }) => {
7
+ let h = e(), g = h("window.openFull", "Open full assistant"), _ = f ? h("window.minimizeEndsVoice", "Minimize (ends voice)") : h("window.minimize", "Minimize"), v = h("window.viewChat", "Chat"), y = h("window.viewVoice", "Voice"), b = (e, t, n) => {
8
+ let r = u === e;
9
+ return /* @__PURE__ */ o("button", {
10
+ type: "button",
11
+ onClick: () => d(e),
12
+ className: `flex size-10 shrink-0 items-center justify-center rounded-full transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] ${r ? "bg-[var(--color-accent-soft)] text-action-primary-bg" : "text-text-secondary hover:bg-bg-sunken hover:text-text-primary"}`,
13
+ "aria-pressed": r,
14
+ "aria-label": t,
15
+ title: t,
16
+ "data-testid": `assistant-window-view-${e}`,
17
+ children: n
18
+ });
19
+ };
20
+ return /* @__PURE__ */ s("div", {
21
+ className: "flex min-h-14 items-center gap-1 border-b border-border-default px-2 py-2",
22
+ "data-testid": "assistant-window-header",
23
+ "data-presentation": "compact",
24
+ "data-surface": "window",
25
+ children: [
26
+ /* @__PURE__ */ s("div", {
27
+ role: "group",
28
+ "aria-label": h("window.view", "View"),
29
+ className: "flex shrink-0 items-center rounded-full border border-border-default",
30
+ "data-testid": "assistant-window-view",
31
+ children: [b("chat", v, /* @__PURE__ */ o(r, {
32
+ className: "size-5",
33
+ "aria-hidden": "true"
34
+ })), b("voice", y, /* @__PURE__ */ o(n, {
35
+ className: "size-5",
36
+ "aria-hidden": "true"
37
+ }))]
38
+ }),
39
+ /* @__PURE__ */ o(t, { voiceLive: f }),
40
+ /* @__PURE__ */ o("button", {
41
+ type: "button",
42
+ onClick: () => p(),
43
+ className: `${c} ${l}`,
44
+ "aria-label": g,
45
+ title: g,
46
+ "data-testid": "assistant-open-full",
47
+ children: /* @__PURE__ */ o(a, {
48
+ className: "size-5",
49
+ "aria-hidden": "true"
50
+ })
51
+ }),
52
+ /* @__PURE__ */ o("button", {
53
+ type: "button",
54
+ onClick: m,
55
+ className: `${c} ${l}`,
56
+ "aria-label": _,
57
+ title: _,
58
+ "data-testid": "assistant-minimize",
59
+ children: /* @__PURE__ */ o(i, {
60
+ className: "size-5",
61
+ "aria-hidden": "true"
62
+ })
63
+ })
64
+ ]
65
+ });
66
+ };
67
+ //#endregion
68
+ export { u as AssistantWindowHeader };
@@ -0,0 +1,50 @@
1
+ import { isAssistantWindowView, AssistantWindowView } from '../assistantOverlayState';
2
+ export { isAssistantWindowView, type AssistantWindowView };
3
+ /**
4
+ * CW-V: the view the window shows.
5
+ *
6
+ * - Not live: the LAST USED view; the first time (nothing stored) Voice where
7
+ * voice is available here, else Chat.
8
+ * - Live: the voice view, which is where a conversation that has just started is
9
+ * (starting it from the Chat view moves the window to it), unless the person
10
+ * stepped to Chat since. That step is NOT the last-used view: it lasts as long
11
+ * as the conversation does.
12
+ */
13
+ export declare function resolveWindowView(input: {
14
+ /** The last-used view the overlay remembers, or null when none was ever chosen. */
15
+ stored: AssistantWindowView | null;
16
+ /** Voice can start here (speech works and the mode takes it). */
17
+ voiceAvailable: boolean;
18
+ /** A voice session is live. */
19
+ voiceLive: boolean;
20
+ /** The view the person chose during the live conversation. */
21
+ liveView: AssistantWindowView;
22
+ }): AssistantWindowView;
23
+ /** What the chat area tells the panel about voice in the window. */
24
+ export interface WindowVoiceState {
25
+ /** Voice can start here: `voiceSession.available` and the active mode takes it. */
26
+ available: boolean;
27
+ /** A session is live. */
28
+ live: boolean;
29
+ }
30
+ /** Until the chat area reports: not available, not live. */
31
+ export declare const WINDOW_VOICE_IDLE: WindowVoiceState;
32
+ /**
33
+ * The window's view and how a person changes it. A choice made while voice is
34
+ * live is the live conversation's alone; any other choice is remembered
35
+ * (`onRemember`, the overlay's storage). Each new conversation starts on Voice.
36
+ *
37
+ * ★ The state is adjusted DURING render, conditional on the flag it follows, so
38
+ * no frame shows the wrong view.
39
+ */
40
+ export declare function useAssistantWindowView(args: {
41
+ stored: AssistantWindowView | null;
42
+ onRemember: ((view: AssistantWindowView) => void) | undefined;
43
+ voice: WindowVoiceState;
44
+ }): {
45
+ view: AssistantWindowView;
46
+ /** The person chose a view in the header. */
47
+ select: (view: AssistantWindowView) => void;
48
+ /** The view is where the person IS now (Continue in chat): remembered, never a live-only choice. */
49
+ remember: (view: AssistantWindowView) => void;
50
+ };
@@ -0,0 +1,31 @@
1
+ import "../assistantOverlayState.js";
2
+ import { useCallback as e, useState as t } from "react";
3
+ //#region src/shared/assistant/ui/compact/windowView.ts
4
+ function n(e) {
5
+ return e.voiceLive ? e.liveView : e.stored ?? (e.voiceAvailable ? "voice" : "chat");
6
+ }
7
+ var r = {
8
+ available: !1,
9
+ live: !1
10
+ };
11
+ function i(r) {
12
+ let { stored: i, onRemember: a, voice: o } = r, [s, c] = t("voice"), [l, u] = t(null), [d, f] = t(o.live);
13
+ d !== o.live && (f(o.live), o.live && c("voice"));
14
+ let p = n({
15
+ stored: l ?? i,
16
+ voiceAvailable: o.available,
17
+ voiceLive: o.live,
18
+ liveView: s
19
+ }), m = o.live, h = e((e) => {
20
+ u(e), a?.(e);
21
+ }, [a]);
22
+ return {
23
+ view: p,
24
+ select: e((e) => {
25
+ m ? c(e) : h(e);
26
+ }, [m, h]),
27
+ remember: h
28
+ };
29
+ }
30
+ //#endregion
31
+ export { r as WINDOW_VOICE_IDLE, n as resolveWindowView, i as useAssistantWindowView };
@@ -32,6 +32,14 @@ interface Props {
32
32
  * into them; and a product composing its own chrome keeps the icon it has.
33
33
  */
34
34
  showLabel?: boolean;
35
+ /**
36
+ * BOFF-10178 (compact only): keep the source menu inside the VISUAL viewport.
37
+ * On a phone with the keyboard up the sheet is only as tall as the visual
38
+ * viewport leaves it (`--assistant-viewport-height`), and the menu opens
39
+ * upward inside it, so its height is capped to what the sheet can hold and it
40
+ * scrolls past that. Absent, the menu is exactly what it was.
41
+ */
42
+ capMenuHeight?: boolean;
35
43
  }
36
44
  /**
37
45
  * Leading circular "+" attach control in the composer.
@@ -9,88 +9,88 @@ import { useCallback as c, useEffect as l, useMemo as u, useRef as d, useState a
9
9
  import { Camera as p, FileText as m, FolderOpen as h, ImageIcon as g, Plus as _ } from "lucide-react";
10
10
  import { jsx as v, jsxs as y } from "react/jsx-runtime";
11
11
  //#region src/shared/assistant/ui/composer/AssistantAttachmentButton.tsx
12
- var b = "flex size-9 items-center justify-center rounded-full border border-border-subtle bg-bg-elevated text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong disabled:cursor-not-allowed disabled:text-text-muted disabled:hover:bg-bg-elevated", x = `${b} sm:w-auto sm:gap-1.5 sm:px-3`, S = {
12
+ var b = " max-h-[calc(var(--assistant-viewport-height,100dvh)-10rem)] overflow-y-auto", x = "flex size-9 items-center justify-center rounded-full border border-border-subtle bg-bg-elevated text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong disabled:cursor-not-allowed disabled:text-text-muted disabled:hover:bg-bg-elevated", S = `${x} sm:w-auto sm:gap-1.5 sm:px-3`, C = {
13
13
  camera: p,
14
14
  gallery: g,
15
15
  documents: m,
16
16
  files: h
17
- }, C = ({ onFiles: p, onPickerOpen: m, direction: h = "up", canAddMore: g, disabled: C = !1, showLabel: w = !1 }) => {
18
- let T = t(), E = d(null), D = d(null), O = d(null), [k, A] = f(!1), j = d(m);
19
- j.current = m;
20
- let M = u(() => n(r()), []), N = i(a()), P = g || N, F = g ? N ? `${T("composer.attach", "Attach files or photos")} · ${T("composer.connectSource", "Connect a source")}` : T("composer.attach", "Attach files or photos") : T("composer.attachFull", "Attachment limit reached ({{max}})", { max: 5 }), I = w && g && N ? T("composer.connectSource", "Connect a source") : null, L = s(O, () => A(!1));
17
+ }, w = ({ onFiles: p, onPickerOpen: m, direction: h = "up", canAddMore: g, disabled: w = !1, showLabel: T = !1, capMenuHeight: E = !1 }) => {
18
+ let D = t(), O = d(null), k = d(null), A = d(null), [j, M] = f(!1), N = d(m);
19
+ N.current = m;
20
+ let P = u(() => n(r()), []), F = i(a()), I = g || F, L = g ? F ? `${D("composer.attach", "Attach files or photos")} · ${D("composer.connectSource", "Connect a source")}` : D("composer.attach", "Attach files or photos") : D("composer.attachFull", "Attachment limit reached ({{max}})", { max: 5 }), R = T && g && F ? D("composer.connectSource", "Connect a source") : null, z = s(A, () => M(!1));
21
21
  l(() => {
22
- if (!k) return;
22
+ if (!j) return;
23
23
  let e = (e) => {
24
24
  let t = e.target;
25
- D.current?.contains(t) || O.current?.contains(t) || A(!1);
25
+ k.current?.contains(t) || A.current?.contains(t) || M(!1);
26
26
  };
27
27
  return window.addEventListener("mousedown", e), () => {
28
28
  window.removeEventListener("mousedown", e);
29
29
  };
30
- }, [k]);
31
- let R = c((e) => {
30
+ }, [j]);
31
+ let B = c((e) => {
32
32
  p(e.target.files), e.target.value = "";
33
- }, [p]), z = c((e) => {
34
- let t = E.current;
35
- t && (t.accept = e.accept, t.multiple = e.multiple, e.capture ? t.setAttribute("capture", e.capture) : t.removeAttribute("capture"), A(!1), j.current?.(), t.click());
33
+ }, [p]), V = c((e) => {
34
+ let t = O.current;
35
+ t && (t.accept = e.accept, t.multiple = e.multiple, e.capture ? t.setAttribute("capture", e.capture) : t.removeAttribute("capture"), M(!1), N.current?.(), t.click());
36
36
  }, []);
37
37
  return /* @__PURE__ */ y("div", {
38
38
  className: "relative flex-shrink-0",
39
39
  children: [
40
40
  /* @__PURE__ */ v("input", {
41
- ref: E,
41
+ ref: O,
42
42
  type: "file",
43
43
  multiple: !0,
44
44
  className: "hidden",
45
- onChange: R,
45
+ onChange: B,
46
46
  tabIndex: -1,
47
47
  "aria-hidden": "true",
48
48
  "data-testid": "assistant-attach-input"
49
49
  }),
50
50
  /* @__PURE__ */ y("button", {
51
- ref: O,
51
+ ref: A,
52
52
  type: "button",
53
- onClick: () => A((e) => !e),
54
- disabled: C || !P,
55
- title: F,
56
- "aria-label": F,
53
+ onClick: () => M((e) => !e),
54
+ disabled: w || !I,
55
+ title: L,
56
+ "aria-label": L,
57
57
  "aria-haspopup": "menu",
58
- "aria-expanded": k,
59
- className: I ? x : b,
58
+ "aria-expanded": j,
59
+ className: R ? S : x,
60
60
  "data-testid": "assistant-attach",
61
- children: [/* @__PURE__ */ v(_, { className: "size-4" }), I ? /* @__PURE__ */ v("span", {
61
+ children: [/* @__PURE__ */ v(_, { className: "size-4" }), R ? /* @__PURE__ */ v("span", {
62
62
  className: "hidden max-w-[10rem] truncate text-[13px] font-medium leading-tight sm:block",
63
63
  "data-testid": "assistant-attach-label",
64
- children: I
64
+ children: R
65
65
  }) : null]
66
66
  }),
67
- k ? /* @__PURE__ */ v(e, {
68
- ...L,
67
+ j ? /* @__PURE__ */ v(e, {
68
+ ...z,
69
69
  children: /* @__PURE__ */ y("div", {
70
- ref: D,
70
+ ref: k,
71
71
  role: "menu",
72
- "aria-label": T("composer.attachSource", "Attach from"),
73
- className: `absolute ${h === "up" ? "bottom-full mb-2" : "top-full mt-2"} left-0 z-30 min-w-44 overflow-hidden rounded-xl border border-border-default bg-bg-elevated py-1 shadow-lg`,
72
+ "aria-label": D("composer.attachSource", "Attach from"),
73
+ className: `absolute ${h === "up" ? "bottom-full mb-2" : "top-full mt-2"} left-0 z-30 min-w-44 overflow-hidden rounded-xl border border-border-default bg-bg-elevated py-1 shadow-lg${E ? b : ""}`,
74
74
  "data-testid": "assistant-attach-menu",
75
- children: [M.map((e) => {
76
- let t = S[e.id];
75
+ children: [P.map((e) => {
76
+ let t = C[e.id];
77
77
  return /* @__PURE__ */ y("button", {
78
78
  type: "button",
79
79
  role: "menuitem",
80
80
  disabled: !g,
81
- onClick: () => z(e),
81
+ onClick: () => V(e),
82
82
  className: "flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-text-primary transition-colors hover:bg-bg-sunken focus:outline-none focus-visible:bg-bg-sunken disabled:cursor-not-allowed disabled:text-text-muted disabled:hover:bg-transparent",
83
83
  "data-testid": `assistant-attach-source-${e.id}`,
84
84
  children: [/* @__PURE__ */ v(t, {
85
85
  className: "size-4 flex-shrink-0 text-text-secondary",
86
86
  "aria-hidden": "true"
87
- }), T(e.labelKey, e.labelFallback)]
87
+ }), D(e.labelKey, e.labelFallback)]
88
88
  }, e.id);
89
- }), /* @__PURE__ */ v(o, { onActivate: () => A(!1) })]
89
+ }), /* @__PURE__ */ v(o, { onActivate: () => M(!1) })]
90
90
  })
91
91
  }) : null
92
92
  ]
93
93
  });
94
94
  };
95
95
  //#endregion
96
- export { C as AssistantAttachmentButton };
96
+ export { w as AssistantAttachmentButton };
@@ -115,6 +115,12 @@ interface Props {
115
115
  * "change waiting" row. Classic never draws it.
116
116
  */
117
117
  aboveCard?: ReactNode;
118
+ /**
119
+ * BOFF-10180 (compact only): show the mode as a read-only label, not a menu. The
120
+ * Quick assistant window names the mode it is in and leaves changing it to the
121
+ * full panel (DESIGN §3.3). Absent: the Mode menu.
122
+ */
123
+ modeReadOnly?: boolean;
118
124
  }
119
125
  /** A local draft snapshot; no files are uploaded by observing it. */
120
126
  export interface AssistantComposerDraft {