@artooi/ag-ui-web-component 0.48.0 → 0.50.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 (40) hide show
  1. package/CHANGELOG.md +192 -1
  2. package/README.md +70 -7
  3. package/dist/ag-ui-web-component.bundle.js +171 -107
  4. package/dist/ag-ui-web-component.bundle.js.map +3 -3
  5. package/dist/core/ag_ui_chat.d.ts +8 -2
  6. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  7. package/dist/index.js +591 -82
  8. package/dist/index.js.map +3 -3
  9. package/dist/tools/chat_surface_tools.d.ts +8 -5
  10. package/dist/tools/chat_surface_tools.d.ts.map +1 -1
  11. package/dist/ui/placement/dock_styles.d.ts.map +1 -1
  12. package/dist/ui/placement/panel_placement.d.ts +18 -1
  13. package/dist/ui/placement/panel_placement.d.ts.map +1 -1
  14. package/dist/ui/placement/surface_for.d.ts +15 -6
  15. package/dist/ui/placement/surface_for.d.ts.map +1 -1
  16. package/dist/ui/progress/tool_group_styles.d.ts.map +1 -1
  17. package/dist/ui/shell/frame_styles.d.ts.map +1 -1
  18. package/dist/ui/shell/token_styles.d.ts.map +1 -1
  19. package/dist/ui/themes/classic_tokens.d.ts.map +1 -1
  20. package/dist/ui/themes/ledger_styles.d.ts.map +1 -1
  21. package/dist/ui/themes/ledger_tokens.d.ts.map +1 -1
  22. package/dist/ui/themes/quiet_styles.d.ts.map +1 -1
  23. package/dist/ui/themes/soft_styles.d.ts.map +1 -1
  24. package/dist/ui/themes/soft_tokens.d.ts.map +1 -1
  25. package/package.json +1 -1
  26. package/src/core/ag_ui_chat.ts +56 -8
  27. package/src/tools/chat_surface_tools.ts +11 -10
  28. package/src/ui/placement/dock_styles.ts +5 -1
  29. package/src/ui/placement/panel_placement.ts +587 -82
  30. package/src/ui/placement/surface_for.ts +33 -8
  31. package/src/ui/progress/tool_group_styles.ts +3 -2
  32. package/src/ui/shell/frame_styles.ts +22 -2
  33. package/src/ui/shell/token_styles.ts +31 -0
  34. package/src/ui/themes/classic_tokens.ts +1 -0
  35. package/src/ui/themes/ledger_styles.ts +4 -3
  36. package/src/ui/themes/ledger_tokens.ts +1 -0
  37. package/src/ui/themes/quiet_styles.ts +6 -3
  38. package/src/ui/themes/soft_styles.ts +4 -3
  39. package/src/ui/themes/soft_tokens.ts +1 -0
  40. package/src/version.ts +1 -1
package/dist/index.js CHANGED
@@ -284,7 +284,7 @@ function createChatSurfaceTools(surface) {
284
284
  if (!report.movable) {
285
285
  return {
286
286
  moved: false,
287
- reason: report.fullBleed ? "the panel fills the screen, so there is nowhere to move it to" : report.collapseTo === "bar" || report.collapseTo === "none" ? "the panel is docked: it rises from the page's bottom bar and stays there" : report.draggable === false ? "this page has turned off moving the panel" : `the "${report.placement ?? ""}" placement owns the panel's position`,
287
+ reason: report.fullBleed ? "the panel fills the screen, so there is nowhere to move it to" : report.draggable === false ? "this page has turned off moving the panel" : `the "${report.placement ?? ""}" placement owns the panel's position`,
288
288
  suggestion: report.collapsible ? "minimise_chat" : null
289
289
  };
290
290
  }
@@ -5810,12 +5810,14 @@ function boxAt(grip, axis, rect, x2, y2) {
5810
5810
  var CORNER = {
5811
5811
  collapseTo: "launcher",
5812
5812
  launcherDraggable: true,
5813
+ panelMovable: true,
5813
5814
  resizeAxis: "both",
5814
5815
  startsCollapsed: true
5815
5816
  };
5816
5817
  var DOCKED = {
5817
5818
  collapseTo: "launcher",
5818
5819
  launcherDraggable: false,
5820
+ panelMovable: false,
5819
5821
  resizeAxis: "width",
5820
5822
  startsCollapsed: false
5821
5823
  };
@@ -5831,22 +5833,36 @@ var SURFACES = /* @__PURE__ */ new Map([
5831
5833
  {
5832
5834
  collapseTo: "launcher",
5833
5835
  launcherDraggable: false,
5836
+ panelMovable: false,
5834
5837
  resizeAxis: "none",
5835
5838
  startsCollapsed: false
5836
5839
  }
5837
5840
  ],
5838
5841
  [
5839
5842
  "page",
5840
- { collapseTo: null, launcherDraggable: false, resizeAxis: "none", startsCollapsed: false }
5843
+ {
5844
+ collapseTo: null,
5845
+ launcherDraggable: false,
5846
+ panelMovable: false,
5847
+ resizeAxis: "none",
5848
+ startsCollapsed: false
5849
+ }
5841
5850
  ],
5842
5851
  [
5843
5852
  "embedded",
5844
- { collapseTo: "header", launcherDraggable: false, resizeAxis: "both", startsCollapsed: false }
5853
+ {
5854
+ collapseTo: "header",
5855
+ launcherDraggable: false,
5856
+ panelMovable: false,
5857
+ resizeAxis: "both",
5858
+ startsCollapsed: false
5859
+ }
5845
5860
  ]
5846
5861
  ]);
5847
5862
  var UNKNOWN = {
5848
5863
  collapseTo: "launcher",
5849
5864
  launcherDraggable: false,
5865
+ panelMovable: false,
5850
5866
  resizeAxis: "both",
5851
5867
  startsCollapsed: false
5852
5868
  };
@@ -5868,6 +5884,7 @@ var DRAG_HEIGHT = "--_drag-height";
5868
5884
  var DOCK_BOTTOM = "--_dock-bottom";
5869
5885
  var DOCK_RIGHT = "--_dock-right";
5870
5886
  var LAUNCHER_KEY = "ag-ui-chat:launcher";
5887
+ var DOCK_KEY = "ag-ui-chat:dock";
5871
5888
  function asPoint(value) {
5872
5889
  if (typeof value !== "object" || value === null) {
5873
5890
  return null;
@@ -5878,6 +5895,18 @@ function asPoint(value) {
5878
5895
  function coversViewport(box, viewport) {
5879
5896
  return box.width >= viewport.width - 1 && box.height >= viewport.height - 1;
5880
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
+ }
5881
5910
  function insetFrom(corner, box, screen) {
5882
5911
  const side = (value) => `${Math.round(value)}px`;
5883
5912
  return [
@@ -5907,8 +5936,9 @@ var PanelPlacement = class {
5907
5936
  * header on screen, and the launcher is the handle then.
5908
5937
  */
5909
5938
  enablePanelDrag(header) {
5939
+ this.#header = header;
5910
5940
  enablePanelDrag(header, {
5911
- enabled: () => !this.#host.collapsed() && this.#launcherDraggable(),
5941
+ enabled: () => !this.#host.collapsed() && this.#canDragPanel(),
5912
5942
  rect: () => this.#host.element.getBoundingClientRect(),
5913
5943
  apply: (box, from) => this.#movePanel(box, from),
5914
5944
  commit: (box, from) => this.#commitPanel(box, from)
@@ -5931,12 +5961,19 @@ var PanelPlacement = class {
5931
5961
  }
5932
5962
  /** Build the eight resize grips into `chat`, with one of them in the tab order. */
5933
5963
  mountResizeGrips(chat) {
5964
+ this.#resizing = false;
5934
5965
  for (const grip of RESIZE_GRIPS) {
5935
5966
  const handle = createResizeHandle(grip, {
5936
5967
  axis: () => this.#resizeAxis(),
5937
5968
  rect: () => this.#host.element.getBoundingClientRect(),
5938
- apply: (box) => this.#applyResize(grip, box),
5939
- 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
+ },
5940
5977
  label: this.#host.strings().resizePanel
5941
5978
  });
5942
5979
  handle.tabIndex = -1;
@@ -5954,13 +5991,63 @@ var PanelPlacement = class {
5954
5991
  * Drop the dragged size and its stored copy, so the placement's own size and
5955
5992
  * the host's tokens apply again. For a host entering a sized state of its
5956
5993
  * own, which a user's drag would otherwise outrank.
5994
+ *
5995
+ * A docked panel the user dragged off its dock goes back to it as well, and
5996
+ * the stored spot with it: there the position is part of the same resting
5997
+ * state the host is asking for. A corner panel keeps a dragged position, as
5998
+ * it always has.
5957
5999
  */
5958
6000
  resetSize() {
6001
+ const sized = this.#host.element.getBoundingClientRect();
5959
6002
  this.#host.element.style.removeProperty(DRAG_WIDTH);
5960
6003
  this.#host.element.style.removeProperty(DRAG_HEIGHT);
5961
6004
  this.#host.clearPreference(SIZE_KEY);
6005
+ if (this.#docked()) {
6006
+ if (this.#dockPos !== null) {
6007
+ this.#host.element.style.removeProperty("--ag-ui-inset");
6008
+ }
6009
+ this.#dockPos = null;
6010
+ this.#storeDockPosition(null);
6011
+ } else {
6012
+ this.#carrySpotWithSize(sized);
6013
+ }
5962
6014
  this.syncResizeAnchor();
5963
6015
  }
6016
+ /**
6017
+ * Carry a stated corner spot along with a dragged size coming off.
6018
+ *
6019
+ * The size comes off toward the corner the layout holds still, so the top-left
6020
+ * the spot is kept as moves with it, and a restore from the old one would swing
6021
+ * the panel away from its launcher. Only that movement goes onto the spot, not
6022
+ * where the box stands: in a window narrower than the spot the box is held
6023
+ * short of it, and that is not something the user stated.
6024
+ *
6025
+ * Left alone while a host's own size is in force, because that cannot be told
6026
+ * from the dragged size coming off, and it may be a state the host is about to
6027
+ * leave: counting it would throw the spot off by as much as that state is
6028
+ * bigger. A panel that is the screen needs no case of its own: the small-viewport
6029
+ * block ignores a dragged size, so its box does not move.
6030
+ *
6031
+ * Left alone too under data-launcher-drag="false", where a spot left over from
6032
+ * before the attribute is a record kept for the day it comes off, and nothing
6033
+ * that happens to the box now is a statement about it.
6034
+ */
6035
+ #carrySpotWithSize(sized) {
6036
+ const element = this.#host.element;
6037
+ const box = element.getBoundingClientRect();
6038
+ const spot = this.#cornerSpot ?? this.#readLauncherPosition();
6039
+ const style = getComputedStyle(element);
6040
+ if (spot?.panel === void 0 || !this.#launcherDraggable() || style.getPropertyValue("--ag-ui-width") !== "" || style.getPropertyValue("--ag-ui-height") !== "") {
6041
+ return;
6042
+ }
6043
+ this.#writeCornerSpot({
6044
+ ...spot,
6045
+ panel: {
6046
+ left: spot.panel.left + box.left - sized.left,
6047
+ top: spot.panel.top + box.top - sized.top
6048
+ }
6049
+ });
6050
+ }
5964
6051
  /**
5965
6052
  * A zero-sized box carrying the host's viewport insets as padding, so they
5966
6053
  * can be read back as used pixel lengths. See the `.viewport-probe` rule for
@@ -5985,6 +6072,49 @@ var PanelPlacement = class {
5985
6072
  * from the launcher would move the panel the user just placed.
5986
6073
  */
5987
6074
  #panelPos = null;
6075
+ /**
6076
+ * The spot #launcherPos and #panelPos were last stated as, or null while
6077
+ * nothing has been stated since this element connected.
6078
+ *
6079
+ * Those two say where the launcher and the panel stand now, held inside the
6080
+ * viewport, because that is where a press finds them and a drag starts from.
6081
+ * This is what the user asked for, kept apart so that a window narrower than
6082
+ * the spot, or a phone that took the panel full-bleed, gives it back unchanged
6083
+ * once there is room again. Only a gesture, a move or a dragged size coming off
6084
+ * changes it; re-applying it against a viewport never does, which is the difference from a clamped
6085
+ * position quietly becoming the record one resize at a time.
6086
+ */
6087
+ #cornerSpot = null;
6088
+ /**
6089
+ * The inset a grip wrote on a corner panel with no spot, or null. A grip on an
6090
+ * edge the layout holds moves the panel without stating a spot, so this is the
6091
+ * only way to tell its inline inset from one a host wrote there, which stays,
6092
+ * and to give it back once a phone has taken the panel full-bleed.
6093
+ */
6094
+ #gripInset = null;
6095
+ /**
6096
+ * The inset standing a panel with no spot at the start of a window a host's
6097
+ * size fills, or null. Told apart from a host's own for the same reason, and it
6098
+ * lasts only as long as that size does.
6099
+ */
6100
+ #fillInset = null;
6101
+ /**
6102
+ * Where the user dragged a docked panel, in viewport coordinates, or null
6103
+ * while it stands on the bar or the anchor.
6104
+ *
6105
+ * Apart from the two corner positions above because it moves nothing else:
6106
+ * a docked panel has no launcher to carry, and the corner it opens from is
6107
+ * the dock's. Held as the user stated it and never as clamped, so a bar that
6108
+ * grew, or a phone that took the panel full-bleed, gives the spot back
6109
+ * unchanged once there is room for it again.
6110
+ */
6111
+ #dockPos = null;
6112
+ /**
6113
+ * The line a docked panel stays above, in viewport coordinates: the bar's
6114
+ * top, or the anchor's. Infinite where neither is drawn. Measured by
6115
+ * syncDock, which runs on every change either can make.
6116
+ */
6117
+ #dockTop = Number.POSITIVE_INFINITY;
5988
6118
  /**
5989
6119
  * The corner the panel opens away from, once this element is placing itself.
5990
6120
  * Null means the host's layout still decides, and the anchor is measured.
@@ -5996,6 +6126,10 @@ var PanelPlacement = class {
5996
6126
  * second while the panel is already being laid out on every one of them.
5997
6127
  */
5998
6128
  #anchor = { x: "right", y: "bottom" };
6129
+ /** The header the panel drag listens on, stamped by #stampHeader. */
6130
+ #header = null;
6131
+ /** Whether a grip has applied a box it has not committed yet. */
6132
+ #resizing = false;
5999
6133
  /** The eight grips, by name, so the keyboard-reachable one can be moved. */
6000
6134
  #resizeHandles = /* @__PURE__ */ new Map();
6001
6135
  /**
@@ -6013,7 +6147,7 @@ var PanelPlacement = class {
6013
6147
  * rather than collapsed to nothing.
6014
6148
  */
6015
6149
  #withinViewport(box) {
6016
- const viewport = this.#viewport();
6150
+ const viewport = this.#docked() && this.#panelMovable() ? this.#dockRoom() : this.#viewport();
6017
6151
  const left = viewport.left + SCREEN_EDGE_MARGIN;
6018
6152
  const top = viewport.top + SCREEN_EDGE_MARGIN;
6019
6153
  const right = viewport.left + viewport.width - SCREEN_EDGE_MARGIN;
@@ -6030,10 +6164,12 @@ var PanelPlacement = class {
6030
6164
  *
6031
6165
  * Read from the stamp the drag helpers already set, rather than tracked
6032
6166
  * separately: one source of truth, and it clears on `pointercancel` as well
6033
- * as `pointerup`, which is the end a touch gesture usually gets.
6167
+ * as `pointerup`, which is the end a touch gesture usually gets. The one
6168
+ * gesture that stamps nothing is an arrow key on a grip, which applies a box
6169
+ * per key and states it only when the key comes up, hence #resizing.
6034
6170
  */
6035
6171
  dragging() {
6036
- return this.#host.launcher.hasAttribute("data-dragging") || this.#host.root.querySelector(".header[data-dragging]") !== null;
6172
+ return this.#resizing || this.#host.launcher.hasAttribute("data-dragging") || this.#host.root.querySelector(".header[data-dragging], .resize-handle[data-dragging]") !== null;
6037
6173
  }
6038
6174
  /** The report behind `AgUiChat.describeSurface`, whose doc is the contract. */
6039
6175
  describeSurface() {
@@ -6045,8 +6181,13 @@ var PanelPlacement = class {
6045
6181
  collapseTo: this.#surface().collapseTo,
6046
6182
  collapsed: this.#host.collapsed(),
6047
6183
  collapsible: this.#host.collapsible(),
6048
- movable: this.#launcherDraggable() && !fullBleed,
6049
- draggable: this.#launcherDraggable(),
6184
+ movable: this.#panelMovable() && !fullBleed,
6185
+ // The page's setting alone: true for every placement unless the host set
6186
+ // `data-launcher-drag="false"`. Whether this panel moves is `movable`,
6187
+ // and reading the launcher or the dock in here made the field change
6188
+ // with `collapse-to`, so a docked sidebar reported that the page had
6189
+ // turned moving off when the placement owned its position.
6190
+ draggable: this.#host.element.getAttribute("data-launcher-drag") !== "false",
6050
6191
  fullBleed,
6051
6192
  box: {
6052
6193
  left: Math.round(box.left),
@@ -6064,7 +6205,7 @@ var PanelPlacement = class {
6064
6205
  }
6065
6206
  /** The move behind `AgUiChat.moveTo`, whose doc is the contract. */
6066
6207
  moveTo(corner, options = {}) {
6067
- if (!this.#launcherDraggable()) {
6208
+ if (!this.#panelMovable()) {
6068
6209
  return false;
6069
6210
  }
6070
6211
  const restore = options.announce === true ? this.#captureGeometry() : null;
@@ -6074,11 +6215,22 @@ var PanelPlacement = class {
6074
6215
  return false;
6075
6216
  }
6076
6217
  const [edgeY, edgeX] = corner.split("-");
6077
- const nearX = viewport.left + EDGE_MARGIN;
6078
- const nearY = viewport.top + EDGE_MARGIN;
6079
- const left = edgeX === "left" ? nearX : Math.max(nearX, viewport.left + viewport.width - EDGE_MARGIN - box.width);
6080
- const top = edgeY === "top" ? nearY : Math.max(nearY, viewport.top + viewport.height - EDGE_MARGIN - box.height);
6218
+ const docked = this.#docked();
6219
+ const room = docked ? this.#dockRoom() : viewport;
6220
+ const nearX = room.left + EDGE_MARGIN;
6221
+ const nearY = room.top + EDGE_MARGIN;
6222
+ const left = edgeX === "left" ? nearX : Math.max(nearX, room.left + room.width - EDGE_MARGIN - box.width);
6223
+ const top = edgeY === "top" ? nearY : Math.max(nearY, room.top + room.height - EDGE_MARGIN - box.height);
6081
6224
  const host = { left, top, right: left + box.width, bottom: top + box.height };
6225
+ if (docked) {
6226
+ this.#dockPos = { left, top };
6227
+ this.#writeDockInset(host);
6228
+ this.#storeDockPosition(this.#dockPos);
6229
+ if (restore !== null) {
6230
+ this.#host.announceSurfaceChange(this.#host.strings().chatMoved, restore);
6231
+ }
6232
+ return true;
6233
+ }
6082
6234
  const launcherWidth = this.#host.launcher.offsetWidth;
6083
6235
  const launcherHeight = this.#host.launcher.offsetHeight;
6084
6236
  this.#placePanelAndLauncher(host, {
@@ -6102,6 +6254,13 @@ var PanelPlacement = class {
6102
6254
  * puts the box back and animates it out of the wrong side. Absent values are
6103
6255
  * captured as absent and removed on the way back, rather than written as
6104
6256
  * empty strings that would outrank the placement.
6257
+ *
6258
+ * The surface may have changed by the time of the undo, so the two halves are
6259
+ * put back apart. Both records are, always, each to its own store, so the
6260
+ * memory and the storage cannot disagree and the surface they belong to finds
6261
+ * them when it comes back. The inline values only mean something to the surface
6262
+ * they were read from: written on any other, they pin a sidebar to where a
6263
+ * floating panel used to be.
6105
6264
  */
6106
6265
  #captureGeometry() {
6107
6266
  const inset2 = this.#host.element.style.getPropertyValue("--ag-ui-inset");
@@ -6109,26 +6268,44 @@ var PanelPlacement = class {
6109
6268
  const corner = this.#host.element.getAttribute("data-expand-corner");
6110
6269
  const launcherPos = this.#launcherPos;
6111
6270
  const panelPos = this.#panelPos;
6271
+ const dockPos = this.#dockPos ?? this.#readDockPosition();
6112
6272
  const expandCorner = this.#expandCorner;
6273
+ const cornerSpot = this.#cornerSpot ?? this.#readLauncherPosition();
6274
+ const owner = this.#positionOwner();
6113
6275
  return () => {
6114
- this.#restoreProperty("--ag-ui-inset", inset2);
6115
- this.#restoreProperty("--ag-ui-launcher-inset", launcherInset);
6116
- if (corner === null) {
6117
- this.#host.element.removeAttribute("data-expand-corner");
6276
+ this.#dockPos = dockPos;
6277
+ this.#storeDockPosition(dockPos);
6278
+ if (cornerSpot === null) {
6279
+ this.#cornerSpot = null;
6280
+ this.#host.clearPreference(LAUNCHER_KEY);
6118
6281
  } else {
6119
- this.#host.element.setAttribute("data-expand-corner", corner);
6282
+ this.#writeCornerSpot(cornerSpot);
6120
6283
  }
6121
- this.#launcherPos = launcherPos;
6122
- this.#panelPos = panelPos;
6123
- this.#expandCorner = expandCorner;
6124
- if (launcherPos === null) {
6125
- this.#host.clearPreference(LAUNCHER_KEY);
6284
+ if (owner === this.#positionOwner()) {
6285
+ this.#restoreProperty("--ag-ui-inset", inset2);
6286
+ this.#restoreProperty("--ag-ui-launcher-inset", launcherInset);
6287
+ if (corner === null) {
6288
+ this.#host.element.removeAttribute("data-expand-corner");
6289
+ } else {
6290
+ this.#host.element.setAttribute("data-expand-corner", corner);
6291
+ }
6292
+ this.#launcherPos = launcherPos;
6293
+ this.#panelPos = panelPos;
6294
+ this.#expandCorner = expandCorner;
6126
6295
  } else {
6127
- this.#storeLauncherPosition();
6296
+ this.#forgetCornerPosition();
6128
6297
  }
6298
+ this.restoreLauncherPosition();
6129
6299
  this.syncResizeAnchor();
6130
6300
  };
6131
6301
  }
6302
+ /** Which of the three kinds of surface decides where the panel stands. */
6303
+ #positionOwner() {
6304
+ if (this.#docked()) {
6305
+ return "dock";
6306
+ }
6307
+ return this.#surface().launcherDraggable ? "corner" : "placement";
6308
+ }
6132
6309
  /**
6133
6310
  * Which axes the current placement allows.
6134
6311
  *
@@ -6189,6 +6366,18 @@ var PanelPlacement = class {
6189
6366
  this.#anchor = anchor;
6190
6367
  this.#host.element.setAttribute("data-resize-anchor", `${anchor.y}-${anchor.x}`);
6191
6368
  this.#focusableGrip();
6369
+ this.#stampHeader();
6370
+ }
6371
+ /**
6372
+ * Tell the stylesheet whether the panel is the screen, for the header's cursor.
6373
+ *
6374
+ * Only this half of what refuses a header drag: the resting rules already answer
6375
+ * for the placement and for data-launcher-drag="false", and neither of those
6376
+ * re-applies anything when it changes, so a stamp taking them in would go on
6377
+ * refusing, in the cursor, a drag the header would take.
6378
+ */
6379
+ #stampHeader() {
6380
+ this.#header?.toggleAttribute("data-fills-screen", this.#fillsScreen());
6192
6381
  }
6193
6382
  /** Put a custom property back to a previous value, or remove it if there was none. */
6194
6383
  #restoreProperty(name, value) {
@@ -6242,7 +6431,10 @@ var PanelPlacement = class {
6242
6431
  const element = this.#host.element;
6243
6432
  return surfaceFor(element.getAttribute("placement"), element.getAttribute("collapse-to"));
6244
6433
  }
6245
- /** Whether the panel rises from the bottom bar or the host's anchor. */
6434
+ /**
6435
+ * Whether the panel rises from the bottom bar or the host's anchor, until the
6436
+ * user drags it somewhere else.
6437
+ */
6246
6438
  #docked() {
6247
6439
  const target = this.#surface().collapseTo;
6248
6440
  return target === "bar" || target === "none";
@@ -6275,8 +6467,98 @@ var PanelPlacement = class {
6275
6467
  element.style.removeProperty(DOCK_RIGHT);
6276
6468
  }
6277
6469
  element.toggleAttribute("data-dock-anchored", target === "none" && anchor !== null);
6470
+ this.#dockTop = target === "bar" && height > 0 ? bar.getBoundingClientRect().top : target === "none" && anchor !== null ? anchor.getBoundingClientRect().top : Number.POSITIVE_INFINITY;
6471
+ this.#placeDocked();
6278
6472
  return height;
6279
6473
  }
6474
+ /**
6475
+ * The part of the usable viewport a docked panel may stand in: all of it
6476
+ * above the dock's line. The bar is what it minimises to, and the anchor is
6477
+ * the host's own way back, so a dragged panel never covers either.
6478
+ */
6479
+ #dockRoom() {
6480
+ const viewport = this.#viewport();
6481
+ const bottom = Math.min(viewport.top + viewport.height, this.#dockTop);
6482
+ return { ...viewport, height: Math.max(0, bottom - viewport.top) };
6483
+ }
6484
+ /**
6485
+ * Put a docked panel back where the user dragged it, held inside its room.
6486
+ *
6487
+ * The inline inset comes off first, which is both how the panel's resting
6488
+ * box is measured and what leaves a phone full-bleed: the small-viewport
6489
+ * block reads the host's inset before its own, so a dragged spot written
6490
+ * there would hold the panel off the screen's edges. The spot itself stays,
6491
+ * for the next time there is room for it.
6492
+ */
6493
+ #placeDocked() {
6494
+ if (!this.#docked() || !this.#panelMovable() || this.dragging()) {
6495
+ return;
6496
+ }
6497
+ const at2 = this.#dockPos ?? this.#readDockPosition();
6498
+ if (at2 === null) {
6499
+ return;
6500
+ }
6501
+ this.#dockPos = at2;
6502
+ this.#host.element.style.removeProperty("--ag-ui-inset");
6503
+ const rest = this.#host.element.getBoundingClientRect();
6504
+ if (coversViewport(rest, this.#viewport())) {
6505
+ return;
6506
+ }
6507
+ this.syncResizeAnchor();
6508
+ this.#writeDockInset(
6509
+ clampPanel(
6510
+ { left: at2.left, top: at2.top, right: at2.left + rest.width, bottom: at2.top + rest.height },
6511
+ this.#dockRoom(),
6512
+ SCREEN_EDGE_MARGIN
6513
+ )
6514
+ );
6515
+ }
6516
+ /** Write the inset that stands a docked panel at `box`. */
6517
+ #writeDockInset(box) {
6518
+ this.#host.element.style.setProperty(
6519
+ "--ag-ui-inset",
6520
+ insetFrom(this.#anchor, box, this.#screen())
6521
+ );
6522
+ }
6523
+ /** Remember a docked panel's dragged spot, or forget it for null. */
6524
+ #storeDockPosition(at2) {
6525
+ if (at2 === null) {
6526
+ this.#host.clearPreference(DOCK_KEY);
6527
+ return;
6528
+ }
6529
+ this.#host.writePreference(DOCK_KEY, JSON.stringify({ left: at2.left, top: at2.top }));
6530
+ }
6531
+ /** The stored spot of a docked panel, or null for none or anything corrupt. */
6532
+ #readDockPosition() {
6533
+ const raw = this.#host.readPreference(DOCK_KEY);
6534
+ if (raw === null) {
6535
+ return null;
6536
+ }
6537
+ try {
6538
+ return asPoint(JSON.parse(raw));
6539
+ } catch {
6540
+ return null;
6541
+ }
6542
+ }
6543
+ /**
6544
+ * Whether the header drag and moveTo may move the open panel: the
6545
+ * placement's answer, unless the host turned moving off.
6546
+ */
6547
+ #panelMovable() {
6548
+ return this.#host.element.getAttribute("data-launcher-drag") !== "false" && this.#surface().panelMovable;
6549
+ }
6550
+ /**
6551
+ * Whether a header drag may start now. A panel that is the screen, on a phone
6552
+ * or by a host's own sizing, has nowhere to go, and a drag there would pull it
6553
+ * off the edges it fills.
6554
+ */
6555
+ #canDragPanel() {
6556
+ return this.#panelMovable() && !this.#fillsScreen();
6557
+ }
6558
+ /** Whether the panel covers the usable viewport, as it stands now. */
6559
+ #fillsScreen() {
6560
+ return coversViewport(this.#host.element.getBoundingClientRect(), this.#viewport());
6561
+ }
6280
6562
  /**
6281
6563
  * Whether this placement lets the launcher be dragged, read per interaction
6282
6564
  * because `placement` is a live attribute. `data-launcher-drag="false"` opts
@@ -6408,9 +6690,6 @@ var PanelPlacement = class {
6408
6690
  * back rather than leaving a stale inline one behind.
6409
6691
  */
6410
6692
  #applyLauncherPlacement(at2) {
6411
- if (!this.#launcherDraggable()) {
6412
- return;
6413
- }
6414
6693
  this.#launcherPos = at2;
6415
6694
  this.#panelPos = null;
6416
6695
  const panel = this.#host.element.getBoundingClientRect();
@@ -6431,6 +6710,7 @@ var PanelPlacement = class {
6431
6710
  }
6432
6711
  /** Move the launcher live during a drag, without persisting. */
6433
6712
  #moveLauncher(left, top) {
6713
+ this.#cornerSpot = { left, top };
6434
6714
  this.#applyLauncherPlacement({ left, top });
6435
6715
  }
6436
6716
  /** Move the launcher and remember where, per tab, like the dragged size. */
@@ -6453,6 +6733,12 @@ var PanelPlacement = class {
6453
6733
  * other still names the old one would move the launcher for no reason.
6454
6734
  */
6455
6735
  #movePanel(box, from) {
6736
+ if (this.#docked()) {
6737
+ const held2 = clampPanel(box, this.#dockRoom(), SCREEN_EDGE_MARGIN);
6738
+ this.#dockPos = { left: held2.left, top: held2.top };
6739
+ this.#writeDockInset(held2);
6740
+ return { held: held2, launcher: null };
6741
+ }
6456
6742
  if (!this.#launcherDraggable()) {
6457
6743
  return { held: box, launcher: null };
6458
6744
  }
@@ -6492,6 +6778,10 @@ var PanelPlacement = class {
6492
6778
  */
6493
6779
  #commitPanel(box, from) {
6494
6780
  const { held, launcher } = this.#movePanel(box, from);
6781
+ if (this.#docked()) {
6782
+ this.#storeDockPosition({ left: held.left, top: held.top });
6783
+ return;
6784
+ }
6495
6785
  if (launcher === null) {
6496
6786
  return;
6497
6787
  }
@@ -6525,9 +6815,6 @@ var PanelPlacement = class {
6525
6815
  * its own right.
6526
6816
  */
6527
6817
  #restorePanelPosition(at2) {
6528
- if (!this.#launcherDraggable()) {
6529
- return;
6530
- }
6531
6818
  const rect = this.#host.element.getBoundingClientRect();
6532
6819
  const held = clampPanel(
6533
6820
  { left: at2.left, top: at2.top, right: at2.left + rect.width, bottom: at2.top + rect.height },
@@ -6553,6 +6840,9 @@ var PanelPlacement = class {
6553
6840
  * with it, the panel goes back where it was put; without it, the panel is
6554
6841
  * re-derived from the launcher and opens into whatever room the viewport has
6555
6842
  * now.
6843
+ *
6844
+ * Every caller is a gesture ending or a move being made, so where the two
6845
+ * stand now is what was stated. A restore never calls this.
6556
6846
  */
6557
6847
  #storeLauncherPosition() {
6558
6848
  const position = this.#launcherPos;
@@ -6560,34 +6850,140 @@ var PanelPlacement = class {
6560
6850
  return;
6561
6851
  }
6562
6852
  const panel = this.#panelPos;
6563
- this.#host.writePreference(
6564
- LAUNCHER_KEY,
6565
- JSON.stringify(panel === null ? position : { ...position, panel })
6566
- );
6853
+ this.#writeCornerSpot(panel === null ? position : { ...position, panel });
6854
+ }
6855
+ /** Make `spot` the stated one, and remember it. */
6856
+ #writeCornerSpot(spot) {
6857
+ this.#cornerSpot = spot;
6858
+ this.#host.writePreference(LAUNCHER_KEY, JSON.stringify(spot));
6567
6859
  }
6568
6860
  /**
6569
6861
  * Re-apply the dragged position against the current viewport: on connect,
6570
6862
  * whenever the window resizes, and before an expand. The viewport that
6571
6863
  * stored a position may since have shrunk, and a launcher past the edge is
6572
6864
  * unreachable -- it is the only way back to a collapsed conversation.
6865
+ *
6866
+ * The one gate on whether a stored position is this element's to honour: the
6867
+ * gestures check their own before they ever reach the placing below.
6573
6868
  */
6574
6869
  restoreLauncherPosition() {
6575
- const stored = this.#readLauncherPosition();
6576
- const launcher = this.#launcherPos ?? stored;
6577
- if (launcher === null) {
6870
+ this.#placeFromRecord(true);
6871
+ this.#stampHeader();
6872
+ }
6873
+ /**
6874
+ * Re-apply it for a change in the panel's own layout, which needs no window or
6875
+ * reopen to happen: a line typed in the composer is one. A position waiting to
6876
+ * be handed back under data-launcher-drag="false" waits on, because a panel that leaves for the
6877
+ * host's corner as the reader starts typing is a jump with no cause they can see.
6878
+ */
6879
+ settleLauncherPosition() {
6880
+ this.#placeFromRecord(false);
6881
+ this.#stampHeader();
6882
+ }
6883
+ /** The placing behind {@link restoreLauncherPosition}. */
6884
+ #placeFromRecord(handBack) {
6885
+ if (this.#docked()) {
6886
+ this.#placeDocked();
6887
+ return;
6888
+ }
6889
+ if (!this.#surface().launcherDraggable) {
6578
6890
  return;
6579
6891
  }
6580
- const panel = this.#panelPos ?? stored?.panel ?? null;
6581
- if (panel !== null) {
6582
- this.#launcherPos = { left: launcher.left, top: launcher.top };
6583
- this.#restorePanelPosition(panel);
6892
+ const draggable = this.#launcherDraggable();
6893
+ if (!draggable && this.#launcherPos !== null) {
6894
+ if (!handBack) {
6895
+ return;
6896
+ }
6897
+ this.releaseLauncherPosition();
6898
+ }
6899
+ const spot = draggable ? this.#cornerSpot ?? this.#readLauncherPosition() : null;
6900
+ if (spot === null) {
6901
+ this.#placeUnstated();
6902
+ return;
6903
+ }
6904
+ if (this.#standFullBleed(spot)) {
6905
+ return;
6906
+ }
6907
+ if (spot.panel !== void 0) {
6908
+ this.#launcherPos = { left: spot.left, top: spot.top };
6909
+ this.#restorePanelPosition(spot.panel);
6584
6910
  return;
6585
6911
  }
6586
6912
  const box = this.#launcherBox();
6587
6913
  this.#applyLauncherPlacement(
6588
- clampLauncher({ ...box, left: launcher.left, top: launcher.top }, this.#viewport())
6914
+ clampLauncher({ ...box, left: spot.left, top: spot.top }, this.#viewport())
6589
6915
  );
6590
6916
  }
6917
+ /**
6918
+ * Leave a corner panel full-bleed if it has become the screen, and say whether
6919
+ * it has.
6920
+ *
6921
+ * The inline inset comes off, because the small-viewport block reads the
6922
+ * host's inset before its own: a spot written there holds the panel off the
6923
+ * edges it should fill, and past them by the same amount, since its size is
6924
+ * the screen's. What the panel covers is read before that, from the size
6925
+ * alone, which no inset changes.
6926
+ *
6927
+ * The launcher stays a place of its own. It is positioned from the host box,
6928
+ * so it is measured again from the box that has just moved, and it is held on
6929
+ * the screen, because it is the way back from a collapsed conversation. The
6930
+ * spot itself is untouched, for the next time there is room for it.
6931
+ *
6932
+ * Above the breakpoint nothing in the stylesheet takes the panel to the edges.
6933
+ * A host's own size makes it the screen while the resting inset still spends a
6934
+ * gutter on one corner, which hangs the box off the opposite one, so there it
6935
+ * stands at the start of the room the host left free and keeps that inset. A
6936
+ * box the placement already holds on the screen has no inset to keep, which is
6937
+ * every phone, and a host's rule that placed the box itself.
6938
+ */
6939
+ #standFullBleed(spot) {
6940
+ const element = this.#host.element;
6941
+ const viewport = this.#viewport();
6942
+ if (!coversViewport(element.getBoundingClientRect(), viewport)) {
6943
+ return false;
6944
+ }
6945
+ element.style.removeProperty("--ag-ui-inset");
6946
+ const rest = element.getBoundingClientRect();
6947
+ const hanging = hangsOff(rest, viewport, this.#screen());
6948
+ const box = hanging ? atStartOf(viewport, rest) : rest;
6949
+ const launcher = { ...this.#launcherBox(), left: spot.left, top: spot.top };
6950
+ this.#placePanelAndLauncher(box, { ...launcher, ...clampLauncher(launcher, viewport) });
6951
+ if (!hanging) {
6952
+ element.style.removeProperty("--ag-ui-inset");
6953
+ }
6954
+ return true;
6955
+ }
6956
+ /**
6957
+ * Place a corner panel that has no spot, the half of {@link #standFullBleed}
6958
+ * with nothing stated to keep and no launcher of its own to measure.
6959
+ *
6960
+ * Only an inset this element wrote comes off or goes back. One the host wrote
6961
+ * inline is a token a host may state on a phone as anywhere else, and with no
6962
+ * spot it never came from here.
6963
+ */
6964
+ #placeUnstated() {
6965
+ const element = this.#host.element;
6966
+ const inline = element.style.getPropertyValue("--ag-ui-inset");
6967
+ const viewport = this.#viewport();
6968
+ if (!coversViewport(element.getBoundingClientRect(), viewport)) {
6969
+ if (inline === "" || inline === this.#fillInset) {
6970
+ this.#restoreProperty("--ag-ui-inset", this.#gripInset ?? "");
6971
+ this.syncResizeAnchor();
6972
+ }
6973
+ this.#fillInset = null;
6974
+ return;
6975
+ }
6976
+ if (inline === this.#gripInset || inline === this.#fillInset) {
6977
+ element.style.removeProperty("--ag-ui-inset");
6978
+ }
6979
+ const rest = element.getBoundingClientRect();
6980
+ const screen = this.#screen();
6981
+ if (element.style.getPropertyValue("--ag-ui-inset") === "" && hangsOff(rest, viewport, screen)) {
6982
+ this.#fillInset = insetFrom({ x: "left", y: "top" }, atStartOf(viewport, rest), screen);
6983
+ element.style.setProperty("--ag-ui-inset", this.#fillInset);
6984
+ this.syncResizeAnchor();
6985
+ }
6986
+ }
6591
6987
  /** The persisted position for this instance, or null. */
6592
6988
  #readLauncherPosition() {
6593
6989
  const raw = this.#host.readPreference(LAUNCHER_KEY);
@@ -6615,12 +7011,22 @@ var PanelPlacement = class {
6615
7011
  * the switch inline and pins a sidebar to wherever the floating launcher was.
6616
7012
  */
6617
7013
  releaseLauncherPosition() {
7014
+ if (this.#dockPos !== null && !(this.#docked() && this.#panelMovable())) {
7015
+ this.#dockPos = null;
7016
+ this.#host.element.style.removeProperty("--ag-ui-inset");
7017
+ }
6618
7018
  if (this.#launcherDraggable()) {
6619
7019
  return;
6620
7020
  }
7021
+ this.#forgetCornerPosition();
7022
+ }
7023
+ /** Take everything off that placed a corner panel, leaving its record alone. */
7024
+ #forgetCornerPosition() {
6621
7025
  this.#launcherPos = null;
6622
7026
  this.#panelPos = null;
6623
7027
  this.#expandCorner = null;
7028
+ this.#gripInset = null;
7029
+ this.#fillInset = null;
6624
7030
  this.#host.element.style.removeProperty("--ag-ui-inset");
6625
7031
  this.#host.element.style.removeProperty("--ag-ui-launcher-inset");
6626
7032
  this.#host.element.removeAttribute("data-expand-corner");
@@ -6641,7 +7047,7 @@ var PanelPlacement = class {
6641
7047
  */
6642
7048
  #applyResize(grip, box) {
6643
7049
  box = this.#withinViewport(box);
6644
- if (this.#docked()) {
7050
+ if (this.#docked() && this.#dockPos === null) {
6645
7051
  const now = this.#host.element.getBoundingClientRect();
6646
7052
  box = {
6647
7053
  left: grip.x === this.#anchor.x ? now.left : box.left,
@@ -6653,28 +7059,40 @@ var PanelPlacement = class {
6653
7059
  return box;
6654
7060
  }
6655
7061
  this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
6656
- if (grip.x !== this.#anchor.x && grip.y !== this.#anchor.y) {
6657
- return box;
6658
- }
6659
- const anchor = this.#anchor;
6660
- this.#host.element.style.setProperty("--ag-ui-inset", insetFrom(anchor, box, this.#screen()));
6661
- if (this.#launcherPos !== null) {
6662
- const size = this.#host.launcher.offsetWidth;
6663
- this.#launcherPos = {
6664
- left: anchor.x === "left" ? box.left : box.right - size,
6665
- top: anchor.y === "top" ? box.top : box.bottom - size
6666
- };
7062
+ if (this.#docked()) {
7063
+ this.#dockPos = { left: box.left, top: box.top };
7064
+ }
7065
+ if (grip.x === this.#anchor.x || grip.y === this.#anchor.y) {
7066
+ const anchor = this.#anchor;
7067
+ const inset2 = insetFrom(anchor, box, this.#screen());
7068
+ this.#host.element.style.setProperty("--ag-ui-inset", inset2);
7069
+ this.#gripInset = this.#launcherPos === null && !this.#docked() ? inset2 : null;
7070
+ if (this.#launcherPos !== null) {
7071
+ const size = this.#host.launcher.offsetWidth;
7072
+ this.#launcherPos = {
7073
+ left: anchor.x === "left" ? box.left : box.right - size,
7074
+ top: anchor.y === "top" ? box.top : box.bottom - size
7075
+ };
7076
+ }
6667
7077
  }
6668
- if (this.#panelPos !== null) {
6669
- this.#panelPos = { left: box.left, top: box.top };
7078
+ if (this.#panelPos !== null && !this.#docked()) {
7079
+ const drawn = this.#host.element.getBoundingClientRect();
7080
+ this.#panelPos = { left: drawn.left, top: drawn.top };
6670
7081
  }
6671
7082
  return box;
6672
7083
  }
6673
7084
  /** Finish a resize: keep the box, remember it, and re-read the pinned edges. */
6674
7085
  #commitResize(grip, box) {
6675
7086
  const held = this.#applyResize(grip, box);
7087
+ if (this.#host.element.style.getPropertyValue("--ag-ui-inset") === this.#fillInset) {
7088
+ this.#gripInset = this.#fillInset;
7089
+ this.#fillInset = null;
7090
+ }
6676
7091
  this.#persistSize({ width: held.right - held.left, height: held.bottom - held.top });
6677
7092
  this.#storeLauncherPosition();
7093
+ if (this.#dockPos !== null) {
7094
+ this.#storeDockPosition(this.#dockPos);
7095
+ }
6678
7096
  this.syncResizeAnchor();
6679
7097
  }
6680
7098
  /**
@@ -8914,7 +9332,11 @@ var DOCK_STYLES = `
8914
9332
  The cap follows the token block's rule: what the host left free less the
8915
9333
  one gutter the anchored edge spends, so a panel grown to the cap runs from
8916
9334
  just above the bar to the far edge. A host therefore needs no inset to keep
8917
- the panel off its bar; --ag-ui-inset still wins when set.
9335
+ the panel off its bar; --ag-ui-inset still wins when set. A panel the user
9336
+ dragged off its dock is written there inline, which is how the dragged spot
9337
+ outranks every rule below, the anchor gap included; the element holds that
9338
+ spot above the bar or the anchor itself, and drops it on a phone, where the
9339
+ small-viewport block reads the same property first.
8918
9340
 
8919
9341
  These blocks restate tokens and nothing else, and sit ahead of the phone
8920
9342
  block at no more weight than it has, so on a phone the panel is the screen
@@ -9891,7 +10313,8 @@ ${GROUPED} .tool-group[data-open="false"] .tool-group-list {
9891
10313
 
9892
10314
  /* A card inside the group is one line of the list: icon, name, and its
9893
10315
  Details control at the end, with the arguments and result opening under
9894
- the whole row. */
10316
+ the whole row. Its ink is the second ink in every theme, unless the host
10317
+ set the tool token, which then reaches the grouped calls too. */
9895
10318
  ${GROUPED} .tool-group .tool-call {
9896
10319
  max-width: 100%;
9897
10320
  align-self: stretch;
@@ -9902,7 +10325,7 @@ ${GROUPED} .tool-group .tool-call {
9902
10325
  border: 0;
9903
10326
  border-radius: 0;
9904
10327
  background: transparent;
9905
- color: var(--_ink-2);
10328
+ color: var(--_tool-fg-set, var(--_ink-2));
9906
10329
  font-family: inherit;
9907
10330
  font-size: var(--_text-sm);
9908
10331
  }
@@ -10517,6 +10940,15 @@ var FRAME_STYLES = `
10517
10940
  touch-action: auto;
10518
10941
  }
10519
10942
 
10943
+ /* A panel that is the screen has nowhere to go, so its header refuses the drag
10944
+ and must not offer it. No selector can see that a host has sized the panel to
10945
+ the window, so the placement stamps this from the measurement that refuses the
10946
+ drag. :host is carried for the specificity, not the scope, as below. Before the
10947
+ dragging rule, so a drag the header did take keeps its grabbing cursor. */
10948
+ :host .header[data-fills-screen] {
10949
+ cursor: auto;
10950
+ }
10951
+
10520
10952
  /* :host is carried for the specificity, not the scope: the placement rules
10521
10953
  above are a host selector plus a class plus an attribute, and a bare
10522
10954
  .header[data-dragging] loses to them -- silently, because the drag still
@@ -10566,6 +10998,14 @@ var FRAME_STYLES = `
10566
10998
  flex: none;
10567
10999
  }
10568
11000
 
11001
+ /* Under every theme: once the host sets the header text, the header behind the
11002
+ buttons is the host's colour, so their rest opacity goes. The frame's 0.85
11003
+ lets the header through the glyph, which took white on a mid-blue header from
11004
+ 5.1:1 to 4.2:1. With the header text set, the opacity below reads a colour,
11005
+ which no opacity can be, so it is invalid at computed-value time and falls to
11006
+ the initial 1; unset, it reads the fallback. The glyph is each theme's own
11007
+ business: this inherits the header text, and a theme that draws the buttons
11008
+ in another ink reads the same switch to yield. */
10569
11009
  .header-btn {
10570
11010
  display: inline-flex;
10571
11011
  flex: 0 0 auto;
@@ -10582,12 +11022,15 @@ var FRAME_STYLES = `
10582
11022
  padding: 0;
10583
11023
  border-radius: 6px;
10584
11024
  cursor: pointer;
10585
- opacity: 0.85;
11025
+ opacity: var(--_header-fg-set, 0.85);
10586
11026
  }
10587
11027
 
11028
+ /* The square is the host's own when it set one; otherwise it is mixed from the
11029
+ header text when the host set that, so it reads on any header the host
11030
+ painted; otherwise it is the theme's own. See the alias in the token defaults. */
10588
11031
  .header-btn:hover {
10589
11032
  opacity: 1;
10590
- background: rgba(255, 255, 255, 0.18);
11033
+ background: var(--_header-btn-hover);
10591
11034
  }
10592
11035
  `;
10593
11036
 
@@ -10662,6 +11105,24 @@ var TOKEN_STYLES = `
10662
11105
  --_tool-fg: var(--ag-ui-tool-fg, var(--_ink-2));
10663
11106
  --_header-bg: var(--ag-ui-header-bg, var(--_bg));
10664
11107
  --_header-fg: var(--ag-ui-header-fg, var(--_fg));
11108
+ /* Switches: a public token read with no fallback, named for it with -set.
11109
+ With the token unset the switch computes to the guaranteed-invalid value,
11110
+ so a rule that reads it with a fallback gets the fallback; that is how a
11111
+ rule tells a token the host set from the theme's default for it. Every
11112
+ theme's header buttons follow the header text only once a host sets it,
11113
+ and a step group's calls follow the tool token only once a host sets it
11114
+ (classic defaults --_tool-fg to the accent, which a group never drew). */
11115
+ --_header-fg-set: var(--ag-ui-header-fg);
11116
+ --_tool-fg-set: var(--ag-ui-tool-fg);
11117
+ /* The header buttons' hover square, by default, once the header text is
11118
+ set. Here it is the text at about a fifth, which every browser can mix;
11119
+ where relative colour syntax is supported (see the block at the end) it
11120
+ is the text's opposite instead. It reads the switch, so it is unset with
11121
+ it, and the square below falls to the theme's own: the panel's hover shade
11122
+ here, restated by each theme that draws another, so one rule in the frame
11123
+ paints them all. */
11124
+ --_header-btn-hover-auto: color-mix(in srgb, var(--_header-fg-set) 18%, transparent);
11125
+ --_header-btn-hover: var(--ag-ui-header-btn-hover, var(--_header-btn-hover-auto, var(--_hover)));
10665
11126
  --_border: var(--ag-ui-border, var(--_line));
10666
11127
  --_radius: var(--ag-ui-radius, 12px);
10667
11128
  /* The panel's own frame, which is the one corner a placement can put against
@@ -11034,6 +11495,19 @@ var TOKEN_STYLES = `
11034
11495
  --_on-accent: var(--ag-ui-on-accent, #ffffff);
11035
11496
  }
11036
11497
  }
11498
+
11499
+ /* The header buttons' default hover square as the header text's opposite,
11500
+ so white text gets a darker square and dark text a lighter one. A square
11501
+ mixed from the text itself moves the ground toward the glyph: white on a
11502
+ mid-blue header fell from 5.1:1 to 3.75:1 under the pointer. Moving away
11503
+ from it can only raise what the host's own pair reads. Gated on this
11504
+ exact expression, so a browser that cannot compute it keeps the square
11505
+ mixed from the text rather than losing the square altogether. */
11506
+ @supports (color: rgb(from red calc(255 - r) calc(255 - g) calc(255 - b))) {
11507
+ :host {
11508
+ --_header-btn-hover-auto: color-mix(in srgb, rgb(from var(--_header-fg-set) calc(255 - r) calc(255 - g) calc(255 - b)) 18%, transparent);
11509
+ }
11510
+ }
11037
11511
  `;
11038
11512
 
11039
11513
  // src/ui/themes/classic_styles.ts
@@ -11087,6 +11561,7 @@ var CLASSIC_TOKENS = `
11087
11561
  --_tool-fg: var(--ag-ui-tool-fg, var(--_accent));
11088
11562
  --_header-bg: var(--ag-ui-header-bg, light-dark(var(--_accent), #1f1f30));
11089
11563
  --_header-fg: var(--ag-ui-header-fg, light-dark(#ffffff, #e8e8f2));
11564
+ --_header-btn-hover: var(--ag-ui-header-btn-hover, var(--_header-btn-hover-auto, rgba(255, 255, 255, 0.18)));
11090
11565
  --_border: var(--ag-ui-border, light-dark(#e2e2ec, #33334a));
11091
11566
  --_muted: var(--ag-ui-muted, light-dark(#6b7280, #9aa0b4));
11092
11567
  --_success: var(--ag-ui-success, #15803d);
@@ -11173,14 +11648,15 @@ var LEDGER_STYLES = `
11173
11648
  vertical-align: 1px;
11174
11649
  }
11175
11650
 
11651
+ /* Both inks yield once the host sets the header text, since the header behind
11652
+ them is then the host's colour (see the switches in the token defaults). */
11176
11653
  :host([theme~="ledger"]) .header-btn {
11177
- color: var(--_ink-3);
11654
+ color: var(--_header-fg-set, var(--_ink-3));
11178
11655
  border-radius: 3px;
11179
11656
  }
11180
11657
 
11181
11658
  :host([theme~="ledger"]) .header-btn:hover {
11182
- background: var(--_hover);
11183
- color: var(--_fg);
11659
+ color: var(--_header-fg-set, var(--_fg));
11184
11660
  }
11185
11661
 
11186
11662
  :host([theme~="ledger"]) .message--user {
@@ -11330,6 +11806,7 @@ var LEDGER_TOKENS = `
11330
11806
  --_msg-radius: var(--ag-ui-msg-radius, 0px);
11331
11807
  --_composer-radius: var(--ag-ui-composer-radius, 4px);
11332
11808
  --_user-bg: var(--ag-ui-user-bg, transparent);
11809
+ --_header-btn-hover: var(--ag-ui-header-btn-hover, var(--_header-btn-hover-auto, var(--_hover)));
11333
11810
  --_frame-shadow: var(--ag-ui-frame-shadow, 0 0 0 1px var(--_line-strong), 0 20px 40px -28px rgb(0 0 0 / 0.45));
11334
11811
  --_launcher-bg: var(--ag-ui-launcher-bg, var(--_fg));
11335
11812
  --_launcher-fg: var(--ag-ui-launcher-fg, var(--_bg));
@@ -11448,14 +11925,17 @@ ${THEME_HOST.quiet} .header-title {
11448
11925
  letter-spacing: -0.01em;
11449
11926
  }
11450
11927
 
11928
+ /* The buttons sit back in the third ink on the panel. Once the host sets the
11929
+ header text they draw in it instead, since the header behind them is then
11930
+ the host's colour (see the switches in the token defaults). */
11451
11931
  ${THEME_HOST.quiet} .header-btn {
11452
- color: var(--_ink-3);
11932
+ color: var(--_header-fg-set, var(--_ink-3));
11453
11933
  border-radius: 8px;
11454
11934
  }
11455
11935
 
11936
+ /* The glyph takes the header text, which is the full ink when that is unset. */
11456
11937
  ${THEME_HOST.quiet} .header-btn:hover {
11457
- color: var(--_fg);
11458
- background: var(--_hover);
11938
+ color: var(--_header-fg);
11459
11939
  }
11460
11940
 
11461
11941
  ${THEME_HOST.quiet} .message--user {
@@ -11598,14 +12078,15 @@ var SOFT_STYLES = `
11598
12078
  background: linear-gradient(140deg, color-mix(in oklch, var(--_accent) 55%, var(--_bg)), var(--_accent));
11599
12079
  }
11600
12080
 
12081
+ /* Both inks yield once the host sets the header text, since the header behind
12082
+ them is then the host's colour (see the switches in the token defaults). */
11601
12083
  :host([theme~="soft"]) .header-btn {
11602
- color: var(--_ink-3);
12084
+ color: var(--_header-fg-set, var(--_ink-3));
11603
12085
  border-radius: 10px;
11604
12086
  }
11605
12087
 
11606
12088
  :host([theme~="soft"]) .header-btn:hover {
11607
- background: var(--_tint-2);
11608
- color: var(--_accent-ink);
12089
+ color: var(--_header-fg-set, var(--_accent-ink));
11609
12090
  }
11610
12091
 
11611
12092
  :host([theme~="soft"]) .messages {
@@ -11726,6 +12207,7 @@ var SOFT_TOKENS = `
11726
12207
  --_tint: var(--ag-ui-tint, color-mix(in oklch, var(--_accent) 5%, var(--_bg)));
11727
12208
  --_tint-2: var(--ag-ui-tint-2, color-mix(in oklch, var(--_accent) 13%, var(--_bg)));
11728
12209
  --_header-bg: var(--ag-ui-header-bg, var(--_tint));
12210
+ --_header-btn-hover: var(--ag-ui-header-btn-hover, var(--_header-btn-hover-auto, var(--_tint-2)));
11729
12211
  --_user-bg: var(--ag-ui-user-bg, var(--_tint-2));
11730
12212
  --_border: var(--ag-ui-border, color-mix(in oklch, var(--_accent) 14%, var(--_bg)));
11731
12213
  --_radius: var(--ag-ui-radius, 16px);
@@ -19612,13 +20094,26 @@ var AgUiChat = class extends HTMLElement {
19612
20094
  * a field so `removeEventListener` on disconnect gets the same reference.
19613
20095
  */
19614
20096
  #onViewportResize = () => {
19615
- this.#placement.publishVisualViewport();
19616
- this.#syncDock();
19617
- if (this.#placement.dragging()) {
19618
- return;
20097
+ if (this.#readyToPlace()) {
20098
+ this.#placement.restoreLauncherPosition();
20099
+ }
20100
+ };
20101
+ /**
20102
+ * The same for a change only the panel's own layout shows: a host's size, a
20103
+ * viewport inset token, a line typed in the composer. Not a moment to hand a
20104
+ * position back, so a panel under data-launcher-drag="false" stays put.
20105
+ */
20106
+ #onLayoutChange = () => {
20107
+ if (this.#readyToPlace()) {
20108
+ this.#placement.settleLauncherPosition();
19619
20109
  }
19620
- this.#placement.restoreLauncherPosition();
19621
20110
  };
20111
+ /** Bring the viewport's readings up to date, and say whether placing may follow. */
20112
+ #readyToPlace() {
20113
+ this.#placement.publishVisualViewport();
20114
+ this.#syncDock();
20115
+ return !this.#placement.dragging();
20116
+ }
19622
20117
  /** Mic button mount point (input row); the control mounts on connect when enabled. */
19623
20118
  #voiceSlot;
19624
20119
  /** The composer's mic, mounted on connect when transcription is available. */
@@ -20411,6 +20906,12 @@ var AgUiChat = class extends HTMLElement {
20411
20906
  const dockObserver = new ResizeObserver(() => this.#syncDock());
20412
20907
  dockObserver.observe(this.#dockBar);
20413
20908
  this.#connection.signal.addEventListener("abort", () => dockObserver.disconnect());
20909
+ const layoutObserver = new ResizeObserver(this.#onLayoutChange);
20910
+ layoutObserver.observe(this.#messages);
20911
+ this.#connection.signal.addEventListener("abort", () => layoutObserver.disconnect());
20912
+ this.#placement.probe.addEventListener("transitionrun", this.#onLayoutChange, {
20913
+ signal: this.#connection.signal
20914
+ });
20414
20915
  this.#syncDock();
20415
20916
  if (this.#surface().collapseTo === "none") {
20416
20917
  this.dispatchEvent(
@@ -20800,7 +21301,9 @@ var AgUiChat = class extends HTMLElement {
20800
21301
  * `movable` folds two separate reasons into the one answer a caller needs:
20801
21302
  * a placement that places itself owns its position, and a panel that fills
20802
21303
  * the screen has nowhere to go. Reporting them apart would make every caller
20803
- * re-derive the same conjunction.
21304
+ * re-derive the same conjunction. A corner panel docked under
21305
+ * `collapse-to="bar"` or `"none"` is movable on a wide viewport, like the
21306
+ * corner panel it is.
20804
21307
  */
20805
21308
  describeSurface() {
20806
21309
  return this.#placement.describeSurface();
@@ -20814,7 +21317,8 @@ var AgUiChat = class extends HTMLElement {
20814
21317
  * the corner it opens from is re-picked, and switching placement hands
20815
21318
  * everything back. What it must not do is claim a move it did not make --
20816
21319
  * hence the boolean, and hence {@link describeSurface} existing so a caller
20817
- * can ask first.
21320
+ * can ask first. A docked panel moves too, within the room above its bar or
21321
+ * anchor, and remembers the spot as a header drag would.
20818
21322
  */
20819
21323
  moveTo(corner, options = {}) {
20820
21324
  return this.#placement.moveTo(corner, options);
@@ -20822,6 +21326,9 @@ var AgUiChat = class extends HTMLElement {
20822
21326
  /**
20823
21327
  * Drop a size the user dragged, and its stored copy, so the placement's
20824
21328
  * default and the host's `--ag-ui-width` / `--ag-ui-height` apply again.
21329
+ * Under `collapse-to="bar"` or `"none"` it also returns a panel the user
21330
+ * dragged off its dock to the dock, and forgets the stored spot; a corner
21331
+ * panel keeps a dragged position.
20825
21332
  */
20826
21333
  resetSize() {
20827
21334
  this.#placement.resetSize();
@@ -21337,6 +21844,8 @@ var AgUiChat = class extends HTMLElement {
21337
21844
  const probe = this.#placement.probe;
21338
21845
  probe.className = "viewport-probe";
21339
21846
  probe.setAttribute("aria-hidden", "true");
21847
+ probe.style.borderRadius = "var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) var(--_viewport-inset-left)";
21848
+ probe.style.transition = "border-radius 1ms";
21340
21849
  this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#dockBar);
21341
21850
  }
21342
21851
  /**
@@ -21984,7 +22493,7 @@ function setControlValue(el2, value) {
21984
22493
  }
21985
22494
 
21986
22495
  // src/version.ts
21987
- var VERSION = "0.48.0";
22496
+ var VERSION = "0.50.0";
21988
22497
  export {
21989
22498
  ATTACHMENT_EVENT,
21990
22499
  AgUiChat,