@artooi/ag-ui-web-component 0.49.0 → 0.50.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 (38) hide show
  1. package/CHANGELOG.md +175 -1
  2. package/README.md +25 -14
  3. package/dist/ag-ui-web-component.bundle.js +168 -125
  4. package/dist/ag-ui-web-component.bundle.js.map +3 -3
  5. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  6. package/dist/index.js +530 -110
  7. package/dist/index.js.map +2 -2
  8. package/dist/tools/chat_surface_tools.d.ts +8 -5
  9. package/dist/tools/chat_surface_tools.d.ts.map +1 -1
  10. package/dist/ui/placement/collapse_styles.d.ts.map +1 -1
  11. package/dist/ui/placement/panel_placement.d.ts +15 -1
  12. package/dist/ui/placement/panel_placement.d.ts.map +1 -1
  13. package/dist/ui/placement/resize_styles.d.ts.map +1 -1
  14. package/dist/ui/shell/frame_styles.d.ts.map +1 -1
  15. package/dist/ui/shell/token_styles.d.ts.map +1 -1
  16. package/dist/ui/themes/classic_tokens.d.ts.map +1 -1
  17. package/dist/ui/themes/ledger_styles.d.ts.map +1 -1
  18. package/dist/ui/themes/ledger_tokens.d.ts.map +1 -1
  19. package/dist/ui/themes/quiet_styles.d.ts.map +1 -1
  20. package/dist/ui/themes/soft_styles.d.ts.map +1 -1
  21. package/dist/ui/themes/soft_tokens.d.ts.map +1 -1
  22. package/dist/ui/transcript/transcript_styles.d.ts.map +1 -1
  23. package/package.json +1 -1
  24. package/src/core/ag_ui_chat.ts +73 -7
  25. package/src/tools/chat_surface_tools.ts +8 -5
  26. package/src/ui/placement/collapse_styles.ts +8 -0
  27. package/src/ui/placement/panel_placement.ts +512 -99
  28. package/src/ui/placement/resize_styles.ts +9 -0
  29. package/src/ui/shell/frame_styles.ts +22 -2
  30. package/src/ui/shell/token_styles.ts +7 -6
  31. package/src/ui/themes/classic_tokens.ts +1 -0
  32. package/src/ui/themes/ledger_styles.ts +4 -3
  33. package/src/ui/themes/ledger_tokens.ts +1 -0
  34. package/src/ui/themes/quiet_styles.ts +2 -15
  35. package/src/ui/themes/soft_styles.ts +4 -3
  36. package/src/ui/themes/soft_tokens.ts +1 -0
  37. package/src/ui/transcript/transcript_styles.ts +14 -1
  38. package/src/version.ts +1 -1
package/dist/index.js CHANGED
@@ -5895,6 +5895,18 @@ function asPoint(value) {
5895
5895
  function coversViewport(box, viewport) {
5896
5896
  return box.width >= viewport.width - 1 && box.height >= viewport.height - 1;
5897
5897
  }
5898
+ function hangsOff(box, viewport, screen) {
5899
+ const slack = 0.5;
5900
+ return box.left < viewport.left - slack || box.top < viewport.top - slack || box.right > screen.width + slack || box.bottom > screen.height + slack;
5901
+ }
5902
+ function atStartOf(viewport, box) {
5903
+ return {
5904
+ left: viewport.left,
5905
+ top: viewport.top,
5906
+ right: viewport.left + (box.right - box.left),
5907
+ bottom: viewport.top + (box.bottom - box.top)
5908
+ };
5909
+ }
5898
5910
  function insetFrom(corner, box, screen) {
5899
5911
  const side = (value) => `${Math.round(value)}px`;
5900
5912
  return [
@@ -5924,6 +5936,7 @@ var PanelPlacement = class {
5924
5936
  * header on screen, and the launcher is the handle then.
5925
5937
  */
5926
5938
  enablePanelDrag(header) {
5939
+ this.#header = header;
5927
5940
  enablePanelDrag(header, {
5928
5941
  enabled: () => !this.#host.collapsed() && this.#canDragPanel(),
5929
5942
  rect: () => this.#host.element.getBoundingClientRect(),
@@ -5948,12 +5961,19 @@ var PanelPlacement = class {
5948
5961
  }
5949
5962
  /** Build the eight resize grips into `chat`, with one of them in the tab order. */
5950
5963
  mountResizeGrips(chat) {
5964
+ this.#resizing = false;
5951
5965
  for (const grip of RESIZE_GRIPS) {
5952
5966
  const handle = createResizeHandle(grip, {
5953
5967
  axis: () => this.#resizeAxis(),
5954
5968
  rect: () => this.#host.element.getBoundingClientRect(),
5955
- apply: (box) => this.#applyResize(grip, box),
5956
- commit: (box) => this.#commitResize(grip, box),
5969
+ apply: (box) => {
5970
+ this.#resizing = true;
5971
+ this.#applyResize(grip, box);
5972
+ },
5973
+ commit: (box) => {
5974
+ this.#resizing = false;
5975
+ this.#commitResize(grip, box);
5976
+ },
5957
5977
  label: this.#host.strings().resizePanel
5958
5978
  });
5959
5979
  handle.tabIndex = -1;
@@ -5978,6 +5998,7 @@ var PanelPlacement = class {
5978
5998
  * it always has.
5979
5999
  */
5980
6000
  resetSize() {
6001
+ const sized = this.#host.element.getBoundingClientRect();
5981
6002
  this.#host.element.style.removeProperty(DRAG_WIDTH);
5982
6003
  this.#host.element.style.removeProperty(DRAG_HEIGHT);
5983
6004
  this.#host.clearPreference(SIZE_KEY);
@@ -5987,9 +6008,47 @@ var PanelPlacement = class {
5987
6008
  }
5988
6009
  this.#dockPos = null;
5989
6010
  this.#storeDockPosition(null);
6011
+ this.#placeDocked();
6012
+ } else {
6013
+ this.#carrySpotWithSize(sized);
5990
6014
  }
5991
6015
  this.syncResizeAnchor();
5992
6016
  }
6017
+ /**
6018
+ * Carry a stated corner spot along with a dragged size coming off.
6019
+ *
6020
+ * The size comes off toward the corner the layout holds still, so the top-left
6021
+ * the spot is kept as moves with it, and a restore from the old one would swing
6022
+ * the panel away from its launcher. Only that movement goes onto the spot, not
6023
+ * where the box stands: in a window narrower than the spot the box is held
6024
+ * short of it, and that is not something the user stated.
6025
+ *
6026
+ * Left alone while a host's own size is in force, because that cannot be told
6027
+ * from the dragged size coming off, and it may be a state the host is about to
6028
+ * leave: counting it would throw the spot off by as much as that state is
6029
+ * bigger. A panel that is the screen needs no case of its own: the small-viewport
6030
+ * block ignores a dragged size, so its box does not move.
6031
+ *
6032
+ * Left alone too under data-launcher-drag="false", where a spot left over from
6033
+ * before the attribute is a record kept for the day it comes off, and nothing
6034
+ * that happens to the box now is a statement about it.
6035
+ */
6036
+ #carrySpotWithSize(sized) {
6037
+ const element = this.#host.element;
6038
+ const box = element.getBoundingClientRect();
6039
+ const spot = this.#cornerSpot ?? this.#readLauncherPosition();
6040
+ const style = getComputedStyle(element);
6041
+ if (spot?.panel === void 0 || !this.#launcherDraggable() || style.getPropertyValue("--ag-ui-width") !== "" || style.getPropertyValue("--ag-ui-height") !== "") {
6042
+ return;
6043
+ }
6044
+ this.#writeCornerSpot({
6045
+ ...spot,
6046
+ panel: {
6047
+ left: spot.panel.left + box.left - sized.left,
6048
+ top: spot.panel.top + box.top - sized.top
6049
+ }
6050
+ });
6051
+ }
5993
6052
  /**
5994
6053
  * A zero-sized box carrying the host's viewport insets as padding, so they
5995
6054
  * can be read back as used pixel lengths. See the `.viewport-probe` rule for
@@ -6014,6 +6073,33 @@ var PanelPlacement = class {
6014
6073
  * from the launcher would move the panel the user just placed.
6015
6074
  */
6016
6075
  #panelPos = null;
6076
+ /**
6077
+ * The spot #launcherPos and #panelPos were last stated as, or null while
6078
+ * nothing has been stated since this element connected.
6079
+ *
6080
+ * Those two say where the launcher and the panel stand now, held inside the
6081
+ * viewport, because that is where a press finds them and a drag starts from.
6082
+ * This is what the user asked for, kept apart so that a window narrower than
6083
+ * the spot, or a phone that took the panel full-bleed, gives it back unchanged
6084
+ * once there is room again. Only a gesture, a move or a dragged size coming off
6085
+ * changes it; re-applying it against a viewport never does, which is the difference from a clamped
6086
+ * position quietly becoming the record one resize at a time.
6087
+ */
6088
+ #cornerSpot = null;
6089
+ /**
6090
+ * The inset a grip wrote on a corner panel with no spot, or null. A grip on an
6091
+ * edge the layout holds moves the panel without stating a spot, so this is the
6092
+ * only way to tell its inline inset from one a host wrote there, which stays,
6093
+ * and to give it back once a phone has taken the panel full-bleed.
6094
+ */
6095
+ #gripInset = null;
6096
+ /**
6097
+ * The inset standing a panel at the start of a window a host's size fills, or
6098
+ * null: a corner panel with no spot, or a docked one with or without one. Told
6099
+ * apart from a host's own for the same reason, and it lasts only as long as that
6100
+ * size does.
6101
+ */
6102
+ #fillInset = null;
6017
6103
  /**
6018
6104
  * Where the user dragged a docked panel, in viewport coordinates, or null
6019
6105
  * while it stands on the bar or the anchor.
@@ -6031,6 +6117,13 @@ var PanelPlacement = class {
6031
6117
  * syncDock, which runs on every change either can make.
6032
6118
  */
6033
6119
  #dockTop = Number.POSITIVE_INFINITY;
6120
+ /** The bar's height, as last measured: what a fill takes away when it stamps. */
6121
+ #barHeight = 0;
6122
+ /**
6123
+ * What the usable box and the window measured when a fill took the bar away, and what
6124
+ * the bar was. Null while no fill holds it. See {@link #fillRoom}.
6125
+ */
6126
+ #barTaken = null;
6034
6127
  /**
6035
6128
  * The corner the panel opens away from, once this element is placing itself.
6036
6129
  * Null means the host's layout still decides, and the anchor is measured.
@@ -6042,6 +6135,10 @@ var PanelPlacement = class {
6042
6135
  * second while the panel is already being laid out on every one of them.
6043
6136
  */
6044
6137
  #anchor = { x: "right", y: "bottom" };
6138
+ /** The header the panel drag listens on, stamped by #stampHeader. */
6139
+ #header = null;
6140
+ /** Whether a grip has applied a box it has not committed yet. */
6141
+ #resizing = false;
6045
6142
  /** The eight grips, by name, so the keyboard-reachable one can be moved. */
6046
6143
  #resizeHandles = /* @__PURE__ */ new Map();
6047
6144
  /**
@@ -6076,27 +6173,30 @@ var PanelPlacement = class {
6076
6173
  *
6077
6174
  * Read from the stamp the drag helpers already set, rather than tracked
6078
6175
  * separately: one source of truth, and it clears on `pointercancel` as well
6079
- * as `pointerup`, which is the end a touch gesture usually gets.
6176
+ * as `pointerup`, which is the end a touch gesture usually gets. The one
6177
+ * gesture that stamps nothing is an arrow key on a grip, which applies a box
6178
+ * per key and states it only when the key comes up, hence #resizing.
6080
6179
  */
6081
6180
  dragging() {
6082
- return this.#host.launcher.hasAttribute("data-dragging") || this.#host.root.querySelector(".header[data-dragging]") !== null;
6181
+ return this.#resizing || this.#host.launcher.hasAttribute("data-dragging") || this.#host.root.querySelector(".header[data-dragging], .resize-handle[data-dragging]") !== null;
6083
6182
  }
6084
6183
  /** The report behind `AgUiChat.describeSurface`, whose doc is the contract. */
6085
6184
  describeSurface() {
6086
6185
  const box = this.#host.element.getBoundingClientRect();
6087
6186
  const viewport = this.#viewport();
6088
- const fullBleed = coversViewport(box, viewport);
6187
+ const fullBleed = coversViewport(box, this.#fillRoom());
6089
6188
  return {
6090
6189
  placement: this.#host.element.getAttribute("placement"),
6091
6190
  collapseTo: this.#surface().collapseTo,
6092
6191
  collapsed: this.#host.collapsed(),
6093
6192
  collapsible: this.#host.collapsible(),
6094
6193
  movable: this.#panelMovable() && !fullBleed,
6095
- // Whether the page left moving on, which is not the same question as
6096
- // whether this panel moves: a docked sidebar's position is owned by its
6097
- // placement, and reporting that as the host having turned moving off is
6098
- // the sentence the move refusal would then get wrong.
6099
- draggable: this.#host.element.getAttribute("data-launcher-drag") !== "false" && (this.#surface().launcherDraggable || this.#docked()),
6194
+ // The page's setting alone: true for every placement unless the host set
6195
+ // `data-launcher-drag="false"`. Whether this panel moves is `movable`,
6196
+ // and reading the launcher or the dock in here made the field change
6197
+ // with `collapse-to`, so a docked sidebar reported that the page had
6198
+ // turned moving off when the placement owned its position.
6199
+ draggable: this.#host.element.getAttribute("data-launcher-drag") !== "false",
6100
6200
  fullBleed,
6101
6201
  box: {
6102
6202
  left: Math.round(box.left),
@@ -6120,7 +6220,7 @@ var PanelPlacement = class {
6120
6220
  const restore = options.announce === true ? this.#captureGeometry() : null;
6121
6221
  const viewport = this.#viewport();
6122
6222
  const box = this.#host.element.getBoundingClientRect();
6123
- if (coversViewport(box, viewport)) {
6223
+ if (coversViewport(box, this.#fillRoom())) {
6124
6224
  return false;
6125
6225
  }
6126
6226
  const [edgeY, edgeX] = corner.split("-");
@@ -6162,7 +6262,16 @@ var PanelPlacement = class {
6162
6262
  * corner is what the panel grows from, so restoring a position without it
6163
6263
  * puts the box back and animates it out of the wrong side. Absent values are
6164
6264
  * captured as absent and removed on the way back, rather than written as
6165
- * empty strings that would outrank the placement.
6265
+ * empty strings that would outrank the placement. And what the inset is, because
6266
+ * one put back with no record of having come from a grip or a fill reads as a
6267
+ * host's own, and a phone would leave it holding the panel off the screen.
6268
+ *
6269
+ * The surface may have changed by the time of the undo, so the two halves are
6270
+ * put back apart. Both records are, always, each to its own store, so the
6271
+ * memory and the storage cannot disagree and the surface they belong to finds
6272
+ * them when it comes back. The inline values only mean something to the surface
6273
+ * they were read from: written on any other, they pin a sidebar to where a
6274
+ * floating panel used to be.
6166
6275
  */
6167
6276
  #captureGeometry() {
6168
6277
  const inset2 = this.#host.element.style.getPropertyValue("--ag-ui-inset");
@@ -6170,33 +6279,48 @@ var PanelPlacement = class {
6170
6279
  const corner = this.#host.element.getAttribute("data-expand-corner");
6171
6280
  const launcherPos = this.#launcherPos;
6172
6281
  const panelPos = this.#panelPos;
6173
- const dockPos = this.#dockPos;
6282
+ const dockPos = this.#dockPos ?? this.#readDockPosition();
6174
6283
  const expandCorner = this.#expandCorner;
6284
+ const gripInset = this.#gripInset;
6285
+ const fillInset = this.#fillInset;
6286
+ const cornerSpot = this.#cornerSpot ?? this.#readLauncherPosition();
6287
+ const owner = this.#positionOwner();
6175
6288
  return () => {
6176
- this.#restoreProperty("--ag-ui-inset", inset2);
6177
- this.#restoreProperty("--ag-ui-launcher-inset", launcherInset);
6178
- if (corner === null) {
6179
- this.#host.element.removeAttribute("data-expand-corner");
6180
- } else {
6181
- this.#host.element.setAttribute("data-expand-corner", corner);
6182
- }
6183
- this.#launcherPos = launcherPos;
6184
- this.#panelPos = panelPos;
6185
6289
  this.#dockPos = dockPos;
6186
- this.#expandCorner = expandCorner;
6187
- if (this.#docked()) {
6188
- this.#storeDockPosition(dockPos);
6189
- this.syncResizeAnchor();
6190
- return;
6191
- }
6192
- if (launcherPos === null) {
6290
+ this.#storeDockPosition(dockPos);
6291
+ if (cornerSpot === null) {
6292
+ this.#cornerSpot = null;
6193
6293
  this.#host.clearPreference(LAUNCHER_KEY);
6194
6294
  } else {
6195
- this.#storeLauncherPosition();
6295
+ this.#writeCornerSpot(cornerSpot);
6296
+ }
6297
+ if (owner === this.#positionOwner()) {
6298
+ this.#restoreProperty("--ag-ui-inset", inset2);
6299
+ this.#restoreProperty("--ag-ui-launcher-inset", launcherInset);
6300
+ if (corner === null) {
6301
+ this.#host.element.removeAttribute("data-expand-corner");
6302
+ } else {
6303
+ this.#host.element.setAttribute("data-expand-corner", corner);
6304
+ }
6305
+ this.#launcherPos = launcherPos;
6306
+ this.#panelPos = panelPos;
6307
+ this.#expandCorner = expandCorner;
6308
+ this.#gripInset = gripInset;
6309
+ this.#fillInset = fillInset;
6310
+ } else {
6311
+ this.#forgetCornerPosition();
6196
6312
  }
6313
+ this.restoreLauncherPosition();
6197
6314
  this.syncResizeAnchor();
6198
6315
  };
6199
6316
  }
6317
+ /** Which of the three kinds of surface decides where the panel stands. */
6318
+ #positionOwner() {
6319
+ if (this.#docked()) {
6320
+ return "dock";
6321
+ }
6322
+ return this.#surface().launcherDraggable ? "corner" : "placement";
6323
+ }
6200
6324
  /**
6201
6325
  * Which axes the current placement allows.
6202
6326
  *
@@ -6257,6 +6381,36 @@ var PanelPlacement = class {
6257
6381
  this.#anchor = anchor;
6258
6382
  this.#host.element.setAttribute("data-resize-anchor", `${anchor.y}-${anchor.x}`);
6259
6383
  this.#focusableGrip();
6384
+ this.#stampHeader();
6385
+ }
6386
+ /**
6387
+ * Tell the stylesheet whether the panel is the screen, for the header's cursor.
6388
+ *
6389
+ * Only this half of what refuses a header drag: the resting rules already answer
6390
+ * for the placement and for data-launcher-drag="false", and neither of those
6391
+ * re-applies anything when it changes, so a stamp taking them in would go on
6392
+ * refusing, in the cursor, a drag the header would take.
6393
+ */
6394
+ #stampHeader() {
6395
+ this.#header?.toggleAttribute("data-fills-screen", this.#fillsScreen());
6396
+ const element = this.#host.element;
6397
+ if (this.#fillsWindow() !== element.hasAttribute("data-fills-window")) {
6398
+ requestAnimationFrame(() => {
6399
+ const fills = this.#fillsWindow();
6400
+ if (fills !== element.hasAttribute("data-fills-window")) {
6401
+ this.#barTaken = fills ? { room: this.#viewport().height, window: this.#screen().height, bar: this.#barHeight } : null;
6402
+ element.toggleAttribute("data-fills-window", fills);
6403
+ }
6404
+ });
6405
+ }
6406
+ }
6407
+ /**
6408
+ * Whether the panel fills the window in a placement it can move in. Not one that owns
6409
+ * its position: full stops at the bar, so taking the bar away would make it fill the
6410
+ * window, and that would take the bar away for good.
6411
+ */
6412
+ #fillsWindow() {
6413
+ return this.#fillsScreen() && this.#surface().panelMovable;
6260
6414
  }
6261
6415
  /** Put a custom property back to a previous value, or remove it if there was none. */
6262
6416
  #restoreProperty(name, value) {
@@ -6346,6 +6500,10 @@ var PanelPlacement = class {
6346
6500
  element.style.removeProperty(DOCK_RIGHT);
6347
6501
  }
6348
6502
  element.toggleAttribute("data-dock-anchored", target === "none" && anchor !== null);
6503
+ this.#barHeight = height;
6504
+ if (target !== "bar") {
6505
+ this.#barTaken = null;
6506
+ }
6349
6507
  this.#dockTop = target === "bar" && height > 0 ? bar.getBoundingClientRect().top : target === "none" && anchor !== null ? anchor.getBoundingClientRect().top : Number.POSITIVE_INFINITY;
6350
6508
  this.#placeDocked();
6351
6509
  return height;
@@ -6368,19 +6526,43 @@ var PanelPlacement = class {
6368
6526
  * block reads the host's inset before its own, so a dragged spot written
6369
6527
  * there would hold the panel off the screen's edges. The spot itself stays,
6370
6528
  * for the next time there is room for it.
6529
+ *
6530
+ * A host's size that makes the panel the window hangs it off the resting inset's
6531
+ * far side, past the top of the window with the header on it under the bar, so
6532
+ * it stands at the start of the room instead, moved or not, as a corner panel
6533
+ * does, and the stylesheet takes the bar away for as long as it does. A placement
6534
+ * that owns its position keeps it, whatever the host's size.
6535
+ *
6536
+ * Under data-launcher-drag="false" a spot from before the attribute is left alone
6537
+ * while the panel is on the screen, as a corner panel's is, and the record stays
6538
+ * for the day the attribute comes off.
6371
6539
  */
6372
6540
  #placeDocked() {
6373
- if (!this.#docked() || !this.#panelMovable() || this.dragging()) {
6541
+ if (!this.#docked() || !this.#surface().panelMovable || this.dragging()) {
6374
6542
  return;
6375
6543
  }
6376
- const at2 = this.#dockPos ?? this.#readDockPosition();
6544
+ const element = this.#host.element;
6545
+ const at2 = this.#panelMovable() ? this.#dockPos ?? this.#readDockPosition() : null;
6377
6546
  if (at2 === null) {
6547
+ if (this.#dockPos !== null) {
6548
+ const filling = element.style.getPropertyValue("--ag-ui-inset") === this.#fillInset;
6549
+ if (!filling && !this.#offScreen()) {
6550
+ return;
6551
+ }
6552
+ this.#dockPos = null;
6553
+ element.style.removeProperty("--ag-ui-inset");
6554
+ }
6555
+ this.#placeUnstated();
6378
6556
  return;
6379
6557
  }
6380
6558
  this.#dockPos = at2;
6381
- this.#host.element.style.removeProperty("--ag-ui-inset");
6382
- const rest = this.#host.element.getBoundingClientRect();
6383
- if (coversViewport(rest, this.#viewport())) {
6559
+ element.style.removeProperty("--ag-ui-inset");
6560
+ const rest = element.getBoundingClientRect();
6561
+ const viewport = this.#viewport();
6562
+ if (coversViewport(rest, this.#fillRoom())) {
6563
+ if (hangsOff(rest, viewport, this.#screen())) {
6564
+ this.#standAtStart(rest);
6565
+ }
6384
6566
  return;
6385
6567
  }
6386
6568
  this.syncResizeAnchor();
@@ -6427,15 +6609,52 @@ var PanelPlacement = class {
6427
6609
  return this.#host.element.getAttribute("data-launcher-drag") !== "false" && this.#surface().panelMovable;
6428
6610
  }
6429
6611
  /**
6430
- * Whether a header drag may start now. A docked panel on a phone is the
6431
- * screen, and a drag there would pull it off the edges it fills; a corner
6432
- * panel's header is left as it was.
6612
+ * Whether a header drag may start now. A panel that is the screen, on a phone
6613
+ * or by a host's own sizing, has nowhere to go, and a drag there would pull it
6614
+ * off the edges it fills.
6433
6615
  */
6434
6616
  #canDragPanel() {
6435
- if (!this.#panelMovable()) {
6436
- return false;
6617
+ return this.#panelMovable() && !this.#fillsScreen();
6618
+ }
6619
+ /** Whether the panel covers the usable viewport, as it stands now. */
6620
+ #fillsScreen() {
6621
+ return coversViewport(this.#host.element.getBoundingClientRect(), this.#fillRoom());
6622
+ }
6623
+ /**
6624
+ * The box a fill is judged against: the usable one, unless taking the bar away is
6625
+ * what grew it.
6626
+ *
6627
+ * A host can write the bar's height into the inset it reserves. The panel fills the
6628
+ * box that leaves, so the bar goes, the height says 0, the host writes that, the box
6629
+ * grows by what the bar took, and the panel no longer fills it. Judged against that,
6630
+ * the bar comes back and it all goes round again every frame without end. So on the
6631
+ * same window, a box grown by the bar's height is the box it was, and the panel
6632
+ * stands where it stood with the bar gone. Grown by anything else, or on a window of
6633
+ * another size, it is a change of the host's or the reader's and is judged as one.
6634
+ * The slack is the pixel a host rounding the height up can add.
6635
+ */
6636
+ #fillRoom() {
6637
+ const viewport = this.#viewport();
6638
+ const taken = this.#barTaken;
6639
+ if (taken !== null && taken.window === this.#screen().height && Math.abs(viewport.height - taken.room - taken.bar) <= 1) {
6640
+ return { ...viewport, height: taken.room };
6437
6641
  }
6438
- return !this.#docked() || !coversViewport(this.#host.element.getBoundingClientRect(), this.#viewport());
6642
+ return viewport;
6643
+ }
6644
+ /**
6645
+ * Whether the panel stands off an edge of the screen, as it stands now.
6646
+ *
6647
+ * The screen rather than the usable box: a panel under chrome the host reserved
6648
+ * has its controls where the host can still bring them back, and is not what a
6649
+ * position kept under data-launcher-drag="false" has to give way to.
6650
+ */
6651
+ #offScreen() {
6652
+ const screen = this.#screen();
6653
+ return hangsOff(
6654
+ this.#host.element.getBoundingClientRect(),
6655
+ { left: 0, top: 0, ...screen },
6656
+ screen
6657
+ );
6439
6658
  }
6440
6659
  /**
6441
6660
  * Whether this placement lets the launcher be dragged, read per interaction
@@ -6568,9 +6787,6 @@ var PanelPlacement = class {
6568
6787
  * back rather than leaving a stale inline one behind.
6569
6788
  */
6570
6789
  #applyLauncherPlacement(at2) {
6571
- if (!this.#launcherDraggable()) {
6572
- return;
6573
- }
6574
6790
  this.#launcherPos = at2;
6575
6791
  this.#panelPos = null;
6576
6792
  const panel = this.#host.element.getBoundingClientRect();
@@ -6591,6 +6807,7 @@ var PanelPlacement = class {
6591
6807
  }
6592
6808
  /** Move the launcher live during a drag, without persisting. */
6593
6809
  #moveLauncher(left, top) {
6810
+ this.#cornerSpot = { left, top };
6594
6811
  this.#applyLauncherPlacement({ left, top });
6595
6812
  }
6596
6813
  /** Move the launcher and remember where, per tab, like the dragged size. */
@@ -6695,9 +6912,6 @@ var PanelPlacement = class {
6695
6912
  * its own right.
6696
6913
  */
6697
6914
  #restorePanelPosition(at2) {
6698
- if (!this.#launcherDraggable()) {
6699
- return;
6700
- }
6701
6915
  const rect = this.#host.element.getBoundingClientRect();
6702
6916
  const held = clampPanel(
6703
6917
  { left: at2.left, top: at2.top, right: at2.left + rect.width, bottom: at2.top + rect.height },
@@ -6723,6 +6937,9 @@ var PanelPlacement = class {
6723
6937
  * with it, the panel goes back where it was put; without it, the panel is
6724
6938
  * re-derived from the launcher and opens into whatever room the viewport has
6725
6939
  * now.
6940
+ *
6941
+ * Every caller is a gesture ending or a move being made, so where the two
6942
+ * stand now is what was stated. A restore never calls this.
6726
6943
  */
6727
6944
  #storeLauncherPosition() {
6728
6945
  const position = this.#launcherPos;
@@ -6730,38 +6947,151 @@ var PanelPlacement = class {
6730
6947
  return;
6731
6948
  }
6732
6949
  const panel = this.#panelPos;
6733
- this.#host.writePreference(
6734
- LAUNCHER_KEY,
6735
- JSON.stringify(panel === null ? position : { ...position, panel })
6736
- );
6950
+ this.#writeCornerSpot(panel === null ? position : { ...position, panel });
6951
+ }
6952
+ /** Make `spot` the stated one, and remember it. */
6953
+ #writeCornerSpot(spot) {
6954
+ this.#cornerSpot = spot;
6955
+ this.#host.writePreference(LAUNCHER_KEY, JSON.stringify(spot));
6737
6956
  }
6738
6957
  /**
6739
6958
  * Re-apply the dragged position against the current viewport: on connect,
6740
6959
  * whenever the window resizes, and before an expand. The viewport that
6741
6960
  * stored a position may since have shrunk, and a launcher past the edge is
6742
6961
  * unreachable -- it is the only way back to a collapsed conversation.
6962
+ *
6963
+ * The one gate on whether a stored position is this element's to honour: the
6964
+ * gestures check their own before they ever reach the placing below.
6743
6965
  */
6744
6966
  restoreLauncherPosition() {
6967
+ this.#placeFromRecord(true);
6968
+ this.#stampHeader();
6969
+ }
6970
+ /**
6971
+ * Re-apply it for a change in the panel's own layout, which needs no window or
6972
+ * reopen to happen: a line typed in the composer is one. A position waiting to
6973
+ * be handed back under data-launcher-drag="false" waits on, because a panel that leaves for the
6974
+ * host's corner as the reader starts typing is a jump with no cause they can see.
6975
+ * It waits no longer than the panel stays on the screen: a host's size can stand
6976
+ * it off an edge, where its header is out of reach until something else happens.
6977
+ */
6978
+ settleLauncherPosition() {
6979
+ this.#placeFromRecord(false);
6980
+ this.#stampHeader();
6981
+ }
6982
+ /** The placing behind {@link restoreLauncherPosition}. */
6983
+ #placeFromRecord(handBack) {
6745
6984
  if (this.#docked()) {
6746
6985
  this.#placeDocked();
6747
6986
  return;
6748
6987
  }
6749
- const stored = this.#readLauncherPosition();
6750
- const launcher = this.#launcherPos ?? stored;
6751
- if (launcher === null) {
6988
+ if (!this.#surface().launcherDraggable) {
6989
+ return;
6990
+ }
6991
+ const draggable = this.#launcherDraggable();
6992
+ if (!draggable && this.#launcherPos !== null) {
6993
+ if (!handBack && !this.#offScreen()) {
6994
+ return;
6995
+ }
6996
+ this.releaseLauncherPosition();
6997
+ }
6998
+ const spot = draggable ? this.#cornerSpot ?? this.#readLauncherPosition() : null;
6999
+ if (spot === null) {
7000
+ this.#placeUnstated();
6752
7001
  return;
6753
7002
  }
6754
- const panel = this.#panelPos ?? stored?.panel ?? null;
6755
- if (panel !== null) {
6756
- this.#launcherPos = { left: launcher.left, top: launcher.top };
6757
- this.#restorePanelPosition(panel);
7003
+ if (this.#standFullBleed(spot)) {
7004
+ return;
7005
+ }
7006
+ if (spot.panel !== void 0) {
7007
+ this.#launcherPos = { left: spot.left, top: spot.top };
7008
+ this.#restorePanelPosition(spot.panel);
6758
7009
  return;
6759
7010
  }
6760
7011
  const box = this.#launcherBox();
6761
7012
  this.#applyLauncherPlacement(
6762
- clampLauncher({ ...box, left: launcher.left, top: launcher.top }, this.#viewport())
7013
+ clampLauncher({ ...box, left: spot.left, top: spot.top }, this.#viewport())
6763
7014
  );
6764
7015
  }
7016
+ /**
7017
+ * Leave a corner panel full-bleed if it has become the screen, and say whether
7018
+ * it has.
7019
+ *
7020
+ * The inline inset comes off, because the small-viewport block reads the
7021
+ * host's inset before its own: a spot written there holds the panel off the
7022
+ * edges it should fill, and past them by the same amount, since its size is
7023
+ * the screen's. What the panel covers is read before that, from the size
7024
+ * alone, which no inset changes.
7025
+ *
7026
+ * The launcher stays a place of its own. It is positioned from the host box,
7027
+ * so it is measured again from the box that has just moved, and it is held on
7028
+ * the screen, because it is the way back from a collapsed conversation. The
7029
+ * spot itself is untouched, for the next time there is room for it.
7030
+ *
7031
+ * Above the breakpoint nothing in the stylesheet takes the panel to the edges.
7032
+ * A host's own size makes it the screen while the resting inset still spends a
7033
+ * gutter on one corner, which hangs the box off the opposite one, so there it
7034
+ * stands at the start of the room the host left free and keeps that inset. A
7035
+ * box the placement already holds on the screen has no inset to keep, which is
7036
+ * every phone, and a host's rule that placed the box itself.
7037
+ */
7038
+ #standFullBleed(spot) {
7039
+ const element = this.#host.element;
7040
+ const viewport = this.#viewport();
7041
+ if (!coversViewport(element.getBoundingClientRect(), viewport)) {
7042
+ return false;
7043
+ }
7044
+ element.style.removeProperty("--ag-ui-inset");
7045
+ const rest = element.getBoundingClientRect();
7046
+ const hanging = hangsOff(rest, viewport, this.#screen());
7047
+ const box = hanging ? atStartOf(viewport, rest) : rest;
7048
+ const launcher = { ...this.#launcherBox(), left: spot.left, top: spot.top };
7049
+ this.#placePanelAndLauncher(box, { ...launcher, ...clampLauncher(launcher, viewport) });
7050
+ if (!hanging) {
7051
+ element.style.removeProperty("--ag-ui-inset");
7052
+ }
7053
+ return true;
7054
+ }
7055
+ /**
7056
+ * Place a panel that has no spot, a corner one or a docked one, the half of
7057
+ * {@link #standFullBleed} with nothing stated to keep and no launcher of its own
7058
+ * to measure.
7059
+ *
7060
+ * Only an inset this element wrote comes off or goes back. One the host wrote
7061
+ * inline is a token a host may state on a phone as anywhere else, and with no
7062
+ * spot it never came from here.
7063
+ */
7064
+ #placeUnstated() {
7065
+ const element = this.#host.element;
7066
+ const inline = element.style.getPropertyValue("--ag-ui-inset");
7067
+ const viewport = this.#viewport();
7068
+ if (!coversViewport(element.getBoundingClientRect(), this.#fillRoom())) {
7069
+ if (inline === "" || inline === this.#fillInset) {
7070
+ this.#restoreProperty("--ag-ui-inset", this.#gripInset ?? "");
7071
+ this.syncResizeAnchor();
7072
+ }
7073
+ this.#fillInset = null;
7074
+ return;
7075
+ }
7076
+ if (inline === this.#gripInset || inline === this.#fillInset) {
7077
+ element.style.removeProperty("--ag-ui-inset");
7078
+ }
7079
+ const rest = element.getBoundingClientRect();
7080
+ const screen = this.#screen();
7081
+ if (element.style.getPropertyValue("--ag-ui-inset") === "" && hangsOff(rest, viewport, screen)) {
7082
+ this.#standAtStart(rest);
7083
+ }
7084
+ }
7085
+ /** Stand a box a host's size fills at the start of the room the host left free. */
7086
+ #standAtStart(rest) {
7087
+ this.#fillInset = insetFrom(
7088
+ { x: "left", y: "top" },
7089
+ atStartOf(this.#viewport(), rest),
7090
+ this.#screen()
7091
+ );
7092
+ this.#host.element.style.setProperty("--ag-ui-inset", this.#fillInset);
7093
+ this.syncResizeAnchor();
7094
+ }
6765
7095
  /** The persisted position for this instance, or null. */
6766
7096
  #readLauncherPosition() {
6767
7097
  const raw = this.#host.readPreference(LAUNCHER_KEY);
@@ -6796,9 +7126,15 @@ var PanelPlacement = class {
6796
7126
  if (this.#launcherDraggable()) {
6797
7127
  return;
6798
7128
  }
7129
+ this.#forgetCornerPosition();
7130
+ }
7131
+ /** Take everything off that placed a corner panel, leaving its record alone. */
7132
+ #forgetCornerPosition() {
6799
7133
  this.#launcherPos = null;
6800
7134
  this.#panelPos = null;
6801
7135
  this.#expandCorner = null;
7136
+ this.#gripInset = null;
7137
+ this.#fillInset = null;
6802
7138
  this.#host.element.style.removeProperty("--ag-ui-inset");
6803
7139
  this.#host.element.style.removeProperty("--ag-ui-launcher-inset");
6804
7140
  this.#host.element.removeAttribute("data-expand-corner");
@@ -6834,26 +7170,32 @@ var PanelPlacement = class {
6834
7170
  if (this.#docked()) {
6835
7171
  this.#dockPos = { left: box.left, top: box.top };
6836
7172
  }
6837
- if (grip.x !== this.#anchor.x && grip.y !== this.#anchor.y) {
6838
- return box;
6839
- }
6840
- const anchor = this.#anchor;
6841
- this.#host.element.style.setProperty("--ag-ui-inset", insetFrom(anchor, box, this.#screen()));
6842
- if (this.#launcherPos !== null) {
6843
- const size = this.#host.launcher.offsetWidth;
6844
- this.#launcherPos = {
6845
- left: anchor.x === "left" ? box.left : box.right - size,
6846
- top: anchor.y === "top" ? box.top : box.bottom - size
6847
- };
7173
+ if (grip.x === this.#anchor.x || grip.y === this.#anchor.y) {
7174
+ const anchor = this.#anchor;
7175
+ const inset2 = insetFrom(anchor, box, this.#screen());
7176
+ this.#host.element.style.setProperty("--ag-ui-inset", inset2);
7177
+ this.#gripInset = this.#launcherPos === null && !this.#docked() ? inset2 : null;
7178
+ if (this.#launcherPos !== null) {
7179
+ const size = this.#host.launcher.offsetWidth;
7180
+ this.#launcherPos = {
7181
+ left: anchor.x === "left" ? box.left : box.right - size,
7182
+ top: anchor.y === "top" ? box.top : box.bottom - size
7183
+ };
7184
+ }
6848
7185
  }
6849
- if (this.#panelPos !== null) {
6850
- this.#panelPos = { left: box.left, top: box.top };
7186
+ if (this.#panelPos !== null && !this.#docked()) {
7187
+ const drawn = this.#host.element.getBoundingClientRect();
7188
+ this.#panelPos = { left: drawn.left, top: drawn.top };
6851
7189
  }
6852
7190
  return box;
6853
7191
  }
6854
7192
  /** Finish a resize: keep the box, remember it, and re-read the pinned edges. */
6855
7193
  #commitResize(grip, box) {
6856
7194
  const held = this.#applyResize(grip, box);
7195
+ if (this.#host.element.style.getPropertyValue("--ag-ui-inset") === this.#fillInset) {
7196
+ this.#gripInset = this.#fillInset;
7197
+ this.#fillInset = null;
7198
+ }
6857
7199
  this.#persistSize({ width: held.right - held.left, height: held.bottom - held.top });
6858
7200
  this.#storeLauncherPosition();
6859
7201
  if (this.#dockPos !== null) {
@@ -9001,6 +9343,14 @@ var COLLAPSE_STYLES = `
9001
9343
  display: none;
9002
9344
  }
9003
9345
 
9346
+ /* A panel a host sized to the whole window stands on the strip, whose fixed box would
9347
+ draw over the composer's foot. So it goes as it does on a phone, by display, so the
9348
+ bar measures 0 and the host's padding goes with it, and the header's minimise
9349
+ control is the way back. Collapsed, the strip is the way back, so it stays. */
9350
+ :host([data-collapsed-to="bar"][data-fills-window]:not([collapsed])) .dock-bar {
9351
+ display: none;
9352
+ }
9353
+
9004
9354
  /* History from the bar: the panel's own drawer, moved beside the bar while
9005
9355
  the panel is away, drawn as a popover standing on the bar's end. There is
9006
9356
  no backdrop; a press outside the bar closes it, as Escape does.
@@ -9553,6 +9903,15 @@ var RESIZE_STYLES = `
9553
9903
  bottom: 0;
9554
9904
  }
9555
9905
 
9906
+ /* A docked panel a host sized to the whole window has nothing to give a grip: its size
9907
+ is the host's, and a place a grip left would outlast that size with the bar back
9908
+ under it. They go as they do on a phone. A corner panel keeps them, since it has no
9909
+ bar to stand clear of and a grip's place is its own. */
9910
+ :host([data-collapsed-to="bar"][data-fills-window]) .resize-handle,
9911
+ :host([data-collapsed-to="none"][data-fills-window]) .resize-handle {
9912
+ display: none;
9913
+ }
9914
+
9556
9915
  /* Docked: the placement owns the height, so the horizontal edges and every
9557
9916
  corner are inert and must not advertise a drag that does nothing. The two
9558
9917
  vertical edges remain, which is the same affordance these placements had
@@ -10706,6 +11065,15 @@ var FRAME_STYLES = `
10706
11065
  touch-action: auto;
10707
11066
  }
10708
11067
 
11068
+ /* A panel that is the screen has nowhere to go, so its header refuses the drag
11069
+ and must not offer it. No selector can see that a host has sized the panel to
11070
+ the window, so the placement stamps this from the measurement that refuses the
11071
+ drag. :host is carried for the specificity, not the scope, as below. Before the
11072
+ dragging rule, so a drag the header did take keeps its grabbing cursor. */
11073
+ :host .header[data-fills-screen] {
11074
+ cursor: auto;
11075
+ }
11076
+
10709
11077
  /* :host is carried for the specificity, not the scope: the placement rules
10710
11078
  above are a host selector plus a class plus an attribute, and a bare
10711
11079
  .header[data-dragging] loses to them -- silently, because the drag still
@@ -10755,6 +11123,14 @@ var FRAME_STYLES = `
10755
11123
  flex: none;
10756
11124
  }
10757
11125
 
11126
+ /* Under every theme: once the host sets the header text, the header behind the
11127
+ buttons is the host's colour, so their rest opacity goes. The frame's 0.85
11128
+ lets the header through the glyph, which took white on a mid-blue header from
11129
+ 5.1:1 to 4.2:1. With the header text set, the opacity below reads a colour,
11130
+ which no opacity can be, so it is invalid at computed-value time and falls to
11131
+ the initial 1; unset, it reads the fallback. The glyph is each theme's own
11132
+ business: this inherits the header text, and a theme that draws the buttons
11133
+ in another ink reads the same switch to yield. */
10758
11134
  .header-btn {
10759
11135
  display: inline-flex;
10760
11136
  flex: 0 0 auto;
@@ -10771,12 +11147,15 @@ var FRAME_STYLES = `
10771
11147
  padding: 0;
10772
11148
  border-radius: 6px;
10773
11149
  cursor: pointer;
10774
- opacity: 0.85;
11150
+ opacity: var(--_header-fg-set, 0.85);
10775
11151
  }
10776
11152
 
11153
+ /* The square is the host's own when it set one; otherwise it is mixed from the
11154
+ header text when the host set that, so it reads on any header the host
11155
+ painted; otherwise it is the theme's own. See the alias in the token defaults. */
10777
11156
  .header-btn:hover {
10778
11157
  opacity: 1;
10779
- background: rgba(255, 255, 255, 0.18);
11158
+ background: var(--_header-btn-hover);
10780
11159
  }
10781
11160
  `;
10782
11161
 
@@ -10854,18 +11233,19 @@ var TOKEN_STYLES = `
10854
11233
  /* Switches: a public token read with no fallback, named for it with -set.
10855
11234
  With the token unset the switch computes to the guaranteed-invalid value,
10856
11235
  so a rule that reads it with a fallback gets the fallback; that is how a
10857
- rule tells a token the host set from the theme's default for it. The
10858
- default theme's header buttons follow the header text only once a host
10859
- sets it, and a step group's calls follow the tool token only once a host
10860
- sets it (classic defaults --_tool-fg to the accent, which a group never
10861
- drew). */
11236
+ rule tells a token the host set from the theme's default for it. Every
11237
+ theme's header buttons follow the header text only once a host sets it,
11238
+ and a step group's calls follow the tool token only once a host sets it
11239
+ (classic defaults --_tool-fg to the accent, which a group never drew). */
10862
11240
  --_header-fg-set: var(--ag-ui-header-fg);
10863
11241
  --_tool-fg-set: var(--ag-ui-tool-fg);
10864
11242
  /* The header buttons' hover square, by default, once the header text is
10865
11243
  set. Here it is the text at about a fifth, which every browser can mix;
10866
11244
  where relative colour syntax is supported (see the block at the end) it
10867
11245
  is the text's opposite instead. It reads the switch, so it is unset with
10868
- it, and the square below falls to the panel's hover shade. */
11246
+ it, and the square below falls to the theme's own: the panel's hover shade
11247
+ here, restated by each theme that draws another, so one rule in the frame
11248
+ paints them all. */
10869
11249
  --_header-btn-hover-auto: color-mix(in srgb, var(--_header-fg-set) 18%, transparent);
10870
11250
  --_header-btn-hover: var(--ag-ui-header-btn-hover, var(--_header-btn-hover-auto, var(--_hover)));
10871
11251
  --_border: var(--ag-ui-border, var(--_line));
@@ -11306,6 +11686,7 @@ var CLASSIC_TOKENS = `
11306
11686
  --_tool-fg: var(--ag-ui-tool-fg, var(--_accent));
11307
11687
  --_header-bg: var(--ag-ui-header-bg, light-dark(var(--_accent), #1f1f30));
11308
11688
  --_header-fg: var(--ag-ui-header-fg, light-dark(#ffffff, #e8e8f2));
11689
+ --_header-btn-hover: var(--ag-ui-header-btn-hover, var(--_header-btn-hover-auto, rgba(255, 255, 255, 0.18)));
11309
11690
  --_border: var(--ag-ui-border, light-dark(#e2e2ec, #33334a));
11310
11691
  --_muted: var(--ag-ui-muted, light-dark(#6b7280, #9aa0b4));
11311
11692
  --_success: var(--ag-ui-success, #15803d);
@@ -11392,14 +11773,15 @@ var LEDGER_STYLES = `
11392
11773
  vertical-align: 1px;
11393
11774
  }
11394
11775
 
11776
+ /* Both inks yield once the host sets the header text, since the header behind
11777
+ them is then the host's colour (see the switches in the token defaults). */
11395
11778
  :host([theme~="ledger"]) .header-btn {
11396
- color: var(--_ink-3);
11779
+ color: var(--_header-fg-set, var(--_ink-3));
11397
11780
  border-radius: 3px;
11398
11781
  }
11399
11782
 
11400
11783
  :host([theme~="ledger"]) .header-btn:hover {
11401
- background: var(--_hover);
11402
- color: var(--_fg);
11784
+ color: var(--_header-fg-set, var(--_fg));
11403
11785
  }
11404
11786
 
11405
11787
  :host([theme~="ledger"]) .message--user {
@@ -11549,6 +11931,7 @@ var LEDGER_TOKENS = `
11549
11931
  --_msg-radius: var(--ag-ui-msg-radius, 0px);
11550
11932
  --_composer-radius: var(--ag-ui-composer-radius, 4px);
11551
11933
  --_user-bg: var(--ag-ui-user-bg, transparent);
11934
+ --_header-btn-hover: var(--ag-ui-header-btn-hover, var(--_header-btn-hover-auto, var(--_hover)));
11552
11935
  --_frame-shadow: var(--ag-ui-frame-shadow, 0 0 0 1px var(--_line-strong), 0 20px 40px -28px rgb(0 0 0 / 0.45));
11553
11936
  --_launcher-bg: var(--ag-ui-launcher-bg, var(--_fg));
11554
11937
  --_launcher-fg: var(--ag-ui-launcher-fg, var(--_bg));
@@ -11669,28 +12052,15 @@ ${THEME_HOST.quiet} .header-title {
11669
12052
 
11670
12053
  /* The buttons sit back in the third ink on the panel. Once the host sets the
11671
12054
  header text they draw in it instead, since the header behind them is then
11672
- the host's colour (see the switches in the token defaults). So does their
11673
- rest opacity: the frame's 0.85 lets the header through the glyph, which
11674
- took white on a mid-blue header from 5.1:1 to 4.2:1. With the header text
11675
- set, the opacity below reads a colour, which no opacity can be, so it is
11676
- invalid at computed-value time and falls to the initial 1; unset, it reads
11677
- the fallback, the frame's own 0.85. */
12055
+ the host's colour (see the switches in the token defaults). */
11678
12056
  ${THEME_HOST.quiet} .header-btn {
11679
12057
  color: var(--_header-fg-set, var(--_ink-3));
11680
- opacity: var(--_header-fg-set, 0.85);
11681
12058
  border-radius: 8px;
11682
12059
  }
11683
12060
 
11684
- /* The hover square is the host's own when it set one; otherwise it is mixed
11685
- from the header text when the host set that, so it reads on any header the
11686
- host painted; otherwise it is the panel shade.
11687
- The glyph takes the header text, which is the full ink when that is unset.
11688
- The opacity restates the frame's hover value, which the rest rule above
11689
- outweighs. */
12061
+ /* The glyph takes the header text, which is the full ink when that is unset. */
11690
12062
  ${THEME_HOST.quiet} .header-btn:hover {
11691
12063
  color: var(--_header-fg);
11692
- opacity: 1;
11693
- background: var(--_header-btn-hover);
11694
12064
  }
11695
12065
 
11696
12066
  ${THEME_HOST.quiet} .message--user {
@@ -11833,14 +12203,15 @@ var SOFT_STYLES = `
11833
12203
  background: linear-gradient(140deg, color-mix(in oklch, var(--_accent) 55%, var(--_bg)), var(--_accent));
11834
12204
  }
11835
12205
 
12206
+ /* Both inks yield once the host sets the header text, since the header behind
12207
+ them is then the host's colour (see the switches in the token defaults). */
11836
12208
  :host([theme~="soft"]) .header-btn {
11837
- color: var(--_ink-3);
12209
+ color: var(--_header-fg-set, var(--_ink-3));
11838
12210
  border-radius: 10px;
11839
12211
  }
11840
12212
 
11841
12213
  :host([theme~="soft"]) .header-btn:hover {
11842
- background: var(--_tint-2);
11843
- color: var(--_accent-ink);
12214
+ color: var(--_header-fg-set, var(--_accent-ink));
11844
12215
  }
11845
12216
 
11846
12217
  :host([theme~="soft"]) .messages {
@@ -11961,6 +12332,7 @@ var SOFT_TOKENS = `
11961
12332
  --_tint: var(--ag-ui-tint, color-mix(in oklch, var(--_accent) 5%, var(--_bg)));
11962
12333
  --_tint-2: var(--ag-ui-tint-2, color-mix(in oklch, var(--_accent) 13%, var(--_bg)));
11963
12334
  --_header-bg: var(--ag-ui-header-bg, var(--_tint));
12335
+ --_header-btn-hover: var(--ag-ui-header-btn-hover, var(--_header-btn-hover-auto, var(--_tint-2)));
11964
12336
  --_user-bg: var(--ag-ui-user-bg, var(--_tint-2));
11965
12337
  --_border: var(--ag-ui-border, color-mix(in oklch, var(--_accent) 14%, var(--_bg)));
11966
12338
  --_radius: var(--ag-ui-radius, 16px);
@@ -12778,7 +13150,18 @@ var TRANSCRIPT_STYLES = `
12778
13150
 
12779
13151
  visibility rather than display: none, because a box that generates no
12780
13152
  layout has no used values to read. Zero-sized, absolutely positioned and
12781
- inert, so it costs nothing but the read. */
13153
+ inert, so it costs nothing but the read.
13154
+
13155
+ The padding gives that box the size of the tokens, which a card narrower than
13156
+ one of them would scroll to reach. Scrollable overflow counts a box as its
13157
+ transform draws it, so a scale of nothing takes it out of that count on every
13158
+ side and in either direction, and leaves what is read: the padding's used values,
13159
+ and the containing block a percentage resolves against. Position fixed would add
13160
+ no scroll either, and would change that block.
13161
+
13162
+ From its own corner, which is inside whatever holds it where the centre of the
13163
+ padding need not be: kept for an engine that counts a point with no area. Chromium
13164
+ does not, so the suite cannot pin the origin. */
12782
13165
  .viewport-probe {
12783
13166
  position: absolute;
12784
13167
  top: 0;
@@ -12787,6 +13170,8 @@ var TRANSCRIPT_STYLES = `
12787
13170
  height: 0;
12788
13171
  visibility: hidden;
12789
13172
  pointer-events: none;
13173
+ transform: scale(0);
13174
+ transform-origin: 0 0;
12790
13175
  padding: var(--_viewport-inset-top) var(--_viewport-inset-right)
12791
13176
  var(--_viewport-inset-bottom) var(--_viewport-inset-left);
12792
13177
  }
@@ -19847,13 +20232,40 @@ var AgUiChat = class extends HTMLElement {
19847
20232
  * a field so `removeEventListener` on disconnect gets the same reference.
19848
20233
  */
19849
20234
  #onViewportResize = () => {
20235
+ if (this.#readyToPlace()) {
20236
+ this.#placement.restoreLauncherPosition();
20237
+ }
20238
+ };
20239
+ /**
20240
+ * The same for a change only the panel's own layout shows: a host's size, a
20241
+ * viewport inset token, a line typed in the composer. Not a moment to hand a
20242
+ * position back, so a panel under data-launcher-drag="false" stays put.
20243
+ */
20244
+ #onLayoutChange = () => {
20245
+ if (this.#readyToPlace()) {
20246
+ this.#placement.settleLauncherPosition();
20247
+ }
20248
+ };
20249
+ /** Bring the viewport's readings up to date, and say whether placing may follow. */
20250
+ #readyToPlace() {
19850
20251
  this.#placement.publishVisualViewport();
19851
20252
  this.#syncDock();
19852
- if (this.#placement.dragging()) {
19853
- return;
20253
+ return !this.#placement.dragging() && this.#hasBox();
20254
+ }
20255
+ /**
20256
+ * Whether this element generates a box at all. Hidden on itself or on any ancestor
20257
+ * it does not; a host collapsed to its launcher does, whatever its size, and that
20258
+ * launcher still has to be placed, which is why this is not a test of the size.
20259
+ */
20260
+ #hasBox() {
20261
+ return this.getClientRects().length > 0;
20262
+ }
20263
+ /** Restore for a moment that is not a viewport's, which showing this reports anyway. */
20264
+ #restoreIfBoxed() {
20265
+ if (this.#hasBox()) {
20266
+ this.#placement.restoreLauncherPosition();
19854
20267
  }
19855
- this.#placement.restoreLauncherPosition();
19856
- };
20268
+ }
19857
20269
  /** Mic button mount point (input row); the control mounts on connect when enabled. */
19858
20270
  #voiceSlot;
19859
20271
  /** The composer's mic, mounted on connect when transcription is available. */
@@ -20624,7 +21036,7 @@ var AgUiChat = class extends HTMLElement {
20624
21036
  this.#chatRegistration.join();
20625
21037
  this.#placement.restoreSize();
20626
21038
  requestAnimationFrame(() => {
20627
- this.#placement.restoreLauncherPosition();
21039
+ this.#restoreIfBoxed();
20628
21040
  this.#placement.syncResizeAnchor();
20629
21041
  });
20630
21042
  this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
@@ -20646,6 +21058,12 @@ var AgUiChat = class extends HTMLElement {
20646
21058
  const dockObserver = new ResizeObserver(() => this.#syncDock());
20647
21059
  dockObserver.observe(this.#dockBar);
20648
21060
  this.#connection.signal.addEventListener("abort", () => dockObserver.disconnect());
21061
+ const layoutObserver = new ResizeObserver(this.#onLayoutChange);
21062
+ layoutObserver.observe(this.#messages);
21063
+ this.#connection.signal.addEventListener("abort", () => layoutObserver.disconnect());
21064
+ this.#placement.probe.addEventListener("transitionrun", this.#onLayoutChange, {
21065
+ signal: this.#connection.signal
21066
+ });
20649
21067
  this.#syncDock();
20650
21068
  if (this.#surface().collapseTo === "none") {
20651
21069
  this.dispatchEvent(
@@ -20868,7 +21286,7 @@ var AgUiChat = class extends HTMLElement {
20868
21286
  this.#announceSurfaceChange(this.#strings.chatMinimised, null);
20869
21287
  }
20870
21288
  if (!collapsed) {
20871
- this.#placement.restoreLauncherPosition();
21289
+ this.#restoreIfBoxed();
20872
21290
  }
20873
21291
  if (collapsed) {
20874
21292
  this.setAttribute("collapsed", "");
@@ -20964,7 +21382,7 @@ var AgUiChat = class extends HTMLElement {
20964
21382
  if (!this.isConnected) {
20965
21383
  return;
20966
21384
  }
20967
- const height = this.#placement.syncDock(this.#dockBar, this.#resolveAnchor());
21385
+ const height = this.#hasBox() ? this.#placement.syncDock(this.#dockBar, this.#resolveAnchor()) : 0;
20968
21386
  if (height === this.#barHeight) {
20969
21387
  return;
20970
21388
  }
@@ -21578,6 +21996,8 @@ var AgUiChat = class extends HTMLElement {
21578
21996
  const probe = this.#placement.probe;
21579
21997
  probe.className = "viewport-probe";
21580
21998
  probe.setAttribute("aria-hidden", "true");
21999
+ probe.style.borderRadius = "var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) var(--_viewport-inset-left)";
22000
+ probe.style.transition = "border-radius 1ms";
21581
22001
  this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#dockBar);
21582
22002
  }
21583
22003
  /**
@@ -22225,7 +22645,7 @@ function setControlValue(el2, value) {
22225
22645
  }
22226
22646
 
22227
22647
  // src/version.ts
22228
- var VERSION = "0.49.0";
22648
+ var VERSION = "0.50.1";
22229
22649
  export {
22230
22650
  ATTACHMENT_EVENT,
22231
22651
  AgUiChat,