@runtypelabs/persona 4.19.0 → 4.20.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.
Files changed (57) hide show
  1. package/dist/approval-ui.d.cts +77 -5
  2. package/dist/approval-ui.d.ts +77 -5
  3. package/dist/artifacts-ui.d.cts +77 -5
  4. package/dist/artifacts-ui.d.ts +77 -5
  5. package/dist/codegen.cjs +1 -1
  6. package/dist/codegen.js +1 -1
  7. package/dist/context-mentions.d.cts +77 -5
  8. package/dist/context-mentions.d.ts +77 -5
  9. package/dist/event-stream-view.d.cts +77 -5
  10. package/dist/event-stream-view.d.ts +77 -5
  11. package/dist/forms-ui.d.cts +77 -5
  12. package/dist/forms-ui.d.ts +77 -5
  13. package/dist/index.cjs +50 -50
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +78 -6
  16. package/dist/index.d.ts +78 -6
  17. package/dist/index.global.js +31 -31
  18. package/dist/index.global.js.map +1 -1
  19. package/dist/index.js +50 -50
  20. package/dist/index.js.map +1 -1
  21. package/dist/launcher.global.js.map +1 -1
  22. package/dist/session-reconnect.d.cts +77 -5
  23. package/dist/session-reconnect.d.ts +77 -5
  24. package/dist/smart-dom-reader.d.cts +77 -5
  25. package/dist/smart-dom-reader.d.ts +77 -5
  26. package/dist/theme-editor-preview.cjs +49 -49
  27. package/dist/theme-editor-preview.d.cts +77 -5
  28. package/dist/theme-editor-preview.d.ts +77 -5
  29. package/dist/theme-editor-preview.js +28 -28
  30. package/dist/theme-editor.d.cts +77 -5
  31. package/dist/theme-editor.d.ts +77 -5
  32. package/dist/theme-reference.cjs +1 -1
  33. package/dist/theme-reference.d.cts +46 -5
  34. package/dist/theme-reference.d.ts +46 -5
  35. package/dist/theme-reference.js +1 -1
  36. package/dist/widget.css +1 -1
  37. package/package.json +1 -1
  38. package/src/components/composer-actions.ts +27 -1
  39. package/src/components/composer-model-picker.ts +4 -14
  40. package/src/components/composer-overflow-menu.ts +14 -0
  41. package/src/components/composer-parts.ts +20 -0
  42. package/src/index-core.ts +2 -0
  43. package/src/styles/widget-styles.test.ts +83 -1
  44. package/src/styles/widget.css +50 -14
  45. package/src/theme-reference.ts +1 -1
  46. package/src/types/theme.ts +47 -5
  47. package/src/types.ts +33 -0
  48. package/src/ui.composer-action-visibility.test.ts +202 -0
  49. package/src/ui.composer-models.test.ts +37 -0
  50. package/src/ui.composer-modes.test.ts +82 -0
  51. package/src/ui.composer-overflow.test.ts +78 -0
  52. package/src/utils/__snapshots__/tokens-parity.test.ts.snap +1633 -686
  53. package/src/utils/composer-modes.test.ts +33 -0
  54. package/src/utils/composer-modes.ts +19 -2
  55. package/src/utils/tokens-parity.test.ts +20 -0
  56. package/src/utils/tokens.test.ts +76 -0
  57. package/src/utils/tokens.ts +4 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "4.19.0",
3
+ "version": "4.20.0",
4
4
  "description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -32,6 +32,7 @@ import type {
32
32
  ComposerActionOverflowConfig,
33
33
  ComposerActionPlacement,
34
34
  ComposerActionPresentation,
35
+ ComposerActionVisibility,
35
36
  ComposerButtonAction,
36
37
  ComposerCustomAction,
37
38
  ComposerState,
@@ -345,6 +346,28 @@ const evaluate = (
345
346
  return typeof value === "function" ? value(state) === true : value === true;
346
347
  };
347
348
 
349
+ /**
350
+ * The draft state `sendButton.visibility` reads: text, a pending attachment, or
351
+ * a live stream all count as "there is something in flight".
352
+ */
353
+ const hasDraftContent = (state: Readonly<ComposerState>): boolean =>
354
+ state.phase === "streaming" ||
355
+ state.text.trim().length > 0 ||
356
+ state.attachments.length > 0;
357
+
358
+ /**
359
+ * `ComposerAction.visibility` against the current draft. Evaluated on every
360
+ * state-driven pass, so the answer is always render-time, never patched on.
361
+ */
362
+ export function matchesComposerActionVisibility(
363
+ visibility: ComposerActionVisibility | undefined,
364
+ state: Readonly<ComposerState>
365
+ ): boolean {
366
+ if (!visibility || visibility === "always") return true;
367
+ const drafting = hasDraftContent(state);
368
+ return visibility === "when-text" ? drafting : !drafting;
369
+ }
370
+
348
371
  const isCustom = (action: ComposerAction): action is ComposerCustomAction =>
349
372
  action.kind === "custom";
350
373
 
@@ -937,7 +960,10 @@ export function createComposerActionRenderer(
937
960
  let entry = entries.get(resolved.id);
938
961
  if (!entry) continue;
939
962
  const action = resolved.action;
940
- const visible = action ? evaluate(action.visible, state, true) : true;
963
+ const visible = action
964
+ ? evaluate(action.visible, state, true) &&
965
+ matchesComposerActionVisibility(action.visibility, state)
966
+ : true;
941
967
  if (entry.visible !== visible) {
942
968
  entry.visible = visible;
943
969
  // Managed elements leave the DOM when hidden; the node (and any custom
@@ -18,6 +18,7 @@ import { createPopover, type PopoverHandle } from "../plugin-kit";
18
18
  import type { ComposerCustomAction, ComposerModel } from "../types";
19
19
  import { createElement, createNode } from "../utils/dom";
20
20
  import { renderLucideIcon } from "../utils/icons";
21
+ import { forwardMenuTokens } from "./composer-parts";
21
22
 
22
23
  export const COMPOSER_MODEL_PICKER_ACTION_ID = "core:model";
23
24
  export const COMPOSER_MODEL_PICKER_ORDER = 700;
@@ -25,9 +26,8 @@ export const COMPOSER_MODEL_PICKER_ORDER = 700;
25
26
  const ROW_ICON_SIZE = 16;
26
27
 
27
28
  /**
28
- * Tokens the popover panel reads. It is portaled outside the themed mount, so
29
- * configured values are forwarded onto it on open; unset ones stamp nothing and
30
- * the stylesheet's fallbacks stand.
29
+ * Tokens the popover panel reads, forwarded onto the portaled panel on every
30
+ * open; unset ones clear so the stylesheet's fallbacks stand.
31
31
  */
32
32
  const MENU_TOKEN_VARS = [
33
33
  "--persona-components-composer-modelPicker-menuBackground",
@@ -62,16 +62,6 @@ export interface ComposerModelPicker {
62
62
  repaint: () => void;
63
63
  }
64
64
 
65
- const forwardMenuTokens = (from: HTMLElement, to: HTMLElement): void => {
66
- const view = from.ownerDocument?.defaultView;
67
- if (!view) return;
68
- const computed = view.getComputedStyle(from);
69
- for (const name of MENU_TOKEN_VARS) {
70
- const value = computed.getPropertyValue(name).trim();
71
- if (value) to.style.setProperty(name, value);
72
- }
73
- };
74
-
75
65
  /** The wrapper both presentations share: one grid cell, control plus chevron. */
76
66
  const createWrapper = (): { wrapper: HTMLElement; chevron: HTMLElement } => {
77
67
  // `appearance: none` drops the platform chevron, which Chrome draws
@@ -221,7 +211,7 @@ export function createComposerModelPickerAction(
221
211
  onDismiss: () => trigger.setAttribute("aria-expanded", "false"),
222
212
  });
223
213
  }
224
- forwardMenuTokens(trigger, menu);
214
+ forwardMenuTokens(trigger, menu, MENU_TOKEN_VARS);
225
215
  popover.open();
226
216
  trigger.setAttribute("aria-expanded", "true");
227
217
  const ids = [...rows.keys()];
@@ -19,6 +19,7 @@ import {
19
19
  COMPOSER_CONTROL_CLASS,
20
20
  COMPOSER_CONTROL_GLYPH_CLASS,
21
21
  COMPOSER_CONTROL_ICON_FALLBACK_PX,
22
+ forwardMenuTokens,
22
23
  } from "./composer-parts";
23
24
 
24
25
  export interface ComposerOverflowMenuOptions {
@@ -54,6 +55,18 @@ export interface ComposerOverflowMenu {
54
55
  const focusableSelector =
55
56
  'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
56
57
 
58
+ /**
59
+ * Panel surface tokens, forwarded onto the portaled panel on every open; unset
60
+ * ones clear so the stylesheet's fallbacks stand.
61
+ */
62
+ const MENU_TOKEN_VARS = [
63
+ "--persona-components-composer-overflowMenu-background",
64
+ "--persona-components-composer-overflowMenu-borderColor",
65
+ "--persona-components-composer-overflowMenu-borderRadius",
66
+ "--persona-components-composer-overflowMenu-foreground",
67
+ "--persona-components-composer-overflowMenu-shadow",
68
+ ] as const;
69
+
57
70
  /**
58
71
  * Set on the menu row that holds keyboard focus. Rows that shade on
59
72
  * `:focus-visible` do not need it; wrapper rows around a live control do.
@@ -249,6 +262,7 @@ export function createComposerOverflowMenu(
249
262
  const open = (focusIndex = 0, viaKeyboard = false): void => {
250
263
  if (destroyed || isOpen()) return;
251
264
  keyboardModality = viaKeyboard;
265
+ forwardMenuTokens(triggerButton, panel, MENU_TOKEN_VARS);
252
266
  ensurePopover().open();
253
267
  triggerButton.setAttribute("aria-expanded", "true");
254
268
  const targets = focusables();
@@ -41,6 +41,26 @@ export const COMPOSER_CONTROL_FALLBACK_PX = 40;
41
41
  /** Icon-size token default in CSS pixels, for the same JS-derived cases. */
42
42
  export const COMPOSER_CONTROL_ICON_FALLBACK_PX = 24;
43
43
 
44
+ /**
45
+ * Forward theme vars onto a portaled menu panel, which lives outside the
46
+ * themed mount. The panel is reused across opens, so an unset var must clear
47
+ * any previously stamped value for the stylesheet fallback to paint again.
48
+ */
49
+ export const forwardMenuTokens = (
50
+ from: HTMLElement,
51
+ to: HTMLElement,
52
+ names: readonly string[]
53
+ ): void => {
54
+ const view = from.ownerDocument?.defaultView;
55
+ if (!view) return;
56
+ const computed = view.getComputedStyle(from);
57
+ for (const name of names) {
58
+ const value = computed.getPropertyValue(name).trim();
59
+ if (value) to.style.setProperty(name, value);
60
+ else to.style.removeProperty(name);
61
+ }
62
+ };
63
+
44
64
  export interface ComposerTextareaParts {
45
65
  textarea: HTMLTextAreaElement;
46
66
  /**
package/src/index-core.ts CHANGED
@@ -91,6 +91,7 @@ export type {
91
91
  ComposerActionBase,
92
92
  ComposerActionPlacement,
93
93
  ComposerActionPresentation,
94
+ ComposerActionVisibility,
94
95
  ComposerButtonAction,
95
96
  ComposerCustomAction,
96
97
  ComposerActionContext,
@@ -103,6 +104,7 @@ export type {
103
104
  ComposerModelPickerPresentation,
104
105
  // Composer modes + mode groups (composer.modes, composer.modeGroups)
105
106
  ComposerMode,
107
+ ComposerModeChipVisibility,
106
108
  ComposerModeGroup,
107
109
  ComposerModeGroupPresentation,
108
110
  // Composer baseline configuration (submit keys, sizing, locks)
@@ -585,6 +585,38 @@ describe("overflow menu row shading", () => {
585
585
  );
586
586
  expect(widgetCss).toContain(".persona-composer-overflow-menu__slot:hover");
587
587
  });
588
+
589
+ it("reads every overflowMenu token as a full path over the stock fallback", () => {
590
+ const at = widgetCss.indexOf(".persona-composer-overflow-menu {");
591
+ expect(at, "overflow menu panel rule not found").toBeGreaterThan(-1);
592
+ const panel = widgetCss.slice(at, widgetCss.indexOf("}", at));
593
+
594
+ expect(panel).toContain(
595
+ "--persona-components-composer-overflowMenu-background,"
596
+ );
597
+ expect(panel).toContain("var(--persona-surface, #ffffff)");
598
+ expect(panel).toContain(
599
+ "--persona-components-composer-overflowMenu-borderColor,"
600
+ );
601
+ expect(panel).toContain("var(--persona-border, #e5e7eb)");
602
+ expect(panel).toContain(
603
+ "--persona-components-composer-overflowMenu-borderRadius,"
604
+ );
605
+ expect(panel).toContain(
606
+ "--persona-components-composer-overflowMenu-foreground,"
607
+ );
608
+ expect(panel).toContain(
609
+ "--persona-components-composer-overflowMenu-shadow,"
610
+ );
611
+ });
612
+
613
+ it("keeps the portaled panel rule unprefixed", () => {
614
+ // createPopover mounts the panel outside the widget root, so a
615
+ // `[data-persona-root]` prefix would never match it.
616
+ expect(widgetCss).not.toContain(
617
+ "[data-persona-root] .persona-composer-overflow-menu"
618
+ );
619
+ });
588
620
  });
589
621
 
590
622
  describe("composer control focus rings", () => {
@@ -658,7 +690,11 @@ describe("model picker chevron", () => {
658
690
 
659
691
  it("colors the chevron from the theme token, never a baked-in hex", () => {
660
692
  const rule = ruleFor(".persona-composer-model-picker-chevron");
661
- expect(rule).toContain("background-color: var(--persona-button-ghost-fg");
693
+ // Same chain as the label, so the glyph and the text never diverge.
694
+ expect(rule).toContain(
695
+ "--persona-components-composer-modelPicker-labelColor"
696
+ );
697
+ expect(rule).toContain("var(--persona-button-ghost-fg");
662
698
  // The mask only needs opaque alpha, so the SVG's own stroke is not a color.
663
699
  expect(rule).toContain("mask-image");
664
700
  expect(rule).toContain("-webkit-mask-image");
@@ -840,6 +876,52 @@ describe("model picker popover", () => {
840
876
  });
841
877
  });
842
878
 
879
+ describe("model picker closed control surface", () => {
880
+ const ruleFor = (selector: string): string => {
881
+ const at = widgetCss.indexOf(selector);
882
+ expect(at, `selector not found: ${selector}`).toBeGreaterThan(-1);
883
+ return widgetCss.slice(at, widgetCss.indexOf("}", at));
884
+ };
885
+
886
+ it("reads the surface tokens over the ghost button fallbacks", () => {
887
+ // One vocabulary for both presentations: the native select and the popover
888
+ // trigger share this rule, so a page that stays native still themes.
889
+ const rule = ruleFor("[data-persona-root] .persona-composer-model-picker {");
890
+ expect(rule).toContain(
891
+ "--persona-components-composer-modelPicker-background,"
892
+ );
893
+ expect(rule).toContain("var(--persona-button-ghost-bg, transparent)");
894
+ expect(rule).toContain(
895
+ "--persona-components-composer-modelPicker-borderRadius,"
896
+ );
897
+ expect(rule).toContain("var(--persona-button-ghost-radius");
898
+ expect(rule).toContain(
899
+ "--persona-components-composer-modelPicker-labelColor,"
900
+ );
901
+ expect(rule).toContain("var(--persona-button-ghost-fg");
902
+ });
903
+
904
+ it("draws borderColor as an inset ring so the control never resizes", () => {
905
+ // A real border would eat 1px of the content box under `border-box` sizing
906
+ // and shift the label on every theme that leaves the token unset.
907
+ const rule = ruleFor("[data-persona-root] .persona-composer-model-picker {");
908
+ expect(rule).toContain("border: none");
909
+ expect(rule).toContain(
910
+ "box-shadow: inset 0 0 0 1px\n var(--persona-components-composer-modelPicker-borderColor, transparent)"
911
+ );
912
+ });
913
+
914
+ it("keeps hover on its own token so a themed fill survives the pointer", () => {
915
+ const rule = ruleFor(
916
+ "[data-persona-root] .persona-composer-model-picker:hover:not(:disabled) {"
917
+ );
918
+ expect(rule).toContain(
919
+ "--persona-components-composer-modelPicker-hoverBackground,"
920
+ );
921
+ expect(rule).toContain("var(--persona-button-ghost-hover-bg");
922
+ });
923
+ });
924
+
843
925
  describe("composer motion", () => {
844
926
  /** Every `@media (prefers-reduced-motion: no-preference)` block, joined. */
845
927
  const reducedMotionSafeCss = (): string => {
@@ -1371,7 +1371,8 @@
1371
1371
  *
1372
1372
  * The panel is mounted by createPopover next to the widget root (or inside the
1373
1373
  * shadow root), so its rules are deliberately un-prefixed by
1374
- * `[data-persona-root]` and must stand on their own. */
1374
+ * `[data-persona-root]` and must stand on their own. It sits outside the themed
1375
+ * mount, so the menu forwards its configured tokens onto the panel on open. */
1375
1376
  .persona-composer-overflow-menu {
1376
1377
  display: flex;
1377
1378
  flex-direction: column;
@@ -1380,11 +1381,27 @@
1380
1381
  max-height: 320px;
1381
1382
  overflow-y: auto;
1382
1383
  padding: 4px;
1383
- border-radius: var(--persona-radius-lg, 0.5rem);
1384
- border: 1px solid var(--persona-border, #e5e7eb);
1385
- background: var(--persona-surface, #ffffff);
1386
- color: var(--persona-text, #111827);
1387
- box-shadow: var(--persona-shadow-md, 0 8px 24px rgba(0, 0, 0, 0.12));
1384
+ border-radius: var(
1385
+ --persona-components-composer-overflowMenu-borderRadius,
1386
+ var(--persona-radius-lg, 0.5rem)
1387
+ );
1388
+ border: 1px solid
1389
+ var(
1390
+ --persona-components-composer-overflowMenu-borderColor,
1391
+ var(--persona-border, #e5e7eb)
1392
+ );
1393
+ background: var(
1394
+ --persona-components-composer-overflowMenu-background,
1395
+ var(--persona-surface, #ffffff)
1396
+ );
1397
+ color: var(
1398
+ --persona-components-composer-overflowMenu-foreground,
1399
+ var(--persona-text, #111827)
1400
+ );
1401
+ box-shadow: var(
1402
+ --persona-components-composer-overflowMenu-shadow,
1403
+ var(--persona-shadow-md, 0 8px 24px rgba(0, 0, 0, 0.12))
1404
+ );
1388
1405
  font-family: var(--persona-font-family, inherit);
1389
1406
  }
1390
1407
 
@@ -1598,9 +1615,22 @@
1598
1615
  padding-inline-start: 0.625rem;
1599
1616
  padding-inline-end: 1.75rem;
1600
1617
  border: none;
1601
- border-radius: var(--persona-button-ghost-radius, var(--persona-radius-md, 0.375rem));
1602
- background-color: var(--persona-button-ghost-bg, transparent);
1603
- color: var(--persona-button-ghost-fg, var(--persona-text, #111827));
1618
+ border-radius: var(
1619
+ --persona-components-composer-modelPicker-borderRadius,
1620
+ var(--persona-button-ghost-radius, var(--persona-radius-md, 0.375rem))
1621
+ );
1622
+ background-color: var(
1623
+ --persona-components-composer-modelPicker-background,
1624
+ var(--persona-button-ghost-bg, transparent)
1625
+ );
1626
+ color: var(
1627
+ --persona-components-composer-modelPicker-labelColor,
1628
+ var(--persona-button-ghost-fg, var(--persona-text, #111827))
1629
+ );
1630
+ /* A ring, not a border: `border-box` sizing would pull the label in by 1px on
1631
+ every theme that never sets the token. */
1632
+ box-shadow: inset 0 0 0 1px
1633
+ var(--persona-components-composer-modelPicker-borderColor, transparent);
1604
1634
  font-family: inherit;
1605
1635
  font-size: 0.8125rem;
1606
1636
  cursor: pointer;
@@ -1616,16 +1646,19 @@
1616
1646
  text-align: start;
1617
1647
  }
1618
1648
 
1619
- /* The chevron is a MASK, not a data-URI stroke color: the fill comes from the
1620
- ghost foreground token, so it themes with every other composer glyph. Same
1621
- Lucide `chevron-down` geometry as the icon registry draws. */
1649
+ /* The chevron is a MASK, not a data-URI stroke color: the fill follows the
1650
+ closed control's own text color, so the glyph and the label never diverge.
1651
+ Same Lucide `chevron-down` geometry as the icon registry draws. */
1622
1652
  .persona-composer-model-picker-chevron {
1623
1653
  justify-self: end;
1624
1654
  margin-inline-end: 0.625rem;
1625
1655
  width: 0.875rem;
1626
1656
  height: 0.875rem;
1627
1657
  pointer-events: none;
1628
- background-color: var(--persona-button-ghost-fg, var(--persona-text, #111827));
1658
+ background-color: var(
1659
+ --persona-components-composer-modelPicker-labelColor,
1660
+ var(--persona-button-ghost-fg, var(--persona-text, #111827))
1661
+ );
1629
1662
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
1630
1663
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
1631
1664
  -webkit-mask-repeat: no-repeat;
@@ -1653,7 +1686,10 @@
1653
1686
  }
1654
1687
 
1655
1688
  [data-persona-root] .persona-composer-model-picker:hover:not(:disabled) {
1656
- background-color: var(--persona-button-ghost-hover-bg, rgba(0, 0, 0, 0.05));
1689
+ background-color: var(
1690
+ --persona-components-composer-modelPicker-hoverBackground,
1691
+ var(--persona-button-ghost-hover-bg, rgba(0, 0, 0, 0.05))
1692
+ );
1657
1693
  }
1658
1694
 
1659
1695
  [data-persona-root] .persona-composer-model-picker:disabled {
@@ -173,7 +173,7 @@ export const THEME_TOKEN_DOCS = {
173
173
  'Shared scrollbar appearance for every scroller: thumb (default semantic.colors.border), track (default transparent).',
174
174
  reasoningBubble: 'shadow: reasoning/thinking row box-shadow.',
175
175
  composer:
176
- 'Message input form: shadow (box-shadow), borderColor, padding (form inset), gap (textarea row to actions row), fontSize and lineHeight (textarea type), controlSize (edge of every icon control in the action row: attachment, mention, mic, action buttons, overflow trigger, icon-mode send; default 40px) and controlIconSize (glyph inside them, default 24px). Per-control config keys (sendButton.size, voiceRecognition.iconSize) override controlSize; coarse-pointer devices floor the hit area at 40px. overlayBand paints the band behind an overlaid footer (composer.placement: "overlay"). segmented styles a segmented mode-group track (trackBackground, trackBorderRadius, padding, activeBackground, activeForeground, activeShadow, inactiveForeground, itemPadding); every key is optional and the stylesheet owns the defaults. modelPicker styles the popover model picker (menuBackground, menuBorderRadius, rowHoverBackground, labelColor, descriptionColor, suffixColor), also all optional.',
176
+ 'Message input form: shadow (box-shadow), borderColor, padding (form inset), gap (textarea row to actions row), fontSize and lineHeight (textarea type), controlSize (edge of every icon control in the action row: attachment, mention, mic, action buttons, overflow trigger, icon-mode send; default 40px) and controlIconSize (glyph inside them, default 24px). Per-control config keys (sendButton.size, voiceRecognition.iconSize) override controlSize; coarse-pointer devices floor the hit area at 40px. overlayBand paints the band behind an overlaid footer (composer.placement: "overlay"). segmented styles a segmented mode-group track (trackBackground, trackBorderRadius, padding, activeBackground, activeForeground, activeShadow, inactiveForeground, itemPadding); every key is optional and the stylesheet owns the defaults. modelPicker styles the model picker (background, hoverBackground, borderColor, borderRadius and labelColor on the closed control, which the native select and the popover trigger share; menuBackground, menuBorderRadius, rowHoverBackground, descriptionColor and suffixColor on the popover only), also all optional. overflowMenu styles the composer overflow "+" menu panel (background, borderColor, borderRadius, foreground, shadow), also all optional.',
177
177
  artifact:
178
178
  'toolbar (icon styling, copy menu), tab (background, active states), pane (background, toolbarBackground).',
179
179
  },
@@ -663,21 +663,41 @@ export interface ComposerSegmentedTokens {
663
663
  }
664
664
 
665
665
  /**
666
- * Model picker popover (`composer.modelPicker.presentation: "popover"`).
666
+ * Model picker chrome (`composer.models`).
667
+ *
668
+ * The surface keys (`background`, `hoverBackground`, `borderColor`,
669
+ * `borderRadius`, `labelColor`) style the CLOSED control, so the native
670
+ * `<select>` and the `presentation: "popover"` trigger share one vocabulary.
671
+ * The `menu*`, `rowHoverBackground`, and `descriptionColor` keys style the
672
+ * popover panel and are inert under the native select, as is `suffixColor`,
673
+ * which a `<select>` cannot carry either.
667
674
  *
668
675
  * Every key is optional and unset ones emit nothing: the stylesheet's `var()`
669
676
  * fallbacks own the default look, which derives from the surface and ghost
670
- * button tokens. Inert under the native `<select>` presentation, except
671
- * `suffixColor`, which the native control cannot carry either.
677
+ * button tokens.
672
678
  */
673
679
  export interface ComposerModelPickerTokens {
680
+ /** Closed control fill. @default the ghost button fill (transparent) */
681
+ background?: TokenReference<'color'>;
682
+ /** Closed control fill under hover. @default the ghost hover wash */
683
+ hoverBackground?: TokenReference<'color'>;
684
+ /**
685
+ * Closed control outline, drawn as a 1px inset ring so setting it never
686
+ * resizes the control. @default none
687
+ */
688
+ borderColor?: TokenReference<'color'>;
689
+ /** Closed control corner radius. @default the ghost button radius */
690
+ borderRadius?: TokenReference<'radius'>;
674
691
  /** Panel fill behind the rows. @default semantic.colors.surface */
675
692
  menuBackground?: TokenReference<'color'>;
676
693
  /** Panel corner radius. @default palette.radius.lg */
677
694
  menuBorderRadius?: TokenReference<'radius'>;
678
695
  /** Row fill under hover and the focused option. @default the ghost hover wash */
679
696
  rowHoverBackground?: TokenReference<'color'>;
680
- /** Row label and closed-control label color. @default inherited text */
697
+ /**
698
+ * Row label color, plus the closed control's text and chevron in both
699
+ * presentations. @default the ghost button foreground
700
+ */
681
701
  labelColor?: TokenReference<'color'>;
682
702
  /** Second line under a row label. @default semantic.colors.textMuted */
683
703
  descriptionColor?: TokenReference<'color'>;
@@ -685,6 +705,26 @@ export interface ComposerModelPickerTokens {
685
705
  suffixColor?: TokenReference<'color'>;
686
706
  }
687
707
 
708
+ /**
709
+ * Composer overflow `+` menu panel (`composer.actionOverflow`).
710
+ *
711
+ * Every key is optional and unset ones emit nothing: the stylesheet's `var()`
712
+ * fallbacks own the default look. The panel is portaled outside the themed
713
+ * mount, so the menu forwards the configured values onto it when it opens.
714
+ */
715
+ export interface ComposerOverflowMenuTokens {
716
+ /** Panel fill behind the rows. @default semantic.colors.surface */
717
+ background?: TokenReference<'color'>;
718
+ /** Panel border color. @default semantic.colors.border */
719
+ borderColor?: TokenReference<'color'>;
720
+ /** Panel corner radius. @default palette.radius.lg */
721
+ borderRadius?: TokenReference<'radius'>;
722
+ /** Row text and glyph color. @default semantic.colors.text */
723
+ foreground?: TokenReference<'color'>;
724
+ /** Panel elevation (raw CSS box-shadow, `none` allowed). @default palette.shadows.md */
725
+ shadow?: string;
726
+ }
727
+
688
728
  /** Composer (message input) chrome. */
689
729
  export interface ComposerChromeTokens {
690
730
  /** Box-shadow on the composer form (raw CSS, e.g. `none`). */
@@ -719,8 +759,10 @@ export interface ComposerChromeTokens {
719
759
  overlayBand?: string;
720
760
  /** Segmented mode-group track. */
721
761
  segmented?: ComposerSegmentedTokens;
722
- /** Model picker popover chrome. */
762
+ /** Model picker chrome (closed control plus popover panel). */
723
763
  modelPicker?: ComposerModelPickerTokens;
764
+ /** Overflow `+` menu panel chrome. */
765
+ overflowMenu?: ComposerOverflowMenuTokens;
724
766
  }
725
767
 
726
768
  /**
package/src/types.ts CHANGED
@@ -4007,6 +4007,16 @@ export type ComposerActionPlacement = "start" | "end";
4007
4007
  */
4008
4008
  export type ComposerActionPresentation = "bar" | "overflow" | "auto";
4009
4009
 
4010
+ /**
4011
+ * When a composer action is mounted, as a function of the draft.
4012
+ *
4013
+ * - `"always"` (default): the control is always mounted.
4014
+ * - `"when-empty"`: hidden as soon as the draft holds text or an attachment,
4015
+ * and while a response streams.
4016
+ * - `"when-text"`: the inverse, on the same draft state the send control reads.
4017
+ */
4018
+ export type ComposerActionVisibility = "always" | "when-empty" | "when-text";
4019
+
4010
4020
  /**
4011
4021
  * Fields shared by every composer action.
4012
4022
  *
@@ -4022,6 +4032,13 @@ export type ComposerActionBase = {
4022
4032
  presentation?: ComposerActionPresentation;
4023
4033
  order?: number;
4024
4034
  visible?: boolean | ((state: Readonly<ComposerState>) => boolean);
4035
+ /**
4036
+ * Draft-driven visibility, on the same state `sendButton.visibility` reads.
4037
+ * Hidden removes the control from layout and tab order. Combined with
4038
+ * `visible`: both must pass.
4039
+ * @default "always"
4040
+ */
4041
+ visibility?: ComposerActionVisibility;
4025
4042
  disabled?: boolean | ((state: Readonly<ComposerState>) => boolean);
4026
4043
  /** Disable while a response streams, through the composer's own mechanism. */
4027
4044
  disableWhenStreaming?: boolean;
@@ -4180,6 +4197,16 @@ export type ComposerMode = {
4180
4197
  */
4181
4198
  export type ComposerModeGroupPresentation = "buttons" | "segmented";
4182
4199
 
4200
+ /**
4201
+ * Whether a group's active modes also render as removable chips.
4202
+ *
4203
+ * - `"auto"` (default): one chip per active mode, except under `"segmented"`
4204
+ * presentation, where the track already shows the state.
4205
+ * - `"hidden"`: no chips for this group under any presentation. The pressed bar
4206
+ * button carries the state, and deselection stays on the button.
4207
+ */
4208
+ export type ComposerModeChipVisibility = "auto" | "hidden";
4209
+
4183
4210
  /**
4184
4211
  * Selection policy for a set of modes. `"single"` deselects siblings when one
4185
4212
  * is chosen; `"multiple"` toggles independently.
@@ -4189,6 +4216,12 @@ export type ComposerModeGroup = {
4189
4216
  selection: "single" | "multiple";
4190
4217
  /** @default "buttons" */
4191
4218
  presentation?: ComposerModeGroupPresentation;
4219
+ /**
4220
+ * Chip row policy for this group's active modes. The chip row itself is
4221
+ * derived from its children, so a group with no other chips leaves no gap.
4222
+ * @default "auto"
4223
+ */
4224
+ chipVisibility?: ComposerModeChipVisibility;
4192
4225
  /**
4193
4226
  * Accessible name for a segmented track. With it the track is a labelled
4194
4227
  * `role="group"`; without it the segments stand on their own names.