@artooi/ag-ui-web-component 0.50.0 → 0.50.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"collapse_styles.d.ts","sourceRoot":"","sources":["../../../src/ui/placement/collapse_styles.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,eAAe,EAAE,MA2X7B,CAAC"}
1
+ {"version":3,"file":"collapse_styles.d.ts","sourceRoot":"","sources":["../../../src/ui/placement/collapse_styles.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,eAAe,EAAE,MAmY7B,CAAC"}
@@ -141,6 +141,8 @@ export declare class PanelPlacement {
141
141
  * reopen to happen: a line typed in the composer is one. A position waiting to
142
142
  * be handed back under data-launcher-drag="false" waits on, because a panel that leaves for the
143
143
  * host's corner as the reader starts typing is a jump with no cause they can see.
144
+ * It waits no longer than the panel stays on the screen: a host's size can stand
145
+ * it off an edge, where its header is out of reach until something else happens.
144
146
  */
145
147
  settleLauncherPosition(): void;
146
148
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"panel_placement.d.ts","sourceRoot":"","sources":["../../../src/ui/placement/panel_placement.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAsJlD;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC;IAClC,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC;IAClC,kEAAkE;IAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC;IACpC,iCAAiC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,SAAS,CAAC;IAClC,qDAAqD;IACrD,QAAQ,CAAC,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACzD,kDAAkD;IAClD,QAAQ,CAAC,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE,iDAAiD;IACjD,QAAQ,CAAC,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,uEAAuE;IACvE,QAAQ,CAAC,qBAAqB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CAC1E;AAED;;;;;;;GAOG;AACH,qBAAa,cAAc;;IAGzB,YAAY,IAAI,EAAE,aAAa,EAE9B;IAED;;;OAGG;IACH,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAQzC;IAED;;;;OAIG;IACH,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAS5C;IAED,mFAAmF;IACnF,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CA+BxC;IAED,sDAAsD;IACtD,WAAW,IAAI,IAAI,CAElB;IAED;;;;;;;;;OASG;IACH,SAAS,IAAI,IAAI,CAiBhB;IA2CD;;;;OAIG;IACH,QAAQ,CAAC,KAAK,iBAAiC;IA8H/C;;;;;;;;OAQG;IACH,QAAQ,IAAI,OAAO,CAOlB;IAED,+EAA+E;IAC/E,eAAe,IAAI,iBAAiB,CA8BnC;IAED,oEAAoE;IACpE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAmDjF;IAqJD,sEAAsE;IACtE,gBAAgB,IAAI,IAAI,CAavB;IAsDD;;;;;;OAMG;IACH,gBAAgB,IAAI,IAAI,CAQvB;IAiBD;;;;;;;;;OASG;IACH,QAAQ,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,GAAG,MAAM,CA8BzD;IA2LD;;;;;;;;;;;;OAYG;IACH,qBAAqB,IAAI,IAAI,CAgD5B;IAyQD;;;;;;;;OAQG;IACH,uBAAuB,IAAI,IAAI,CAG9B;IAED;;;;;OAKG;IACH,sBAAsB,IAAI,IAAI,CAG7B;IAsJD;;;;OAIG;IACH,uBAAuB,IAAI,IAAI,CAY9B;CAyJF"}
1
+ {"version":3,"file":"panel_placement.d.ts","sourceRoot":"","sources":["../../../src/ui/placement/panel_placement.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAsJlD;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC;IAClC,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC;IAClC,kEAAkE;IAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC;IACpC,iCAAiC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,SAAS,CAAC;IAClC,qDAAqD;IACrD,QAAQ,CAAC,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACzD,kDAAkD;IAClD,QAAQ,CAAC,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE,iDAAiD;IACjD,QAAQ,CAAC,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,uEAAuE;IACvE,QAAQ,CAAC,qBAAqB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CAC1E;AAED;;;;;;;GAOG;AACH,qBAAa,cAAc;;IAGzB,YAAY,IAAI,EAAE,aAAa,EAE9B;IAED;;;OAGG;IACH,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAQzC;IAED;;;;OAIG;IACH,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAS5C;IAED,mFAAmF;IACnF,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CA+BxC;IAED,sDAAsD;IACtD,WAAW,IAAI,IAAI,CAElB;IAED;;;;;;;;;OASG;IACH,SAAS,IAAI,IAAI,CAoBhB;IA2CD;;;;OAIG;IACH,QAAQ,CAAC,KAAK,iBAAiC;IAsI/C;;;;;;;;OAQG;IACH,QAAQ,IAAI,OAAO,CAOlB;IAED,+EAA+E;IAC/E,eAAe,IAAI,iBAAiB,CA8BnC;IAED,oEAAoE;IACpE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAmDjF;IA2JD,sEAAsE;IACtE,gBAAgB,IAAI,IAAI,CAavB;IA+ED;;;;;;OAMG;IACH,gBAAgB,IAAI,IAAI,CAQvB;IAiBD;;;;;;;;;OASG;IACH,QAAQ,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,GAAG,MAAM,CAkCzD;IA6PD;;;;;;;;;;;;OAYG;IACH,qBAAqB,IAAI,IAAI,CAgD5B;IAyQD;;;;;;;;OAQG;IACH,uBAAuB,IAAI,IAAI,CAG9B;IAED;;;;;;;OAOG;IACH,sBAAsB,IAAI,IAAI,CAG7B;IAgKD;;;;OAIG;IACH,uBAAuB,IAAI,IAAI,CAY9B;CAyJF"}
@@ -1 +1 @@
1
- {"version":3,"file":"resize_styles.d.ts","sourceRoot":"","sources":["../../../src/ui/placement/resize_styles.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,aAAa,EAAE,MAwL3B,CAAC"}
1
+ {"version":3,"file":"resize_styles.d.ts","sourceRoot":"","sources":["../../../src/ui/placement/resize_styles.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,aAAa,EAAE,MAiM3B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"transcript_styles.d.ts","sourceRoot":"","sources":["../../../src/ui/transcript/transcript_styles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,iBAAiB,EAAE,MAyI/B,CAAC"}
1
+ {"version":3,"file":"transcript_styles.d.ts","sourceRoot":"","sources":["../../../src/ui/transcript/transcript_styles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,iBAAiB,EAAE,MAsJ/B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artooi/ag-ui-web-component",
3
- "version": "0.50.0",
3
+ "version": "0.50.1",
4
4
  "description": "Framework-free <ag-ui-chat> Web Component over the AG-UI protocol. Drop-in chat sidebar with a pluggable client-side tool registry, DOM driver primitives, animations, and destructive-action confirmation modal.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -665,7 +665,27 @@ export class AgUiChat extends HTMLElement {
665
665
  // sent me looking -- that was an inset measured from the wrong box -- so it
666
666
  // is a guard against a fight that had not been observed rather than a fix
667
667
  // for one that had.
668
- return !this.#placement.dragging();
668
+ //
669
+ // Nor while this element has no box, which is what hiding it reports to the
670
+ // transcript's observer: placing measures a rect of nothing and writes an inset
671
+ // from it. Showing it reports again, so nothing waits on this.
672
+ return !this.#placement.dragging() && this.#hasBox();
673
+ }
674
+
675
+ /**
676
+ * Whether this element generates a box at all. Hidden on itself or on any ancestor
677
+ * it does not; a host collapsed to its launcher does, whatever its size, and that
678
+ * launcher still has to be placed, which is why this is not a test of the size.
679
+ */
680
+ #hasBox(): boolean {
681
+ return this.getClientRects().length > 0;
682
+ }
683
+
684
+ /** Restore for a moment that is not a viewport's, which showing this reports anyway. */
685
+ #restoreIfBoxed(): void {
686
+ if (this.#hasBox()) {
687
+ this.#placement.restoreLauncherPosition();
688
+ }
669
689
  }
670
690
 
671
691
  /** Mic button mount point (input row); the control mounts on connect when enabled. */
@@ -1567,7 +1587,7 @@ export class AgUiChat extends HTMLElement {
1567
1587
  requestAnimationFrame(() => {
1568
1588
  // Before the anchor, which the placement stamps itself once a dragged
1569
1589
  // position exists.
1570
- this.#placement.restoreLauncherPosition();
1590
+ this.#restoreIfBoxed();
1571
1591
  this.#placement.syncResizeAnchor();
1572
1592
  });
1573
1593
  // Resolve the string table before rendering any chrome (defaults are the
@@ -1924,7 +1944,7 @@ export class AgUiChat extends HTMLElement {
1924
1944
  // Re-decide which way to open before opening: the viewport may have
1925
1945
  // changed since the launcher was dropped, and this corner is what the
1926
1946
  // panel grows from.
1927
- this.#placement.restoreLauncherPosition();
1947
+ this.#restoreIfBoxed();
1928
1948
  }
1929
1949
  if (collapsed) {
1930
1950
  this.setAttribute("collapsed", "");
@@ -2036,7 +2056,12 @@ export class AgUiChat extends HTMLElement {
2036
2056
  if (!this.isConnected) {
2037
2057
  return;
2038
2058
  }
2039
- const height = this.#placement.syncDock(this.#dockBar, this.#resolveAnchor());
2059
+ // With no box there is no bar drawn, which the host still hears of, and nothing to
2060
+ // measure: syncing would place a dragged panel from a rect of nothing. Showing this
2061
+ // element resizes the bar or the transcript, and their observers sync it then.
2062
+ const height = this.#hasBox()
2063
+ ? this.#placement.syncDock(this.#dockBar, this.#resolveAnchor())
2064
+ : 0;
2040
2065
  if (height === this.#barHeight) {
2041
2066
  return;
2042
2067
  }
@@ -300,6 +300,14 @@ export const COLLAPSE_STYLES: string = `
300
300
  display: none;
301
301
  }
302
302
 
303
+ /* A panel a host sized to the whole window stands on the strip, whose fixed box would
304
+ draw over the composer's foot. So it goes as it does on a phone, by display, so the
305
+ bar measures 0 and the host's padding goes with it, and the header's minimise
306
+ control is the way back. Collapsed, the strip is the way back, so it stays. */
307
+ :host([data-collapsed-to="bar"][data-fills-window]:not([collapsed])) .dock-bar {
308
+ display: none;
309
+ }
310
+
303
311
  /* History from the bar: the panel's own drawer, moved beside the bar while
304
312
  the panel is away, drawn as a popover standing on the bar's end. There is
305
313
  no backdrop; a press outside the bar closes it, as Escape does.
@@ -288,6 +288,9 @@ export class PanelPlacement {
288
288
  }
289
289
  this.#dockPos = null;
290
290
  this.#storeDockPosition(null);
291
+ // A size the host holds does not change here, so no observer is coming to
292
+ // stand a panel that fills the window back on the screen.
293
+ this.#placeDocked();
291
294
  } else {
292
295
  this.#carrySpotWithSize(sized);
293
296
  }
@@ -387,9 +390,10 @@ export class PanelPlacement {
387
390
  #gripInset: string | null = null;
388
391
 
389
392
  /**
390
- * The inset standing a panel with no spot at the start of a window a host's
391
- * size fills, or null. Told apart from a host's own for the same reason, and it
392
- * lasts only as long as that size does.
393
+ * The inset standing a panel at the start of a window a host's size fills, or
394
+ * null: a corner panel with no spot, or a docked one with or without one. Told
395
+ * apart from a host's own for the same reason, and it lasts only as long as that
396
+ * size does.
393
397
  */
394
398
  #fillInset: string | null = null;
395
399
 
@@ -411,6 +415,13 @@ export class PanelPlacement {
411
415
  * syncDock, which runs on every change either can make.
412
416
  */
413
417
  #dockTop = Number.POSITIVE_INFINITY;
418
+ /** The bar's height, as last measured: what a fill takes away when it stamps. */
419
+ #barHeight = 0;
420
+ /**
421
+ * What the usable box and the window measured when a fill took the bar away, and what
422
+ * the bar was. Null while no fill holds it. See {@link #fillRoom}.
423
+ */
424
+ #barTaken: { readonly room: number; readonly window: number; readonly bar: number } | null = null;
414
425
 
415
426
  /**
416
427
  * The corner the panel opens away from, once this element is placing itself.
@@ -490,7 +501,7 @@ export class PanelPlacement {
490
501
  describeSurface(): ChatSurfaceReport {
491
502
  const box = this.#host.element.getBoundingClientRect();
492
503
  const viewport = this.#viewport();
493
- const fullBleed = coversViewport(box, viewport);
504
+ const fullBleed = coversViewport(box, this.#fillRoom());
494
505
  return {
495
506
  placement: this.#host.element.getAttribute("placement"),
496
507
  collapseTo: this.#surface().collapseTo,
@@ -527,7 +538,7 @@ export class PanelPlacement {
527
538
  const restore = options.announce === true ? this.#captureGeometry() : null;
528
539
  const viewport = this.#viewport();
529
540
  const box = this.#host.element.getBoundingClientRect();
530
- if (coversViewport(box, viewport)) {
541
+ if (coversViewport(box, this.#fillRoom())) {
531
542
  return false;
532
543
  }
533
544
  const [edgeY, edgeX] = corner.split("-");
@@ -581,7 +592,9 @@ export class PanelPlacement {
581
592
  * corner is what the panel grows from, so restoring a position without it
582
593
  * puts the box back and animates it out of the wrong side. Absent values are
583
594
  * captured as absent and removed on the way back, rather than written as
584
- * empty strings that would outrank the placement.
595
+ * empty strings that would outrank the placement. And what the inset is, because
596
+ * one put back with no record of having come from a grip or a fill reads as a
597
+ * host's own, and a phone would leave it holding the panel off the screen.
585
598
  *
586
599
  * The surface may have changed by the time of the undo, so the two halves are
587
600
  * put back apart. Both records are, always, each to its own store, so the
@@ -598,6 +611,8 @@ export class PanelPlacement {
598
611
  const panelPos = this.#panelPos;
599
612
  const dockPos = this.#dockPos ?? this.#readDockPosition();
600
613
  const expandCorner = this.#expandCorner;
614
+ const gripInset = this.#gripInset;
615
+ const fillInset = this.#fillInset;
601
616
  const cornerSpot = this.#cornerSpot ?? this.#readLauncherPosition();
602
617
  const owner = this.#positionOwner();
603
618
  return () => {
@@ -624,6 +639,8 @@ export class PanelPlacement {
624
639
  this.#launcherPos = launcherPos;
625
640
  this.#panelPos = panelPos;
626
641
  this.#expandCorner = expandCorner;
642
+ this.#gripInset = gripInset;
643
+ this.#fillInset = fillInset;
627
644
  } else {
628
645
  this.#forgetCornerPosition();
629
646
  }
@@ -746,6 +763,31 @@ export class PanelPlacement {
746
763
  */
747
764
  #stampHeader(): void {
748
765
  this.#header?.toggleAttribute("data-fills-screen", this.#fillsScreen());
766
+ // And for the bar and the grips, which go while it does. The next frame, because
767
+ // this runs inside the transcript's observer, and the bar it takes away sits
768
+ // above that in the tree: its own observer would be left undelivered, which the
769
+ // browser reports on the window, where a host's error tracking counts it.
770
+ const element = this.#host.element;
771
+ if (this.#fillsWindow() !== element.hasAttribute("data-fills-window")) {
772
+ requestAnimationFrame(() => {
773
+ const fills = this.#fillsWindow();
774
+ if (fills !== element.hasAttribute("data-fills-window")) {
775
+ this.#barTaken = fills
776
+ ? { room: this.#viewport().height, window: this.#screen().height, bar: this.#barHeight }
777
+ : null;
778
+ element.toggleAttribute("data-fills-window", fills);
779
+ }
780
+ });
781
+ }
782
+ }
783
+
784
+ /**
785
+ * Whether the panel fills the window in a placement it can move in. Not one that owns
786
+ * its position: full stops at the bar, so taking the bar away would make it fill the
787
+ * window, and that would take the bar away for good.
788
+ */
789
+ #fillsWindow(): boolean {
790
+ return this.#fillsScreen() && this.#surface().panelMovable;
749
791
  }
750
792
 
751
793
  /** Put a custom property back to a previous value, or remove it if there was none. */
@@ -848,6 +890,10 @@ export class PanelPlacement {
848
890
  element.style.removeProperty(DOCK_RIGHT);
849
891
  }
850
892
  element.toggleAttribute("data-dock-anchored", target === "none" && anchor !== null);
893
+ this.#barHeight = height;
894
+ if (target !== "bar") {
895
+ this.#barTaken = null;
896
+ }
851
897
  this.#dockTop =
852
898
  target === "bar" && height > 0
853
899
  ? bar.getBoundingClientRect().top
@@ -881,19 +927,43 @@ export class PanelPlacement {
881
927
  * block reads the host's inset before its own, so a dragged spot written
882
928
  * there would hold the panel off the screen's edges. The spot itself stays,
883
929
  * for the next time there is room for it.
930
+ *
931
+ * A host's size that makes the panel the window hangs it off the resting inset's
932
+ * far side, past the top of the window with the header on it under the bar, so
933
+ * it stands at the start of the room instead, moved or not, as a corner panel
934
+ * does, and the stylesheet takes the bar away for as long as it does. A placement
935
+ * that owns its position keeps it, whatever the host's size.
936
+ *
937
+ * Under data-launcher-drag="false" a spot from before the attribute is left alone
938
+ * while the panel is on the screen, as a corner panel's is, and the record stays
939
+ * for the day the attribute comes off.
884
940
  */
885
941
  #placeDocked(): void {
886
- if (!this.#docked() || !this.#panelMovable() || this.dragging()) {
942
+ if (!this.#docked() || !this.#surface().panelMovable || this.dragging()) {
887
943
  return;
888
944
  }
889
- const at = this.#dockPos ?? this.#readDockPosition();
945
+ const element = this.#host.element;
946
+ const at = this.#panelMovable() ? (this.#dockPos ?? this.#readDockPosition()) : null;
890
947
  if (at === null) {
948
+ if (this.#dockPos !== null) {
949
+ const filling = element.style.getPropertyValue("--ag-ui-inset") === this.#fillInset;
950
+ if (!filling && !this.#offScreen()) {
951
+ return;
952
+ }
953
+ this.#dockPos = null;
954
+ element.style.removeProperty("--ag-ui-inset");
955
+ }
956
+ this.#placeUnstated();
891
957
  return;
892
958
  }
893
959
  this.#dockPos = at;
894
- this.#host.element.style.removeProperty("--ag-ui-inset");
895
- const rest = this.#host.element.getBoundingClientRect();
896
- if (coversViewport(rest, this.#viewport())) {
960
+ element.style.removeProperty("--ag-ui-inset");
961
+ const rest = element.getBoundingClientRect();
962
+ const viewport = this.#viewport();
963
+ if (coversViewport(rest, this.#fillRoom())) {
964
+ if (hangsOff(rest, viewport, this.#screen())) {
965
+ this.#standAtStart(rest);
966
+ }
897
967
  return;
898
968
  }
899
969
  // Measured at the dock, so the inset below is written from the corner the
@@ -960,7 +1030,49 @@ export class PanelPlacement {
960
1030
 
961
1031
  /** Whether the panel covers the usable viewport, as it stands now. */
962
1032
  #fillsScreen(): boolean {
963
- return coversViewport(this.#host.element.getBoundingClientRect(), this.#viewport());
1033
+ return coversViewport(this.#host.element.getBoundingClientRect(), this.#fillRoom());
1034
+ }
1035
+
1036
+ /**
1037
+ * The box a fill is judged against: the usable one, unless taking the bar away is
1038
+ * what grew it.
1039
+ *
1040
+ * A host can write the bar's height into the inset it reserves. The panel fills the
1041
+ * box that leaves, so the bar goes, the height says 0, the host writes that, the box
1042
+ * grows by what the bar took, and the panel no longer fills it. Judged against that,
1043
+ * the bar comes back and it all goes round again every frame without end. So on the
1044
+ * same window, a box grown by the bar's height is the box it was, and the panel
1045
+ * stands where it stood with the bar gone. Grown by anything else, or on a window of
1046
+ * another size, it is a change of the host's or the reader's and is judged as one.
1047
+ * The slack is the pixel a host rounding the height up can add.
1048
+ */
1049
+ #fillRoom(): ViewportBox {
1050
+ const viewport = this.#viewport();
1051
+ const taken = this.#barTaken;
1052
+ if (
1053
+ taken !== null &&
1054
+ taken.window === this.#screen().height &&
1055
+ Math.abs(viewport.height - taken.room - taken.bar) <= 1
1056
+ ) {
1057
+ return { ...viewport, height: taken.room };
1058
+ }
1059
+ return viewport;
1060
+ }
1061
+
1062
+ /**
1063
+ * Whether the panel stands off an edge of the screen, as it stands now.
1064
+ *
1065
+ * The screen rather than the usable box: a panel under chrome the host reserved
1066
+ * has its controls where the host can still bring them back, and is not what a
1067
+ * position kept under data-launcher-drag="false" has to give way to.
1068
+ */
1069
+ #offScreen(): boolean {
1070
+ const screen = this.#screen();
1071
+ return hangsOff(
1072
+ this.#host.element.getBoundingClientRect(),
1073
+ { left: 0, top: 0, ...screen },
1074
+ screen,
1075
+ );
964
1076
  }
965
1077
 
966
1078
  /**
@@ -1392,6 +1504,8 @@ export class PanelPlacement {
1392
1504
  * reopen to happen: a line typed in the composer is one. A position waiting to
1393
1505
  * be handed back under data-launcher-drag="false" waits on, because a panel that leaves for the
1394
1506
  * host's corner as the reader starts typing is a jump with no cause they can see.
1507
+ * It waits no longer than the panel stays on the screen: a host's size can stand
1508
+ * it off an edge, where its header is out of reach until something else happens.
1395
1509
  */
1396
1510
  settleLauncherPosition(): void {
1397
1511
  this.#placeFromRecord(false);
@@ -1417,7 +1531,7 @@ export class PanelPlacement {
1417
1531
  // interaction, like every other answer here.
1418
1532
  const draggable = this.#launcherDraggable();
1419
1533
  if (!draggable && this.#launcherPos !== null) {
1420
- if (!handBack) {
1534
+ if (!handBack && !this.#offScreen()) {
1421
1535
  return;
1422
1536
  }
1423
1537
  this.releaseLauncherPosition();
@@ -1485,8 +1599,9 @@ export class PanelPlacement {
1485
1599
  }
1486
1600
 
1487
1601
  /**
1488
- * Place a corner panel that has no spot, the half of {@link #standFullBleed}
1489
- * with nothing stated to keep and no launcher of its own to measure.
1602
+ * Place a panel that has no spot, a corner one or a docked one, the half of
1603
+ * {@link #standFullBleed} with nothing stated to keep and no launcher of its own
1604
+ * to measure.
1490
1605
  *
1491
1606
  * Only an inset this element wrote comes off or goes back. One the host wrote
1492
1607
  * inline is a token a host may state on a phone as anywhere else, and with no
@@ -1496,7 +1611,7 @@ export class PanelPlacement {
1496
1611
  const element = this.#host.element;
1497
1612
  const inline = element.style.getPropertyValue("--ag-ui-inset");
1498
1613
  const viewport = this.#viewport();
1499
- if (!coversViewport(element.getBoundingClientRect(), viewport)) {
1614
+ if (!coversViewport(element.getBoundingClientRect(), this.#fillRoom())) {
1500
1615
  if (inline === "" || inline === this.#fillInset) {
1501
1616
  this.#restoreProperty("--ag-ui-inset", this.#gripInset ?? "");
1502
1617
  this.syncResizeAnchor();
@@ -1513,12 +1628,21 @@ export class PanelPlacement {
1513
1628
  element.style.getPropertyValue("--ag-ui-inset") === "" &&
1514
1629
  hangsOff(rest, viewport, screen)
1515
1630
  ) {
1516
- this.#fillInset = insetFrom({ x: "left", y: "top" }, atStartOf(viewport, rest), screen);
1517
- element.style.setProperty("--ag-ui-inset", this.#fillInset);
1518
- this.syncResizeAnchor();
1631
+ this.#standAtStart(rest);
1519
1632
  }
1520
1633
  }
1521
1634
 
1635
+ /** Stand a box a host's size fills at the start of the room the host left free. */
1636
+ #standAtStart(rest: PanelRect): void {
1637
+ this.#fillInset = insetFrom(
1638
+ { x: "left", y: "top" },
1639
+ atStartOf(this.#viewport(), rest),
1640
+ this.#screen(),
1641
+ );
1642
+ this.#host.element.style.setProperty("--ag-ui-inset", this.#fillInset);
1643
+ this.syncResizeAnchor();
1644
+ }
1645
+
1522
1646
  /** The persisted position for this instance, or null. */
1523
1647
  #readLauncherPosition(): CornerSpot | null {
1524
1648
  const raw = this.#host.readPreference(LAUNCHER_KEY);
@@ -90,6 +90,15 @@ export const RESIZE_STYLES: string = `
90
90
  bottom: 0;
91
91
  }
92
92
 
93
+ /* A docked panel a host sized to the whole window has nothing to give a grip: its size
94
+ is the host's, and a place a grip left would outlast that size with the bar back
95
+ under it. They go as they do on a phone. A corner panel keeps them, since it has no
96
+ bar to stand clear of and a grip's place is its own. */
97
+ :host([data-collapsed-to="bar"][data-fills-window]) .resize-handle,
98
+ :host([data-collapsed-to="none"][data-fills-window]) .resize-handle {
99
+ display: none;
100
+ }
101
+
93
102
  /* Docked: the placement owns the height, so the horizontal edges and every
94
103
  corner are inert and must not advertise a drag that does nothing. The two
95
104
  vertical edges remain, which is the same affordance these placements had
@@ -97,7 +97,18 @@ export const TRANSCRIPT_STYLES: string = `
97
97
 
98
98
  visibility rather than display: none, because a box that generates no
99
99
  layout has no used values to read. Zero-sized, absolutely positioned and
100
- inert, so it costs nothing but the read. */
100
+ inert, so it costs nothing but the read.
101
+
102
+ The padding gives that box the size of the tokens, which a card narrower than
103
+ one of them would scroll to reach. Scrollable overflow counts a box as its
104
+ transform draws it, so a scale of nothing takes it out of that count on every
105
+ side and in either direction, and leaves what is read: the padding's used values,
106
+ and the containing block a percentage resolves against. Position fixed would add
107
+ no scroll either, and would change that block.
108
+
109
+ From its own corner, which is inside whatever holds it where the centre of the
110
+ padding need not be: kept for an engine that counts a point with no area. Chromium
111
+ does not, so the suite cannot pin the origin. */
101
112
  .viewport-probe {
102
113
  position: absolute;
103
114
  top: 0;
@@ -106,6 +117,8 @@ export const TRANSCRIPT_STYLES: string = `
106
117
  height: 0;
107
118
  visibility: hidden;
108
119
  pointer-events: none;
120
+ transform: scale(0);
121
+ transform-origin: 0 0;
109
122
  padding: var(--_viewport-inset-top) var(--_viewport-inset-right)
110
123
  var(--_viewport-inset-bottom) var(--_viewport-inset-left);
111
124
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.50.0";
1
+ export const VERSION: string = "0.50.1";