@artooi/ag-ui-web-component 0.47.0 → 0.48.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 (58) hide show
  1. package/CHANGELOG.md +135 -1
  2. package/README.md +171 -15
  3. package/dist/ag-ui-web-component.bundle.js +591 -176
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +27 -2
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +36 -3
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/events/bar_height_detail.d.ts +6 -0
  10. package/dist/core/events/bar_height_detail.d.ts.map +1 -0
  11. package/dist/dom/highlight_overlay.d.ts +2 -1
  12. package/dist/dom/highlight_overlay.d.ts.map +1 -1
  13. package/dist/dom/ring_color.d.ts +8 -0
  14. package/dist/dom/ring_color.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +926 -52
  18. package/dist/index.js.map +4 -4
  19. package/dist/tools/chat_surface_tools.d.ts +8 -1
  20. package/dist/tools/chat_surface_tools.d.ts.map +1 -1
  21. package/dist/ui/history/conversation_history.d.ts +16 -3
  22. package/dist/ui/history/conversation_history.d.ts.map +1 -1
  23. package/dist/ui/placement/collapse_styles.d.ts.map +1 -1
  24. package/dist/ui/placement/dock_styles.d.ts +2 -0
  25. package/dist/ui/placement/dock_styles.d.ts.map +1 -0
  26. package/dist/ui/placement/panel_placement.d.ts +11 -0
  27. package/dist/ui/placement/panel_placement.d.ts.map +1 -1
  28. package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -1
  29. package/dist/ui/placement/surface_for.d.ts +21 -17
  30. package/dist/ui/placement/surface_for.d.ts.map +1 -1
  31. package/dist/ui/progress/tool_group_styles.d.ts.map +1 -1
  32. package/dist/ui/progress/tool_step_styles.d.ts.map +1 -1
  33. package/dist/ui/shell/build_launcher.d.ts +15 -4
  34. package/dist/ui/shell/build_launcher.d.ts.map +1 -1
  35. package/dist/ui/shell/token_styles.d.ts.map +1 -1
  36. package/dist/ui/styles.d.ts.map +1 -1
  37. package/dist/ui/themes/scheme_styles.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/constants.ts +30 -2
  40. package/src/core/ag_ui_chat.ts +326 -26
  41. package/src/core/events/bar_height_detail.ts +5 -0
  42. package/src/dom/highlight_overlay.ts +2 -1
  43. package/src/dom/ring_color.ts +141 -4
  44. package/src/index.ts +4 -0
  45. package/src/tools/chat_surface_tools.ts +18 -8
  46. package/src/ui/history/conversation_history.ts +51 -7
  47. package/src/ui/placement/collapse_styles.ts +248 -2
  48. package/src/ui/placement/dock_styles.ts +91 -0
  49. package/src/ui/placement/panel_placement.ts +71 -3
  50. package/src/ui/placement/small_viewport_styles.ts +9 -0
  51. package/src/ui/placement/surface_for.ts +29 -18
  52. package/src/ui/progress/tool_group_styles.ts +49 -0
  53. package/src/ui/progress/tool_step_styles.ts +5 -3
  54. package/src/ui/shell/build_launcher.ts +32 -5
  55. package/src/ui/shell/token_styles.ts +28 -1
  56. package/src/ui/styles.ts +4 -0
  57. package/src/ui/themes/scheme_styles.ts +10 -1
  58. package/src/version.ts +1 -1
@@ -0,0 +1,91 @@
1
+ // Where a docked panel stands: above the bottom bar under collapse-to="bar",
2
+ // above the host's anchor under collapse-to="none". Token restatements only;
3
+ // the bar's own shape is in the collapse module, after the launcher it
4
+ // restyles.
5
+ export const DOCK_STYLES: string = `
6
+ /* ── Dock ────────────────────────────────────────────────────────────────────
7
+ A docked panel rises from the bar along the bottom edge, or from the host's
8
+ own control, instead of resting in a corner of its own. The element stamps
9
+ what it collapses to as data-collapsed-to, and writes where the dock is as
10
+ two private properties: --_dock-bottom, the bar's height or the distance
11
+ from the layout viewport's bottom to the anchor's top, and --_dock-right,
12
+ the anchor's distance from the right edge. Both are read with a 0px
13
+ fallback, so a panel measured before the bar has laid out stands where a
14
+ bar of no height would put it.
15
+
16
+ The cap follows the token block's rule: what the host left free less the
17
+ one gutter the anchored edge spends, so a panel grown to the cap runs from
18
+ just above the bar to the far edge. A host therefore needs no inset to keep
19
+ the panel off its bar; --ag-ui-inset still wins when set.
20
+
21
+ These blocks restate tokens and nothing else, and sit ahead of the phone
22
+ block at no more weight than it has, so on a phone the panel is the screen
23
+ as every other placement is there. The bar measures 0 while it is not drawn
24
+ (a phone with the panel open), which is what lets the side and full shapes
25
+ below fall back to the whole height without a phone rule of their own.
26
+
27
+ Each placement keeps its own side and shape; only the bottom edge moves. */
28
+ :host([data-collapsed-to="bar"]) {
29
+ --_inset: var(
30
+ --ag-ui-inset,
31
+ auto calc(var(--_edge-gutter) + var(--_viewport-inset-right))
32
+ calc(
33
+ var(--_edge-gutter) + var(--_viewport-inset-bottom) +
34
+ max(var(--_dock-bottom, 0px), var(--_keyboard-inset))
35
+ )
36
+ auto
37
+ );
38
+ --_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - var(--_edge-gutter)));
39
+ }
40
+
41
+ :host([data-collapsed-to="bar"][placement="bottom-left"]) {
42
+ --_inset: var(
43
+ --ag-ui-inset,
44
+ auto auto
45
+ calc(
46
+ var(--_edge-gutter) + var(--_viewport-inset-bottom) +
47
+ max(var(--_dock-bottom, 0px), var(--_keyboard-inset))
48
+ )
49
+ calc(var(--_edge-gutter) + var(--_viewport-inset-left))
50
+ );
51
+ }
52
+
53
+ /* The column and the full-screen panel stop at the bar rather than covering
54
+ it, so the bar stays where the user minimises from. */
55
+ :host([data-collapsed-to="bar"][placement="side"]) {
56
+ --_inset: var(
57
+ --ag-ui-inset,
58
+ calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
59
+ calc(var(--_viewport-inset-bottom) + var(--_dock-bottom, 0px)) auto
60
+ );
61
+ --_height: var(--ag-ui-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
62
+ --_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
63
+ }
64
+
65
+ :host([data-collapsed-to="bar"][placement="full"]) {
66
+ --_inset: var(
67
+ --ag-ui-inset,
68
+ calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
69
+ calc(var(--_viewport-inset-bottom) + var(--_dock-bottom, 0px)) var(--_viewport-inset-left)
70
+ );
71
+ --_height: var(--ag-ui-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
72
+ --_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
73
+ }
74
+
75
+ /* The host's own bar: the panel stands the anchor gap (8px unless the host
76
+ says otherwise) above the anchor and lines up with its right edge. Only
77
+ once the element found the anchor; until then, and when it never does, the
78
+ placement's own corner stands.
79
+
80
+ The cap spends the larger of the gap and the gutter. The gutter alone is
81
+ what this always spent, so the default is unchanged; but a host that opens
82
+ the gap past the gutter to clear a tall toolbar would otherwise push a
83
+ panel grown to the cap off the top of the window by the difference. */
84
+ :host([data-collapsed-to="none"][data-dock-anchored]) {
85
+ --_inset: var(--ag-ui-inset, auto var(--_dock-right, 0px) calc(var(--_dock-bottom, 0px) + var(--_anchor-gap)) auto);
86
+ --_max-height: var(
87
+ --ag-ui-max-height,
88
+ calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - max(var(--_edge-gutter), var(--_anchor-gap)))
89
+ );
90
+ }
91
+ `;
@@ -22,7 +22,7 @@ import {
22
22
  type ResizeGrip,
23
23
  type ResizeSize,
24
24
  } from "./resize_handle.js";
25
- import { surfaceFor } from "./surface_for.js";
25
+ import { type Surface, surfaceFor } from "./surface_for.js";
26
26
 
27
27
  /** Per-tab persistence key for a dragged panel size. */
28
28
  const SIZE_KEY = "ag-ui-chat:size";
@@ -37,6 +37,15 @@ const SIZE_KEY = "ag-ui-chat:size";
37
37
  */
38
38
  const DRAG_WIDTH = "--_drag-width";
39
39
  const DRAG_HEIGHT = "--_drag-height";
40
+ /**
41
+ * How far above the bottom of the layout viewport a docked panel stands, and
42
+ * how far in from its right edge: the bar's height, or the host anchor's top
43
+ * and right. Private and written inline, like the dragged size, and read only
44
+ * by the dock rules; never an inline --_inset, which would outrank the phone
45
+ * block and keep a docked panel off full-bleed on a phone.
46
+ */
47
+ const DOCK_BOTTOM = "--_dock-bottom";
48
+ const DOCK_RIGHT = "--_dock-right";
40
49
 
41
50
  /** Per-tab persistence key for a dragged launcher position. */
42
51
  const LAUNCHER_KEY = "ag-ui-chat:launcher";
@@ -309,6 +318,7 @@ export class PanelPlacement {
309
318
  const fullBleed = coversViewport(box, viewport);
310
319
  return {
311
320
  placement: this.#host.element.getAttribute("placement"),
321
+ collapseTo: this.#surface().collapseTo,
312
322
  collapsed: this.#host.collapsed(),
313
323
  collapsible: this.#host.collapsible(),
314
324
  movable: this.#launcherDraggable() && !fullBleed,
@@ -425,7 +435,7 @@ export class PanelPlacement {
425
435
  * per interaction, because `placement` is a live attribute.
426
436
  */
427
437
  #resizeAxis(): ResizeAxis {
428
- return surfaceFor(this.#host.element.getAttribute("placement")).resizeAxis;
438
+ return this.#surface().resizeAxis;
429
439
  }
430
440
 
431
441
  /**
@@ -567,6 +577,49 @@ export class PanelPlacement {
567
577
  }
568
578
  }
569
579
 
580
+ /** The surface the live placement and collapse-to attributes describe. */
581
+ #surface(): Surface {
582
+ const element = this.#host.element;
583
+ return surfaceFor(element.getAttribute("placement"), element.getAttribute("collapse-to"));
584
+ }
585
+
586
+ /** Whether the panel rises from the bottom bar or the host's anchor. */
587
+ #docked(): boolean {
588
+ const target = this.#surface().collapseTo;
589
+ return target === "bar" || target === "none";
590
+ }
591
+
592
+ /**
593
+ * Write where a docked panel stands, and return the bar's height.
594
+ *
595
+ * Under the bar it is the strip's border box, which is 0 whenever the strip
596
+ * is not drawn (a phone with the panel open, where the panel is the screen).
597
+ * Under a host anchor it is the anchor's top and right, measured against the
598
+ * layout viewport a fixed element is placed in; an anchor of null leaves the
599
+ * panel in its placement's own corner. Every other surface carries neither
600
+ * property, so nothing stale is left behind by a live switch.
601
+ */
602
+ syncDock(bar: HTMLElement, anchor: Element | null): number {
603
+ const element = this.#host.element;
604
+ const target = this.#surface().collapseTo;
605
+ const height = target === "bar" ? bar.offsetHeight : 0;
606
+ if (target === "bar") {
607
+ element.style.setProperty(DOCK_BOTTOM, `${height}px`);
608
+ } else if (target === "none" && anchor !== null) {
609
+ const rect = anchor.getBoundingClientRect();
610
+ const screen = this.#screen();
611
+ element.style.setProperty(DOCK_BOTTOM, `${Math.max(0, screen.height - rect.top)}px`);
612
+ element.style.setProperty(DOCK_RIGHT, `${Math.max(0, screen.width - rect.right)}px`);
613
+ } else {
614
+ element.style.removeProperty(DOCK_BOTTOM);
615
+ }
616
+ if (target !== "none" || anchor === null) {
617
+ element.style.removeProperty(DOCK_RIGHT);
618
+ }
619
+ element.toggleAttribute("data-dock-anchored", target === "none" && anchor !== null);
620
+ return height;
621
+ }
622
+
570
623
  /**
571
624
  * Whether this placement lets the launcher be dragged, read per interaction
572
625
  * because `placement` is a live attribute. `data-launcher-drag="false"` opts
@@ -575,7 +628,7 @@ export class PanelPlacement {
575
628
  #launcherDraggable(): boolean {
576
629
  return (
577
630
  this.#host.element.getAttribute("data-launcher-drag") !== "false" &&
578
- surfaceFor(this.#host.element.getAttribute("placement")).launcherDraggable
631
+ this.#surface().launcherDraggable
579
632
  );
580
633
  }
581
634
 
@@ -1057,6 +1110,21 @@ export class PanelPlacement {
1057
1110
  */
1058
1111
  #applyResize(grip: ResizeGrip, box: PanelRect): PanelRect {
1059
1112
  box = this.#withinViewport(box);
1113
+ if (this.#docked()) {
1114
+ // A docked panel keeps its resize but never its position: the edges it
1115
+ // stands on are the bar's or the anchor's, and a grip on one of them
1116
+ // would write the position below and take the panel off its dock. So
1117
+ // the pinned edges stay where they are and only the free ones follow.
1118
+ const now = this.#host.element.getBoundingClientRect();
1119
+ box = {
1120
+ left: grip.x === this.#anchor.x ? now.left : box.left,
1121
+ right: grip.x === this.#anchor.x ? now.right : box.right,
1122
+ top: grip.y === this.#anchor.y ? now.top : box.top,
1123
+ bottom: grip.y === this.#anchor.y ? now.bottom : box.bottom,
1124
+ };
1125
+ this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
1126
+ return box;
1127
+ }
1060
1128
  this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
1061
1129
  if (grip.x !== this.#anchor.x && grip.y !== this.#anchor.y) {
1062
1130
  return box;
@@ -57,5 +57,14 @@ export const SMALL_VIEWPORT_STYLES: string = `
57
57
  :host([placement=""]:not([data-small-viewport="off"])) .resize-handle {
58
58
  display: none;
59
59
  }
60
+
61
+ /* The bottom bar shows only while collapsed. With the panel open the panel
62
+ is the screen, its header's minimise control is the way back, and a
63
+ second strip fixed to the bottom edge would sit under the keyboard while
64
+ the composer has focus. Hidden by display, so the bar measures 0 and the
65
+ host's padding goes with it. */
66
+ :host([data-collapsed-to="bar"]:not([collapsed]):not([data-small-viewport="off"])) .dock-bar {
67
+ display: none;
68
+ }
60
69
  }
61
70
  `;
@@ -1,20 +1,19 @@
1
1
  import type { ResizeAxis } from "./resize_handle.js";
2
2
 
3
3
  /**
4
- * What a collapsed panel shrinks to.
5
- *
6
- * Only `null` versus not-`null` is read today: every caller asks whether the
7
- * surface can collapse at all, never which target it collapses to. The three
8
- * names below therefore describe the surface rather than drive it, and nothing
9
- * relies on them (changing one fails only the test of this record). The
10
- * bottom-bar collapse is expected to read or replace them, and a reader that
11
- * does should come with a test outside this record's own.
4
+ * What a collapsed panel shrinks to, stamped on the element as
5
+ * data-collapsed-to so the stylesheet draws it.
12
6
  *
13
7
  * - `launcher` -- the round button in a corner.
14
8
  * - `rail` -- the slim full-height edge the sidebar docks against; the same
15
9
  * launcher element, shaped by the stylesheet.
16
10
  * - `header` -- the in-flow collapse: the body hides and the header bar stays
17
11
  * in the host's layout, so there is no launcher at all.
12
+ * - `bar` -- `collapse-to="bar"` over a placement that would otherwise
13
+ * collapse to `launcher`: a strip along the bottom edge, the panel rising
14
+ * from its right end.
15
+ * - `none` -- `collapse-to="none"` over any collapsible placement: collapsed
16
+ * is hidden, and the host's own control brings the panel back.
18
17
  * - `null` -- no collapsed state. `page` is a dedicated route rather than a
19
18
  * panel on someone else's page, so there is no "away" for it to go to:
20
19
  * collapsing it left a strip of application chrome fixed over a route with
@@ -25,7 +24,7 @@ import type { ResizeAxis } from "./resize_handle.js";
25
24
  * those paths asks this record; the stylesheet covers the one path that
26
25
  * never does, an attribute written straight onto the element.
27
26
  */
28
- export type CollapseTarget = "launcher" | "rail" | "header" | null;
27
+ export type CollapseTarget = "launcher" | "rail" | "header" | "bar" | "none" | null;
29
28
 
30
29
  /**
31
30
  * Everything the script needs to know about one display surface, in one
@@ -36,11 +35,7 @@ export type CollapseTarget = "launcher" | "rail" | "header" | null;
36
35
  * under this directory); this is the part of a surface that code decides.
37
36
  */
38
37
  export interface Surface {
39
- /**
40
- * What collapsing shrinks the panel to; `null` when it cannot collapse. Only
41
- * the `null` test is authoritative: the non-null names are descriptive (see
42
- * {@link CollapseTarget}), and nothing branches on which one a surface has.
43
- */
38
+ /** What collapsing shrinks the panel to; `null` when it cannot collapse. */
44
39
  readonly collapseTo: CollapseTarget;
45
40
  /**
46
41
  * Whether a gesture may move the launcher. The rest have nowhere to put it:
@@ -127,9 +122,25 @@ const UNKNOWN: Surface = {
127
122
  };
128
123
 
129
124
  /**
130
- * The surface `placement` describes. Read per interaction by every caller,
131
- * because `placement` is a live attribute.
125
+ * The surface `placement` and `collapse-to` describe together. Read per
126
+ * interaction by every caller, because both are live attributes.
127
+ *
128
+ * `collapse-to` only ever replaces the round launcher or the whole collapsed
129
+ * state, never a placement's own shape: `bar` applies where the placement
130
+ * collapses to `launcher`, so the sidebar keeps its rail and an embedded chat
131
+ * its header bar, and `none` applies to every collapsible placement. Absent,
132
+ * `auto` and any other value leave the placement as it was. A docked surface
133
+ * cannot be moved -- the panel rises from the bar or the host's anchor, and a
134
+ * position the user dragged would take it away from either -- so both turn
135
+ * the launcher drag off, and with it the header drag and `moveTo`.
132
136
  */
133
- export function surfaceFor(placement: string | null): Surface {
134
- return SURFACES.get(placement) ?? UNKNOWN;
137
+ export function surfaceFor(placement: string | null, collapseTo: string | null = null): Surface {
138
+ const surface = SURFACES.get(placement) ?? UNKNOWN;
139
+ if (collapseTo === "bar" && surface.collapseTo === "launcher") {
140
+ return { ...surface, collapseTo: "bar", launcherDraggable: false };
141
+ }
142
+ if (collapseTo === "none" && surface.collapseTo !== null) {
143
+ return { ...surface, collapseTo: "none", launcherDraggable: false };
144
+ }
145
+ return surface;
135
146
  }
@@ -21,6 +21,18 @@ const GROUPED = `:host(:where(:not(${CARD_MODES})))`;
21
21
  /** The host in one of the four card modes, where a group drawn earlier is flattened. */
22
22
  const CARDS = `:host(:is(${CARD_MODES}))`;
23
23
 
24
+ /**
25
+ * A card inside an opened group, with the host in grouped mode and the given
26
+ * data-tool-group-display. The inner attribute sits inside the same :host()
27
+ * as the grouped test, since the host is featureless outside it, and being
28
+ * grouped-only is what lets an outer card mode flatten the group into its own
29
+ * cards: none of these rules match there. Full is no rule at all, so an absent
30
+ * or unknown inner value draws the full card a group always drew.
31
+ */
32
+ function inner(mode: string): string {
33
+ return `:host(:where(:not(${CARD_MODES}))[data-tool-group-display="${mode}"]) .tool-group-list .tool-call`;
34
+ }
35
+
24
36
  export const TOOL_GROUP_STYLES: string = `
25
37
  /* ── The folded tool-step group ──────────────────────────────────
26
38
  One quiet row per run of consecutive calls, the cards one click down as
@@ -205,6 +217,43 @@ ${GROUPED} .tool-group .tool-call-subagent {
205
217
  flex-basis: 100%;
206
218
  }
207
219
 
220
+ /* The inner display mode: how an opened group draws its calls. The same
221
+ visibility rules the card modes apply to a flat card, so a call inside the
222
+ group shows the regions the flat card of that mode shows. The flat inline
223
+ mode's chrome rule is left out on purpose: a card in the group has already
224
+ lost its box and is laid out as a row of the list, and that rule's padding
225
+ and gap would undo the row. Selected from the host attribute, so changing it
226
+ restyles groups already on screen. */
227
+ ${inner("compact")}[data-expanded="false"] .tool-call-section {
228
+ display: none;
229
+ }
230
+
231
+ ${inner("inline")} .tool-call-section--args {
232
+ display: none;
233
+ }
234
+
235
+ ${inner("minimal")} .tool-call-toggle,
236
+ ${inner("minimal")} .tool-call-body {
237
+ display: none;
238
+ }
239
+
240
+ /* A call waiting on an approval shows its arguments in every inner mode, as it
241
+ does in every card mode, and for the same reason: the arguments are what
242
+ tell three gated calls of one tool apart. These come after the hides above,
243
+ which they tie with on weight. The arguments region only, never every
244
+ section, since the result region carries the hidden attribute until a
245
+ result exists and a display value here would override it. The arguments
246
+ region carries it too when the call has none, so it is excluded while
247
+ hidden rather than framed empty. */
248
+ ${inner("minimal")}[data-status="deferred"] .tool-call-body {
249
+ display: flex;
250
+ }
251
+
252
+ ${inner("compact")}[data-status="deferred"] .tool-call-section--args:not([hidden]),
253
+ ${inner("inline")}[data-status="deferred"] .tool-call-section--args:not([hidden]) {
254
+ display: flex;
255
+ }
256
+
208
257
  /* Grouping is decided as each card is built, so a card mode set afterwards
209
258
  finds groups already drawn. It flattens them: the group and its list stop
210
259
  generating boxes, the row and the repeat ordinals go, and every card lays
@@ -277,9 +277,11 @@ export const TOOL_STEP_STYLES: string = `
277
277
 
278
278
  /* The arguments region only, never every section: the result region carries the
279
279
  hidden attribute until a result exists, and a display value here overrides it,
280
- framing an empty RESULT heading under the question. */
281
- :host([data-tool-display="compact"]) .tool-call[data-status="deferred"] .tool-call-section--args,
282
- :host([data-tool-display="inline"]) .tool-call[data-status="deferred"] .tool-call-section--args {
280
+ framing an empty RESULT heading under the question. The arguments region is
281
+ hidden too when the call has none, and is left alone then, or it would frame
282
+ an empty ARGUMENTS heading. */
283
+ :host([data-tool-display="compact"]) .tool-call[data-status="deferred"] .tool-call-section--args:not([hidden]),
284
+ :host([data-tool-display="inline"]) .tool-call[data-status="deferred"] .tool-call-section--args:not([hidden]) {
283
285
  display: flex;
284
286
  }
285
287
 
@@ -7,21 +7,32 @@ export interface LauncherOptions {
7
7
  readonly launcher: HTMLButtonElement;
8
8
  readonly badge: HTMLElement;
9
9
  readonly railLabel: HTMLElement;
10
+ /** The bottom bar's wrapper, which holds the launcher and the history button. */
11
+ readonly bar: HTMLElement;
12
+ /** The bottom bar's history button, a sibling of the launcher in the bar. */
13
+ readonly barHistory: HTMLButtonElement;
10
14
  readonly titleText: string;
11
15
  readonly iconUrl: string | null;
12
16
  readonly strings: UiStrings;
13
17
  readonly signal: AbortSignal;
14
18
  readonly onOpen: () => void;
19
+ readonly onHistory: () => void;
15
20
  }
16
21
 
17
22
  /**
18
- * Build what a collapsed widget shrinks to: a round floating button, or the
19
- * slim edge rail under `placement="sidebar"` -- one element, shaped by the
20
- * stylesheet. The element mounts it as a sibling of the panel, so it survives
21
- * the panel being hidden.
23
+ * Build what a collapsed widget shrinks to: a round floating button, the slim
24
+ * edge rail under `placement="sidebar"`, or the strip along the bottom edge
25
+ * under `collapse-to="bar"` -- one element, shaped by the stylesheet. The
26
+ * element mounts the bar wrapper as a sibling of the panel, so it survives the
27
+ * panel being hidden.
28
+ *
29
+ * The wrapper is there for the bar's history button: a button cannot nest in
30
+ * a button, so the strip is the launcher and the history button side by side.
31
+ * Outside the bar the wrapper draws no box and the history button is not
32
+ * drawn, so the launcher lays out against the host exactly as it did alone.
22
33
  */
23
34
  export function buildLauncher(options: LauncherOptions): void {
24
- const { launcher, badge, railLabel } = options;
35
+ const { launcher, badge, railLabel, bar, barHistory } = options;
25
36
  launcher.className = "launcher";
26
37
  launcher.type = "button";
27
38
  launcher.setAttribute("part", "launcher");
@@ -47,4 +58,20 @@ export function buildLauncher(options: LauncherOptions): void {
47
58
  badge,
48
59
  );
49
60
  launcher.addEventListener("click", options.onOpen, { signal: options.signal });
61
+ bar.className = "dock-bar";
62
+ bar.setAttribute("part", "bar");
63
+ barHistory.className = "bar-history";
64
+ barHistory.type = "button";
65
+ barHistory.setAttribute("part", "bar-history");
66
+ barHistory.title = options.strings.chatHistory;
67
+ barHistory.setAttribute("aria-label", options.strings.chatHistory);
68
+ // A glyph rather than the header's icon slot: a slot name may appear once
69
+ // per shadow root, and the header's history button already holds it.
70
+ const glyph = document.createElement("span");
71
+ glyph.className = "bar-history-glyph";
72
+ glyph.setAttribute("aria-hidden", "true");
73
+ glyph.textContent = "\u2630";
74
+ barHistory.replaceChildren(glyph);
75
+ barHistory.addEventListener("click", options.onHistory, { signal: options.signal });
76
+ bar.replaceChildren(launcher, barHistory);
50
77
  }
@@ -37,7 +37,15 @@ export const TOKEN_STYLES: string = `
37
37
  --_line-strong: var(--ag-ui-line-strong, color-mix(in oklch, var(--_fg) 20%, var(--_bg)));
38
38
  --_wash: var(--ag-ui-wash, color-mix(in oklch, var(--_fg) 4.5%, var(--_bg)));
39
39
  --_accent-ink: var(--ag-ui-accent-ink, color-mix(in oklch, var(--_accent) 82%, var(--_fg)));
40
- --_ring: var(--ag-ui-ring, color-mix(in oklch, var(--_accent) 55%, transparent));
40
+ /* The focus ring is drawn on the panel, so it has to hold 3:1 against
41
+ --_bg whatever accent a host brings (WCAG 2.4.13). Two parts accent to
42
+ three parts ink does that at the worst case, an accent at the far end
43
+ from the ink (white on a light panel, black on a dark one): the ink
44
+ alone sits far above 3:1, and three fifths of it keeps the mix there
45
+ while the hue still reads as the accent. Half and half fell to 2.3:1
46
+ for black on the code theme. The accent over transparent, as before,
47
+ measured only the accent, and a light host accent fell to 1.6:1. */
48
+ --_ring: var(--ag-ui-ring, color-mix(in oklch, var(--_accent) 40%, var(--_fg)));
41
49
  /* The user's turn is the accent at a whisper. With Quiet's graphite accent
42
50
  that is a neutral wash; once a host sets a brand accent it becomes a tint
43
51
  of the brand, which is the one thing Soft does that Quiet keeps. */
@@ -300,6 +308,25 @@ export const TOKEN_STYLES: string = `
300
308
  grid/flex layout) to embed it in the page flow instead of floating. */
301
309
  --_position: var(--ag-ui-position, fixed);
302
310
  --_z-index: var(--ag-ui-z-index, 2147483000);
311
+ /* A size the user dragged, written inline on the host by the placement
312
+ code, and read ahead of the host's tokens so the drag wins. There is no
313
+ public token behind it; it is declared initial here only so a chat that
314
+ was never dragged does not inherit one, from a dragged chat it is
315
+ slotted into or from any page rule naming it. The inline value still
316
+ beats this default. */
317
+ --_drag-width: initial;
318
+ --_drag-height: initial;
319
+ /* Where a docked panel stands, written inline the same way by the placement
320
+ code: the bottom bar's height, or the host anchor's distance from the
321
+ bottom and right edges. Read only by the dock rules, each with a 0px
322
+ fallback; declared initial so it never inherits either. */
323
+ --_dock-bottom: initial;
324
+ --_dock-right: initial;
325
+ /* The gap between a panel and the host's own button it rises from under
326
+ collapse-to="none". The button usually sits inside a taller toolbar, so a
327
+ host sets this to the button's distance from the toolbar's top plus the
328
+ clearance it wants, and the panel stands clear of the toolbar. */
329
+ --_anchor-gap: var(--ag-ui-anchor-gap, 8px);
303
330
  --_width: var(--_drag-width, var(--ag-ui-width, 380px));
304
331
  --_height: var(--_drag-height, var(--ag-ui-height, 560px));
305
332
  /* The gutter a resting floating panel keeps between itself and the edge of
package/src/ui/styles.ts CHANGED
@@ -23,6 +23,7 @@ import { APPROVAL_STYLES } from "./interrupts/approval_styles.js";
23
23
  import { CONFIRMATION_STYLES } from "./interrupts/confirmation_styles.js";
24
24
  import { QUESTION_STYLES } from "./interrupts/question_styles.js";
25
25
  import { COLLAPSE_STYLES } from "./placement/collapse_styles.js";
26
+ import { DOCK_STYLES } from "./placement/dock_styles.js";
26
27
  import { EMBEDDED_STYLES } from "./placement/embedded_styles.js";
27
28
  import { LAUNCHER_STYLES } from "./placement/launcher_styles.js";
28
29
  import { PAGE_STYLES } from "./placement/page_styles.js";
@@ -65,6 +66,9 @@ export const STYLES: string = [
65
66
  THEME_STYLES,
66
67
  PRESET_STYLES,
67
68
  PAGE_STYLES,
69
+ // Ahead of the phone block and no heavier than it: a docked panel is still
70
+ // full-bleed on a phone.
71
+ DOCK_STYLES,
68
72
  SMALL_VIEWPORT_STYLES,
69
73
  SIDEBAR_STYLES,
70
74
  LAUNCHER_STYLES,
@@ -6,9 +6,18 @@ export const SCHEME_STYLES: string = `
6
6
  dark) and beside a theme name (soft dark). auto follows the operating
7
7
  system through color-scheme itself, with no media query to keep in step.
8
8
  code is a dark-only theme and has no light half. */
9
+ /* Important, so a scheme word holds against a host rule that sets the
10
+ element's color-scheme (a page that follows its own dark mode). A rule
11
+ from outside the shadow tree beats every :host rule at normal weight, and
12
+ the theme toggle writes the word; without this its press changed nothing.
13
+ code stays last, so code light is still dark. */
14
+ :host([theme~="light"]) {
15
+ color-scheme: light !important;
16
+ }
17
+
9
18
  :host([theme~="dark"]),
10
19
  :host([theme~="code"]) {
11
- color-scheme: dark;
20
+ color-scheme: dark !important;
12
21
  }
13
22
 
14
23
  :host([theme~="auto"]) {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.47.0";
1
+ export const VERSION: string = "0.48.0";