@artooi/ag-ui-web-component 0.49.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.
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,46 @@ var PanelPlacement = class {
5987
6008
  }
5988
6009
  this.#dockPos = null;
5989
6010
  this.#storeDockPosition(null);
6011
+ } else {
6012
+ this.#carrySpotWithSize(sized);
5990
6013
  }
5991
6014
  this.syncResizeAnchor();
5992
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
+ }
5993
6051
  /**
5994
6052
  * A zero-sized box carrying the host's viewport insets as padding, so they
5995
6053
  * can be read back as used pixel lengths. See the `.viewport-probe` rule for
@@ -6014,6 +6072,32 @@ var PanelPlacement = class {
6014
6072
  * from the launcher would move the panel the user just placed.
6015
6073
  */
6016
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;
6017
6101
  /**
6018
6102
  * Where the user dragged a docked panel, in viewport coordinates, or null
6019
6103
  * while it stands on the bar or the anchor.
@@ -6042,6 +6126,10 @@ var PanelPlacement = class {
6042
6126
  * second while the panel is already being laid out on every one of them.
6043
6127
  */
6044
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;
6045
6133
  /** The eight grips, by name, so the keyboard-reachable one can be moved. */
6046
6134
  #resizeHandles = /* @__PURE__ */ new Map();
6047
6135
  /**
@@ -6076,10 +6164,12 @@ var PanelPlacement = class {
6076
6164
  *
6077
6165
  * Read from the stamp the drag helpers already set, rather than tracked
6078
6166
  * separately: one source of truth, and it clears on `pointercancel` as well
6079
- * 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.
6080
6170
  */
6081
6171
  dragging() {
6082
- 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;
6083
6173
  }
6084
6174
  /** The report behind `AgUiChat.describeSurface`, whose doc is the contract. */
6085
6175
  describeSurface() {
@@ -6092,11 +6182,12 @@ var PanelPlacement = class {
6092
6182
  collapsed: this.#host.collapsed(),
6093
6183
  collapsible: this.#host.collapsible(),
6094
6184
  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()),
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",
6100
6191
  fullBleed,
6101
6192
  box: {
6102
6193
  left: Math.round(box.left),
@@ -6163,6 +6254,13 @@ var PanelPlacement = class {
6163
6254
  * puts the box back and animates it out of the wrong side. Absent values are
6164
6255
  * captured as absent and removed on the way back, rather than written as
6165
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.
6166
6264
  */
6167
6265
  #captureGeometry() {
6168
6266
  const inset2 = this.#host.element.style.getPropertyValue("--ag-ui-inset");
@@ -6170,33 +6268,44 @@ var PanelPlacement = class {
6170
6268
  const corner = this.#host.element.getAttribute("data-expand-corner");
6171
6269
  const launcherPos = this.#launcherPos;
6172
6270
  const panelPos = this.#panelPos;
6173
- const dockPos = this.#dockPos;
6271
+ const dockPos = this.#dockPos ?? this.#readDockPosition();
6174
6272
  const expandCorner = this.#expandCorner;
6273
+ const cornerSpot = this.#cornerSpot ?? this.#readLauncherPosition();
6274
+ const owner = this.#positionOwner();
6175
6275
  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
6276
  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) {
6277
+ this.#storeDockPosition(dockPos);
6278
+ if (cornerSpot === null) {
6279
+ this.#cornerSpot = null;
6193
6280
  this.#host.clearPreference(LAUNCHER_KEY);
6194
6281
  } else {
6195
- this.#storeLauncherPosition();
6282
+ this.#writeCornerSpot(cornerSpot);
6283
+ }
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;
6295
+ } else {
6296
+ this.#forgetCornerPosition();
6196
6297
  }
6298
+ this.restoreLauncherPosition();
6197
6299
  this.syncResizeAnchor();
6198
6300
  };
6199
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
+ }
6200
6309
  /**
6201
6310
  * Which axes the current placement allows.
6202
6311
  *
@@ -6257,6 +6366,18 @@ var PanelPlacement = class {
6257
6366
  this.#anchor = anchor;
6258
6367
  this.#host.element.setAttribute("data-resize-anchor", `${anchor.y}-${anchor.x}`);
6259
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());
6260
6381
  }
6261
6382
  /** Put a custom property back to a previous value, or remove it if there was none. */
6262
6383
  #restoreProperty(name, value) {
@@ -6427,15 +6548,16 @@ var PanelPlacement = class {
6427
6548
  return this.#host.element.getAttribute("data-launcher-drag") !== "false" && this.#surface().panelMovable;
6428
6549
  }
6429
6550
  /**
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.
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.
6433
6554
  */
6434
6555
  #canDragPanel() {
6435
- if (!this.#panelMovable()) {
6436
- return false;
6437
- }
6438
- return !this.#docked() || !coversViewport(this.#host.element.getBoundingClientRect(), this.#viewport());
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());
6439
6561
  }
6440
6562
  /**
6441
6563
  * Whether this placement lets the launcher be dragged, read per interaction
@@ -6568,9 +6690,6 @@ var PanelPlacement = class {
6568
6690
  * back rather than leaving a stale inline one behind.
6569
6691
  */
6570
6692
  #applyLauncherPlacement(at2) {
6571
- if (!this.#launcherDraggable()) {
6572
- return;
6573
- }
6574
6693
  this.#launcherPos = at2;
6575
6694
  this.#panelPos = null;
6576
6695
  const panel = this.#host.element.getBoundingClientRect();
@@ -6591,6 +6710,7 @@ var PanelPlacement = class {
6591
6710
  }
6592
6711
  /** Move the launcher live during a drag, without persisting. */
6593
6712
  #moveLauncher(left, top) {
6713
+ this.#cornerSpot = { left, top };
6594
6714
  this.#applyLauncherPlacement({ left, top });
6595
6715
  }
6596
6716
  /** Move the launcher and remember where, per tab, like the dragged size. */
@@ -6695,9 +6815,6 @@ var PanelPlacement = class {
6695
6815
  * its own right.
6696
6816
  */
6697
6817
  #restorePanelPosition(at2) {
6698
- if (!this.#launcherDraggable()) {
6699
- return;
6700
- }
6701
6818
  const rect = this.#host.element.getBoundingClientRect();
6702
6819
  const held = clampPanel(
6703
6820
  { left: at2.left, top: at2.top, right: at2.left + rect.width, bottom: at2.top + rect.height },
@@ -6723,6 +6840,9 @@ var PanelPlacement = class {
6723
6840
  * with it, the panel goes back where it was put; without it, the panel is
6724
6841
  * re-derived from the launcher and opens into whatever room the viewport has
6725
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.
6726
6846
  */
6727
6847
  #storeLauncherPosition() {
6728
6848
  const position = this.#launcherPos;
@@ -6730,38 +6850,140 @@ var PanelPlacement = class {
6730
6850
  return;
6731
6851
  }
6732
6852
  const panel = this.#panelPos;
6733
- this.#host.writePreference(
6734
- LAUNCHER_KEY,
6735
- JSON.stringify(panel === null ? position : { ...position, panel })
6736
- );
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));
6737
6859
  }
6738
6860
  /**
6739
6861
  * Re-apply the dragged position against the current viewport: on connect,
6740
6862
  * whenever the window resizes, and before an expand. The viewport that
6741
6863
  * stored a position may since have shrunk, and a launcher past the edge is
6742
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.
6743
6868
  */
6744
6869
  restoreLauncherPosition() {
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) {
6745
6885
  if (this.#docked()) {
6746
6886
  this.#placeDocked();
6747
6887
  return;
6748
6888
  }
6749
- const stored = this.#readLauncherPosition();
6750
- const launcher = this.#launcherPos ?? stored;
6751
- if (launcher === null) {
6889
+ if (!this.#surface().launcherDraggable) {
6890
+ return;
6891
+ }
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();
6752
6902
  return;
6753
6903
  }
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);
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);
6758
6910
  return;
6759
6911
  }
6760
6912
  const box = this.#launcherBox();
6761
6913
  this.#applyLauncherPlacement(
6762
- clampLauncher({ ...box, left: launcher.left, top: launcher.top }, this.#viewport())
6914
+ clampLauncher({ ...box, left: spot.left, top: spot.top }, this.#viewport())
6763
6915
  );
6764
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
+ }
6765
6987
  /** The persisted position for this instance, or null. */
6766
6988
  #readLauncherPosition() {
6767
6989
  const raw = this.#host.readPreference(LAUNCHER_KEY);
@@ -6796,9 +7018,15 @@ var PanelPlacement = class {
6796
7018
  if (this.#launcherDraggable()) {
6797
7019
  return;
6798
7020
  }
7021
+ this.#forgetCornerPosition();
7022
+ }
7023
+ /** Take everything off that placed a corner panel, leaving its record alone. */
7024
+ #forgetCornerPosition() {
6799
7025
  this.#launcherPos = null;
6800
7026
  this.#panelPos = null;
6801
7027
  this.#expandCorner = null;
7028
+ this.#gripInset = null;
7029
+ this.#fillInset = null;
6802
7030
  this.#host.element.style.removeProperty("--ag-ui-inset");
6803
7031
  this.#host.element.style.removeProperty("--ag-ui-launcher-inset");
6804
7032
  this.#host.element.removeAttribute("data-expand-corner");
@@ -6834,26 +7062,32 @@ var PanelPlacement = class {
6834
7062
  if (this.#docked()) {
6835
7063
  this.#dockPos = { left: box.left, top: box.top };
6836
7064
  }
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
- };
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
+ }
6848
7077
  }
6849
- if (this.#panelPos !== null) {
6850
- 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 };
6851
7081
  }
6852
7082
  return box;
6853
7083
  }
6854
7084
  /** Finish a resize: keep the box, remember it, and re-read the pinned edges. */
6855
7085
  #commitResize(grip, box) {
6856
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
+ }
6857
7091
  this.#persistSize({ width: held.right - held.left, height: held.bottom - held.top });
6858
7092
  this.#storeLauncherPosition();
6859
7093
  if (this.#dockPos !== null) {
@@ -10706,6 +10940,15 @@ var FRAME_STYLES = `
10706
10940
  touch-action: auto;
10707
10941
  }
10708
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
+
10709
10952
  /* :host is carried for the specificity, not the scope: the placement rules
10710
10953
  above are a host selector plus a class plus an attribute, and a bare
10711
10954
  .header[data-dragging] loses to them -- silently, because the drag still
@@ -10755,6 +10998,14 @@ var FRAME_STYLES = `
10755
10998
  flex: none;
10756
10999
  }
10757
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. */
10758
11009
  .header-btn {
10759
11010
  display: inline-flex;
10760
11011
  flex: 0 0 auto;
@@ -10771,12 +11022,15 @@ var FRAME_STYLES = `
10771
11022
  padding: 0;
10772
11023
  border-radius: 6px;
10773
11024
  cursor: pointer;
10774
- opacity: 0.85;
11025
+ opacity: var(--_header-fg-set, 0.85);
10775
11026
  }
10776
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. */
10777
11031
  .header-btn:hover {
10778
11032
  opacity: 1;
10779
- background: rgba(255, 255, 255, 0.18);
11033
+ background: var(--_header-btn-hover);
10780
11034
  }
10781
11035
  `;
10782
11036
 
@@ -10854,18 +11108,19 @@ var TOKEN_STYLES = `
10854
11108
  /* Switches: a public token read with no fallback, named for it with -set.
10855
11109
  With the token unset the switch computes to the guaranteed-invalid value,
10856
11110
  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). */
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). */
10862
11115
  --_header-fg-set: var(--ag-ui-header-fg);
10863
11116
  --_tool-fg-set: var(--ag-ui-tool-fg);
10864
11117
  /* The header buttons' hover square, by default, once the header text is
10865
11118
  set. Here it is the text at about a fifth, which every browser can mix;
10866
11119
  where relative colour syntax is supported (see the block at the end) it
10867
11120
  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. */
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. */
10869
11124
  --_header-btn-hover-auto: color-mix(in srgb, var(--_header-fg-set) 18%, transparent);
10870
11125
  --_header-btn-hover: var(--ag-ui-header-btn-hover, var(--_header-btn-hover-auto, var(--_hover)));
10871
11126
  --_border: var(--ag-ui-border, var(--_line));
@@ -11306,6 +11561,7 @@ var CLASSIC_TOKENS = `
11306
11561
  --_tool-fg: var(--ag-ui-tool-fg, var(--_accent));
11307
11562
  --_header-bg: var(--ag-ui-header-bg, light-dark(var(--_accent), #1f1f30));
11308
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)));
11309
11565
  --_border: var(--ag-ui-border, light-dark(#e2e2ec, #33334a));
11310
11566
  --_muted: var(--ag-ui-muted, light-dark(#6b7280, #9aa0b4));
11311
11567
  --_success: var(--ag-ui-success, #15803d);
@@ -11392,14 +11648,15 @@ var LEDGER_STYLES = `
11392
11648
  vertical-align: 1px;
11393
11649
  }
11394
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). */
11395
11653
  :host([theme~="ledger"]) .header-btn {
11396
- color: var(--_ink-3);
11654
+ color: var(--_header-fg-set, var(--_ink-3));
11397
11655
  border-radius: 3px;
11398
11656
  }
11399
11657
 
11400
11658
  :host([theme~="ledger"]) .header-btn:hover {
11401
- background: var(--_hover);
11402
- color: var(--_fg);
11659
+ color: var(--_header-fg-set, var(--_fg));
11403
11660
  }
11404
11661
 
11405
11662
  :host([theme~="ledger"]) .message--user {
@@ -11549,6 +11806,7 @@ var LEDGER_TOKENS = `
11549
11806
  --_msg-radius: var(--ag-ui-msg-radius, 0px);
11550
11807
  --_composer-radius: var(--ag-ui-composer-radius, 4px);
11551
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)));
11552
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));
11553
11811
  --_launcher-bg: var(--ag-ui-launcher-bg, var(--_fg));
11554
11812
  --_launcher-fg: var(--ag-ui-launcher-fg, var(--_bg));
@@ -11669,28 +11927,15 @@ ${THEME_HOST.quiet} .header-title {
11669
11927
 
11670
11928
  /* The buttons sit back in the third ink on the panel. Once the host sets the
11671
11929
  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. */
11930
+ the host's colour (see the switches in the token defaults). */
11678
11931
  ${THEME_HOST.quiet} .header-btn {
11679
11932
  color: var(--_header-fg-set, var(--_ink-3));
11680
- opacity: var(--_header-fg-set, 0.85);
11681
11933
  border-radius: 8px;
11682
11934
  }
11683
11935
 
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. */
11936
+ /* The glyph takes the header text, which is the full ink when that is unset. */
11690
11937
  ${THEME_HOST.quiet} .header-btn:hover {
11691
11938
  color: var(--_header-fg);
11692
- opacity: 1;
11693
- background: var(--_header-btn-hover);
11694
11939
  }
11695
11940
 
11696
11941
  ${THEME_HOST.quiet} .message--user {
@@ -11833,14 +12078,15 @@ var SOFT_STYLES = `
11833
12078
  background: linear-gradient(140deg, color-mix(in oklch, var(--_accent) 55%, var(--_bg)), var(--_accent));
11834
12079
  }
11835
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). */
11836
12083
  :host([theme~="soft"]) .header-btn {
11837
- color: var(--_ink-3);
12084
+ color: var(--_header-fg-set, var(--_ink-3));
11838
12085
  border-radius: 10px;
11839
12086
  }
11840
12087
 
11841
12088
  :host([theme~="soft"]) .header-btn:hover {
11842
- background: var(--_tint-2);
11843
- color: var(--_accent-ink);
12089
+ color: var(--_header-fg-set, var(--_accent-ink));
11844
12090
  }
11845
12091
 
11846
12092
  :host([theme~="soft"]) .messages {
@@ -11961,6 +12207,7 @@ var SOFT_TOKENS = `
11961
12207
  --_tint: var(--ag-ui-tint, color-mix(in oklch, var(--_accent) 5%, var(--_bg)));
11962
12208
  --_tint-2: var(--ag-ui-tint-2, color-mix(in oklch, var(--_accent) 13%, var(--_bg)));
11963
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)));
11964
12211
  --_user-bg: var(--ag-ui-user-bg, var(--_tint-2));
11965
12212
  --_border: var(--ag-ui-border, color-mix(in oklch, var(--_accent) 14%, var(--_bg)));
11966
12213
  --_radius: var(--ag-ui-radius, 16px);
@@ -19847,13 +20094,26 @@ var AgUiChat = class extends HTMLElement {
19847
20094
  * a field so `removeEventListener` on disconnect gets the same reference.
19848
20095
  */
19849
20096
  #onViewportResize = () => {
19850
- this.#placement.publishVisualViewport();
19851
- this.#syncDock();
19852
- if (this.#placement.dragging()) {
19853
- return;
20097
+ if (this.#readyToPlace()) {
20098
+ this.#placement.restoreLauncherPosition();
19854
20099
  }
19855
- this.#placement.restoreLauncherPosition();
19856
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();
20109
+ }
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
+ }
19857
20117
  /** Mic button mount point (input row); the control mounts on connect when enabled. */
19858
20118
  #voiceSlot;
19859
20119
  /** The composer's mic, mounted on connect when transcription is available. */
@@ -20646,6 +20906,12 @@ var AgUiChat = class extends HTMLElement {
20646
20906
  const dockObserver = new ResizeObserver(() => this.#syncDock());
20647
20907
  dockObserver.observe(this.#dockBar);
20648
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
+ });
20649
20915
  this.#syncDock();
20650
20916
  if (this.#surface().collapseTo === "none") {
20651
20917
  this.dispatchEvent(
@@ -21578,6 +21844,8 @@ var AgUiChat = class extends HTMLElement {
21578
21844
  const probe = this.#placement.probe;
21579
21845
  probe.className = "viewport-probe";
21580
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";
21581
21849
  this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#dockBar);
21582
21850
  }
21583
21851
  /**
@@ -22225,7 +22493,7 @@ function setControlValue(el2, value) {
22225
22493
  }
22226
22494
 
22227
22495
  // src/version.ts
22228
- var VERSION = "0.49.0";
22496
+ var VERSION = "0.50.0";
22229
22497
  export {
22230
22498
  ATTACHMENT_EVENT,
22231
22499
  AgUiChat,