@artooi/ag-ui-web-component 0.49.0 → 0.50.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +175 -1
- package/README.md +25 -14
- package/dist/ag-ui-web-component.bundle.js +168 -125
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/index.js +530 -110
- package/dist/index.js.map +2 -2
- package/dist/tools/chat_surface_tools.d.ts +8 -5
- package/dist/tools/chat_surface_tools.d.ts.map +1 -1
- package/dist/ui/placement/collapse_styles.d.ts.map +1 -1
- package/dist/ui/placement/panel_placement.d.ts +15 -1
- package/dist/ui/placement/panel_placement.d.ts.map +1 -1
- package/dist/ui/placement/resize_styles.d.ts.map +1 -1
- package/dist/ui/shell/frame_styles.d.ts.map +1 -1
- package/dist/ui/shell/token_styles.d.ts.map +1 -1
- package/dist/ui/themes/classic_tokens.d.ts.map +1 -1
- package/dist/ui/themes/ledger_styles.d.ts.map +1 -1
- package/dist/ui/themes/ledger_tokens.d.ts.map +1 -1
- package/dist/ui/themes/quiet_styles.d.ts.map +1 -1
- package/dist/ui/themes/soft_styles.d.ts.map +1 -1
- package/dist/ui/themes/soft_tokens.d.ts.map +1 -1
- package/dist/ui/transcript/transcript_styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/ag_ui_chat.ts +73 -7
- package/src/tools/chat_surface_tools.ts +8 -5
- package/src/ui/placement/collapse_styles.ts +8 -0
- package/src/ui/placement/panel_placement.ts +512 -99
- package/src/ui/placement/resize_styles.ts +9 -0
- package/src/ui/shell/frame_styles.ts +22 -2
- package/src/ui/shell/token_styles.ts +7 -6
- package/src/ui/themes/classic_tokens.ts +1 -0
- package/src/ui/themes/ledger_styles.ts +4 -3
- package/src/ui/themes/ledger_tokens.ts +1 -0
- package/src/ui/themes/quiet_styles.ts +2 -15
- package/src/ui/themes/soft_styles.ts +4 -3
- package/src/ui/themes/soft_tokens.ts +1 -0
- package/src/ui/transcript/transcript_styles.ts +14 -1
- package/src/version.ts +1 -1
|
@@ -59,6 +59,16 @@ const LAUNCHER_KEY = "ag-ui-chat:launcher";
|
|
|
59
59
|
*/
|
|
60
60
|
const DOCK_KEY = "ag-ui-chat:dock";
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Where the user last put a corner panel's launcher, and the panel itself when
|
|
64
|
+
* they stated that too. The shape the launcher's record is stored in.
|
|
65
|
+
*/
|
|
66
|
+
interface CornerSpot {
|
|
67
|
+
readonly left: number;
|
|
68
|
+
readonly top: number;
|
|
69
|
+
readonly panel?: { readonly left: number; readonly top: number };
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
/** A stored `{ left, top }` pair, or null for anything that is not one. */
|
|
63
73
|
function asPoint(value: unknown): { readonly left: number; readonly top: number } | null {
|
|
64
74
|
if (typeof value !== "object" || value === null) {
|
|
@@ -79,6 +89,34 @@ function coversViewport(box: Extent, viewport: Extent): boolean {
|
|
|
79
89
|
return box.width >= viewport.width - 1 && box.height >= viewport.height - 1;
|
|
80
90
|
}
|
|
81
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Whether a box that covers the usable viewport stands off any edge of it.
|
|
94
|
+
*
|
|
95
|
+
* The far edges are read against the layout viewport rather than the usable box,
|
|
96
|
+
* because a keyboard shortens the second and pans the panel down inside the
|
|
97
|
+
* first: a phone's full-bleed panel is correctly past the usable box's foot then,
|
|
98
|
+
* and pinning it back would put its header above the screen.
|
|
99
|
+
*/
|
|
100
|
+
function hangsOff(box: PanelRect, viewport: ViewportBox, screen: Extent): boolean {
|
|
101
|
+
const slack = 0.5;
|
|
102
|
+
return (
|
|
103
|
+
box.left < viewport.left - slack ||
|
|
104
|
+
box.top < viewport.top - slack ||
|
|
105
|
+
box.right > screen.width + slack ||
|
|
106
|
+
box.bottom > screen.height + slack
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A box of the same size as `box`, standing at the start of the usable viewport. */
|
|
111
|
+
function atStartOf(viewport: ViewportBox, box: PanelRect): PanelRect {
|
|
112
|
+
return {
|
|
113
|
+
left: viewport.left,
|
|
114
|
+
top: viewport.top,
|
|
115
|
+
right: viewport.left + (box.right - box.left),
|
|
116
|
+
bottom: viewport.top + (box.bottom - box.top),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
82
120
|
/**
|
|
83
121
|
* The `--ag-ui-inset` value that pins `box` by `corner`.
|
|
84
122
|
*
|
|
@@ -165,6 +203,7 @@ export class PanelPlacement {
|
|
|
165
203
|
* header on screen, and the launcher is the handle then.
|
|
166
204
|
*/
|
|
167
205
|
enablePanelDrag(header: HTMLElement): void {
|
|
206
|
+
this.#header = header;
|
|
168
207
|
enablePanelDrag(header, {
|
|
169
208
|
enabled: () => !this.#host.collapsed() && this.#canDragPanel(),
|
|
170
209
|
rect: () => this.#host.element.getBoundingClientRect(),
|
|
@@ -191,12 +230,22 @@ export class PanelPlacement {
|
|
|
191
230
|
|
|
192
231
|
/** Build the eight resize grips into `chat`, with one of them in the tab order. */
|
|
193
232
|
mountResizeGrips(chat: HTMLElement): void {
|
|
233
|
+
// The grips a key gesture was on are gone, and a key that comes up on nothing
|
|
234
|
+
// ends nothing, so an element taken out mid-gesture would otherwise come back
|
|
235
|
+
// deaf to every window resize.
|
|
236
|
+
this.#resizing = false;
|
|
194
237
|
for (const grip of RESIZE_GRIPS) {
|
|
195
238
|
const handle = createResizeHandle(grip, {
|
|
196
239
|
axis: () => this.#resizeAxis(),
|
|
197
240
|
rect: () => this.#host.element.getBoundingClientRect(),
|
|
198
|
-
apply: (box) =>
|
|
199
|
-
|
|
241
|
+
apply: (box) => {
|
|
242
|
+
this.#resizing = true;
|
|
243
|
+
this.#applyResize(grip, box);
|
|
244
|
+
},
|
|
245
|
+
commit: (box) => {
|
|
246
|
+
this.#resizing = false;
|
|
247
|
+
this.#commitResize(grip, box);
|
|
248
|
+
},
|
|
200
249
|
label: this.#host.strings().resizePanel,
|
|
201
250
|
});
|
|
202
251
|
// Only one of the eight is in the tab order. Eight separators between the
|
|
@@ -229,6 +278,7 @@ export class PanelPlacement {
|
|
|
229
278
|
* it always has.
|
|
230
279
|
*/
|
|
231
280
|
resetSize(): void {
|
|
281
|
+
const sized = this.#host.element.getBoundingClientRect();
|
|
232
282
|
this.#host.element.style.removeProperty(DRAG_WIDTH);
|
|
233
283
|
this.#host.element.style.removeProperty(DRAG_HEIGHT);
|
|
234
284
|
this.#host.clearPreference(SIZE_KEY);
|
|
@@ -238,12 +288,58 @@ export class PanelPlacement {
|
|
|
238
288
|
}
|
|
239
289
|
this.#dockPos = null;
|
|
240
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();
|
|
294
|
+
} else {
|
|
295
|
+
this.#carrySpotWithSize(sized);
|
|
241
296
|
}
|
|
242
297
|
// The anchor was measured against the dragged box; the resting one may
|
|
243
298
|
// pin different edges.
|
|
244
299
|
this.syncResizeAnchor();
|
|
245
300
|
}
|
|
246
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Carry a stated corner spot along with a dragged size coming off.
|
|
304
|
+
*
|
|
305
|
+
* The size comes off toward the corner the layout holds still, so the top-left
|
|
306
|
+
* the spot is kept as moves with it, and a restore from the old one would swing
|
|
307
|
+
* the panel away from its launcher. Only that movement goes onto the spot, not
|
|
308
|
+
* where the box stands: in a window narrower than the spot the box is held
|
|
309
|
+
* short of it, and that is not something the user stated.
|
|
310
|
+
*
|
|
311
|
+
* Left alone while a host's own size is in force, because that cannot be told
|
|
312
|
+
* from the dragged size coming off, and it may be a state the host is about to
|
|
313
|
+
* leave: counting it would throw the spot off by as much as that state is
|
|
314
|
+
* bigger. A panel that is the screen needs no case of its own: the small-viewport
|
|
315
|
+
* block ignores a dragged size, so its box does not move.
|
|
316
|
+
*
|
|
317
|
+
* Left alone too under data-launcher-drag="false", where a spot left over from
|
|
318
|
+
* before the attribute is a record kept for the day it comes off, and nothing
|
|
319
|
+
* that happens to the box now is a statement about it.
|
|
320
|
+
*/
|
|
321
|
+
#carrySpotWithSize(sized: DOMRect): void {
|
|
322
|
+
const element = this.#host.element;
|
|
323
|
+
const box = element.getBoundingClientRect();
|
|
324
|
+
const spot = this.#cornerSpot ?? this.#readLauncherPosition();
|
|
325
|
+
const style = getComputedStyle(element);
|
|
326
|
+
if (
|
|
327
|
+
spot?.panel === undefined ||
|
|
328
|
+
!this.#launcherDraggable() ||
|
|
329
|
+
style.getPropertyValue("--ag-ui-width") !== "" ||
|
|
330
|
+
style.getPropertyValue("--ag-ui-height") !== ""
|
|
331
|
+
) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
this.#writeCornerSpot({
|
|
335
|
+
...spot,
|
|
336
|
+
panel: {
|
|
337
|
+
left: spot.panel.left + box.left - sized.left,
|
|
338
|
+
top: spot.panel.top + box.top - sized.top,
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
247
343
|
/**
|
|
248
344
|
* A zero-sized box carrying the host's viewport insets as padding, so they
|
|
249
345
|
* can be read back as used pixel lengths. See the `.viewport-probe` rule for
|
|
@@ -271,6 +367,36 @@ export class PanelPlacement {
|
|
|
271
367
|
*/
|
|
272
368
|
#panelPos: { readonly left: number; readonly top: number } | null = null;
|
|
273
369
|
|
|
370
|
+
/**
|
|
371
|
+
* The spot #launcherPos and #panelPos were last stated as, or null while
|
|
372
|
+
* nothing has been stated since this element connected.
|
|
373
|
+
*
|
|
374
|
+
* Those two say where the launcher and the panel stand now, held inside the
|
|
375
|
+
* viewport, because that is where a press finds them and a drag starts from.
|
|
376
|
+
* This is what the user asked for, kept apart so that a window narrower than
|
|
377
|
+
* the spot, or a phone that took the panel full-bleed, gives it back unchanged
|
|
378
|
+
* once there is room again. Only a gesture, a move or a dragged size coming off
|
|
379
|
+
* changes it; re-applying it against a viewport never does, which is the difference from a clamped
|
|
380
|
+
* position quietly becoming the record one resize at a time.
|
|
381
|
+
*/
|
|
382
|
+
#cornerSpot: CornerSpot | null = null;
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The inset a grip wrote on a corner panel with no spot, or null. A grip on an
|
|
386
|
+
* edge the layout holds moves the panel without stating a spot, so this is the
|
|
387
|
+
* only way to tell its inline inset from one a host wrote there, which stays,
|
|
388
|
+
* and to give it back once a phone has taken the panel full-bleed.
|
|
389
|
+
*/
|
|
390
|
+
#gripInset: string | null = null;
|
|
391
|
+
|
|
392
|
+
/**
|
|
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.
|
|
397
|
+
*/
|
|
398
|
+
#fillInset: string | null = null;
|
|
399
|
+
|
|
274
400
|
/**
|
|
275
401
|
* Where the user dragged a docked panel, in viewport coordinates, or null
|
|
276
402
|
* while it stands on the bar or the anchor.
|
|
@@ -289,6 +415,13 @@ export class PanelPlacement {
|
|
|
289
415
|
* syncDock, which runs on every change either can make.
|
|
290
416
|
*/
|
|
291
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;
|
|
292
425
|
|
|
293
426
|
/**
|
|
294
427
|
* The corner the panel opens away from, once this element is placing itself.
|
|
@@ -303,6 +436,12 @@ export class PanelPlacement {
|
|
|
303
436
|
*/
|
|
304
437
|
#anchor: ResizeAnchor = { x: "right", y: "bottom" };
|
|
305
438
|
|
|
439
|
+
/** The header the panel drag listens on, stamped by #stampHeader. */
|
|
440
|
+
#header: HTMLElement | null = null;
|
|
441
|
+
|
|
442
|
+
/** Whether a grip has applied a box it has not committed yet. */
|
|
443
|
+
#resizing = false;
|
|
444
|
+
|
|
306
445
|
/** The eight grips, by name, so the keyboard-reachable one can be moved. */
|
|
307
446
|
readonly #resizeHandles = new Map<string, HTMLDivElement>();
|
|
308
447
|
|
|
@@ -345,12 +484,16 @@ export class PanelPlacement {
|
|
|
345
484
|
*
|
|
346
485
|
* Read from the stamp the drag helpers already set, rather than tracked
|
|
347
486
|
* separately: one source of truth, and it clears on `pointercancel` as well
|
|
348
|
-
* as `pointerup`, which is the end a touch gesture usually gets.
|
|
487
|
+
* as `pointerup`, which is the end a touch gesture usually gets. The one
|
|
488
|
+
* gesture that stamps nothing is an arrow key on a grip, which applies a box
|
|
489
|
+
* per key and states it only when the key comes up, hence #resizing.
|
|
349
490
|
*/
|
|
350
491
|
dragging(): boolean {
|
|
351
492
|
return (
|
|
493
|
+
this.#resizing ||
|
|
352
494
|
this.#host.launcher.hasAttribute("data-dragging") ||
|
|
353
|
-
this.#host.root.querySelector(".header[data-dragging]") !==
|
|
495
|
+
this.#host.root.querySelector(".header[data-dragging], .resize-handle[data-dragging]") !==
|
|
496
|
+
null
|
|
354
497
|
);
|
|
355
498
|
}
|
|
356
499
|
|
|
@@ -358,20 +501,19 @@ export class PanelPlacement {
|
|
|
358
501
|
describeSurface(): ChatSurfaceReport {
|
|
359
502
|
const box = this.#host.element.getBoundingClientRect();
|
|
360
503
|
const viewport = this.#viewport();
|
|
361
|
-
const fullBleed = coversViewport(box,
|
|
504
|
+
const fullBleed = coversViewport(box, this.#fillRoom());
|
|
362
505
|
return {
|
|
363
506
|
placement: this.#host.element.getAttribute("placement"),
|
|
364
507
|
collapseTo: this.#surface().collapseTo,
|
|
365
508
|
collapsed: this.#host.collapsed(),
|
|
366
509
|
collapsible: this.#host.collapsible(),
|
|
367
510
|
movable: this.#panelMovable() && !fullBleed,
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
(this.#surface().launcherDraggable || this.#docked()),
|
|
511
|
+
// The page's setting alone: true for every placement unless the host set
|
|
512
|
+
// `data-launcher-drag="false"`. Whether this panel moves is `movable`,
|
|
513
|
+
// and reading the launcher or the dock in here made the field change
|
|
514
|
+
// with `collapse-to`, so a docked sidebar reported that the page had
|
|
515
|
+
// turned moving off when the placement owned its position.
|
|
516
|
+
draggable: this.#host.element.getAttribute("data-launcher-drag") !== "false",
|
|
375
517
|
fullBleed,
|
|
376
518
|
box: {
|
|
377
519
|
left: Math.round(box.left),
|
|
@@ -396,7 +538,7 @@ export class PanelPlacement {
|
|
|
396
538
|
const restore = options.announce === true ? this.#captureGeometry() : null;
|
|
397
539
|
const viewport = this.#viewport();
|
|
398
540
|
const box = this.#host.element.getBoundingClientRect();
|
|
399
|
-
if (coversViewport(box,
|
|
541
|
+
if (coversViewport(box, this.#fillRoom())) {
|
|
400
542
|
return false;
|
|
401
543
|
}
|
|
402
544
|
const [edgeY, edgeX] = corner.split("-");
|
|
@@ -450,7 +592,16 @@ export class PanelPlacement {
|
|
|
450
592
|
* corner is what the panel grows from, so restoring a position without it
|
|
451
593
|
* puts the box back and animates it out of the wrong side. Absent values are
|
|
452
594
|
* captured as absent and removed on the way back, rather than written as
|
|
453
|
-
* 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.
|
|
598
|
+
*
|
|
599
|
+
* The surface may have changed by the time of the undo, so the two halves are
|
|
600
|
+
* put back apart. Both records are, always, each to its own store, so the
|
|
601
|
+
* memory and the storage cannot disagree and the surface they belong to finds
|
|
602
|
+
* them when it comes back. The inline values only mean something to the surface
|
|
603
|
+
* they were read from: written on any other, they pin a sidebar to where a
|
|
604
|
+
* floating panel used to be.
|
|
454
605
|
*/
|
|
455
606
|
#captureGeometry(): () => void {
|
|
456
607
|
const inset = this.#host.element.style.getPropertyValue("--ag-ui-inset");
|
|
@@ -458,41 +609,56 @@ export class PanelPlacement {
|
|
|
458
609
|
const corner = this.#host.element.getAttribute("data-expand-corner");
|
|
459
610
|
const launcherPos = this.#launcherPos;
|
|
460
611
|
const panelPos = this.#panelPos;
|
|
461
|
-
const dockPos = this.#dockPos;
|
|
612
|
+
const dockPos = this.#dockPos ?? this.#readDockPosition();
|
|
462
613
|
const expandCorner = this.#expandCorner;
|
|
614
|
+
const gripInset = this.#gripInset;
|
|
615
|
+
const fillInset = this.#fillInset;
|
|
616
|
+
const cornerSpot = this.#cornerSpot ?? this.#readLauncherPosition();
|
|
617
|
+
const owner = this.#positionOwner();
|
|
463
618
|
return () => {
|
|
464
|
-
this.#restoreProperty("--ag-ui-inset", inset);
|
|
465
|
-
this.#restoreProperty("--ag-ui-launcher-inset", launcherInset);
|
|
466
|
-
if (corner === null) {
|
|
467
|
-
this.#host.element.removeAttribute("data-expand-corner");
|
|
468
|
-
} else {
|
|
469
|
-
this.#host.element.setAttribute("data-expand-corner", corner);
|
|
470
|
-
}
|
|
471
|
-
this.#launcherPos = launcherPos;
|
|
472
|
-
this.#panelPos = panelPos;
|
|
473
619
|
this.#dockPos = dockPos;
|
|
474
|
-
this.#
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
// Erased rather than rewritten when there was nothing to go back to.
|
|
483
|
-
// #storeLauncherPosition returns early for a null position, which would
|
|
484
|
-
// leave the move this is undoing sitting in storage -- and since that
|
|
485
|
-
// store outlives the tab, the next resize or reload would quietly put
|
|
486
|
-
// the panel back in the corner the user had just rejected.
|
|
487
|
-
if (launcherPos === null) {
|
|
620
|
+
this.#storeDockPosition(dockPos);
|
|
621
|
+
// Erased rather than rewritten when there was nothing to go back to,
|
|
622
|
+
// which would leave the move this is undoing sitting in storage -- and
|
|
623
|
+
// since that store outlives the tab, the next resize or reload would
|
|
624
|
+
// quietly put the panel back in the corner the user had just rejected.
|
|
625
|
+
if (cornerSpot === null) {
|
|
626
|
+
this.#cornerSpot = null;
|
|
488
627
|
this.#host.clearPreference(LAUNCHER_KEY);
|
|
489
628
|
} else {
|
|
490
|
-
this.#
|
|
629
|
+
this.#writeCornerSpot(cornerSpot);
|
|
630
|
+
}
|
|
631
|
+
if (owner === this.#positionOwner()) {
|
|
632
|
+
this.#restoreProperty("--ag-ui-inset", inset);
|
|
633
|
+
this.#restoreProperty("--ag-ui-launcher-inset", launcherInset);
|
|
634
|
+
if (corner === null) {
|
|
635
|
+
this.#host.element.removeAttribute("data-expand-corner");
|
|
636
|
+
} else {
|
|
637
|
+
this.#host.element.setAttribute("data-expand-corner", corner);
|
|
638
|
+
}
|
|
639
|
+
this.#launcherPos = launcherPos;
|
|
640
|
+
this.#panelPos = panelPos;
|
|
641
|
+
this.#expandCorner = expandCorner;
|
|
642
|
+
this.#gripInset = gripInset;
|
|
643
|
+
this.#fillInset = fillInset;
|
|
644
|
+
} else {
|
|
645
|
+
this.#forgetCornerPosition();
|
|
491
646
|
}
|
|
647
|
+
// Against the viewport there is now, which may be a phone's: the insets
|
|
648
|
+
// put back above were written for the one the move was made in.
|
|
649
|
+
this.restoreLauncherPosition();
|
|
492
650
|
this.syncResizeAnchor();
|
|
493
651
|
};
|
|
494
652
|
}
|
|
495
653
|
|
|
654
|
+
/** Which of the three kinds of surface decides where the panel stands. */
|
|
655
|
+
#positionOwner(): "dock" | "corner" | "placement" {
|
|
656
|
+
if (this.#docked()) {
|
|
657
|
+
return "dock";
|
|
658
|
+
}
|
|
659
|
+
return this.#surface().launcherDraggable ? "corner" : "placement";
|
|
660
|
+
}
|
|
661
|
+
|
|
496
662
|
/**
|
|
497
663
|
* Which axes the current placement allows.
|
|
498
664
|
*
|
|
@@ -584,6 +750,44 @@ export class PanelPlacement {
|
|
|
584
750
|
this.#anchor = anchor;
|
|
585
751
|
this.#host.element.setAttribute("data-resize-anchor", `${anchor.y}-${anchor.x}`);
|
|
586
752
|
this.#focusableGrip();
|
|
753
|
+
this.#stampHeader();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Tell the stylesheet whether the panel is the screen, for the header's cursor.
|
|
758
|
+
*
|
|
759
|
+
* Only this half of what refuses a header drag: the resting rules already answer
|
|
760
|
+
* for the placement and for data-launcher-drag="false", and neither of those
|
|
761
|
+
* re-applies anything when it changes, so a stamp taking them in would go on
|
|
762
|
+
* refusing, in the cursor, a drag the header would take.
|
|
763
|
+
*/
|
|
764
|
+
#stampHeader(): void {
|
|
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;
|
|
587
791
|
}
|
|
588
792
|
|
|
589
793
|
/** Put a custom property back to a previous value, or remove it if there was none. */
|
|
@@ -686,6 +890,10 @@ export class PanelPlacement {
|
|
|
686
890
|
element.style.removeProperty(DOCK_RIGHT);
|
|
687
891
|
}
|
|
688
892
|
element.toggleAttribute("data-dock-anchored", target === "none" && anchor !== null);
|
|
893
|
+
this.#barHeight = height;
|
|
894
|
+
if (target !== "bar") {
|
|
895
|
+
this.#barTaken = null;
|
|
896
|
+
}
|
|
689
897
|
this.#dockTop =
|
|
690
898
|
target === "bar" && height > 0
|
|
691
899
|
? bar.getBoundingClientRect().top
|
|
@@ -719,19 +927,43 @@ export class PanelPlacement {
|
|
|
719
927
|
* block reads the host's inset before its own, so a dragged spot written
|
|
720
928
|
* there would hold the panel off the screen's edges. The spot itself stays,
|
|
721
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.
|
|
722
940
|
*/
|
|
723
941
|
#placeDocked(): void {
|
|
724
|
-
if (!this.#docked() || !this.#
|
|
942
|
+
if (!this.#docked() || !this.#surface().panelMovable || this.dragging()) {
|
|
725
943
|
return;
|
|
726
944
|
}
|
|
727
|
-
const
|
|
945
|
+
const element = this.#host.element;
|
|
946
|
+
const at = this.#panelMovable() ? (this.#dockPos ?? this.#readDockPosition()) : null;
|
|
728
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();
|
|
729
957
|
return;
|
|
730
958
|
}
|
|
731
959
|
this.#dockPos = at;
|
|
732
|
-
|
|
733
|
-
const rest =
|
|
734
|
-
|
|
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
|
+
}
|
|
735
967
|
return;
|
|
736
968
|
}
|
|
737
969
|
// Measured at the dock, so the inset below is written from the corner the
|
|
@@ -788,17 +1020,58 @@ export class PanelPlacement {
|
|
|
788
1020
|
}
|
|
789
1021
|
|
|
790
1022
|
/**
|
|
791
|
-
* Whether a header drag may start now. A
|
|
792
|
-
*
|
|
793
|
-
*
|
|
1023
|
+
* Whether a header drag may start now. A panel that is the screen, on a phone
|
|
1024
|
+
* or by a host's own sizing, has nowhere to go, and a drag there would pull it
|
|
1025
|
+
* off the edges it fills.
|
|
794
1026
|
*/
|
|
795
1027
|
#canDragPanel(): boolean {
|
|
796
|
-
|
|
797
|
-
|
|
1028
|
+
return this.#panelMovable() && !this.#fillsScreen();
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/** Whether the panel covers the usable viewport, as it stands now. */
|
|
1032
|
+
#fillsScreen(): boolean {
|
|
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 };
|
|
798
1058
|
}
|
|
799
|
-
return
|
|
800
|
-
|
|
801
|
-
|
|
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,
|
|
802
1075
|
);
|
|
803
1076
|
}
|
|
804
1077
|
|
|
@@ -988,12 +1261,6 @@ export class PanelPlacement {
|
|
|
988
1261
|
* back rather than leaving a stale inline one behind.
|
|
989
1262
|
*/
|
|
990
1263
|
#applyLauncherPlacement(at: { readonly left: number; readonly top: number }): void {
|
|
991
|
-
// The single gate: callers hand over a position and this decides whether
|
|
992
|
-
// it is this element's to honour. Checking in both places instead would
|
|
993
|
-
// leave one of the two checks permanently unreachable.
|
|
994
|
-
if (!this.#launcherDraggable()) {
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
1264
|
this.#launcherPos = at;
|
|
998
1265
|
// Dropping the bubble hands the panel's position back to the placement.
|
|
999
1266
|
// Keeping a stated one would pin the panel where it was dragged and leave
|
|
@@ -1021,6 +1288,9 @@ export class PanelPlacement {
|
|
|
1021
1288
|
|
|
1022
1289
|
/** Move the launcher live during a drag, without persisting. */
|
|
1023
1290
|
#moveLauncher(left: number, top: number): void {
|
|
1291
|
+
// Stated from the first move, not the last: a key gesture only ends at its
|
|
1292
|
+
// keyup, and an Enter pressed before then expands by way of a restore.
|
|
1293
|
+
this.#cornerSpot = { left, top };
|
|
1024
1294
|
this.#applyLauncherPlacement({ left, top });
|
|
1025
1295
|
}
|
|
1026
1296
|
|
|
@@ -1166,9 +1436,6 @@ export class PanelPlacement {
|
|
|
1166
1436
|
* its own right.
|
|
1167
1437
|
*/
|
|
1168
1438
|
#restorePanelPosition(at: { readonly left: number; readonly top: number }): void {
|
|
1169
|
-
if (!this.#launcherDraggable()) {
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
1439
|
const rect = this.#host.element.getBoundingClientRect();
|
|
1173
1440
|
// The same bound the drag itself used. Taking the default here instead is
|
|
1174
1441
|
// what made a panel dragged to an edge jump a whole resting gutter inward
|
|
@@ -1199,6 +1466,9 @@ export class PanelPlacement {
|
|
|
1199
1466
|
* with it, the panel goes back where it was put; without it, the panel is
|
|
1200
1467
|
* re-derived from the launcher and opens into whatever room the viewport has
|
|
1201
1468
|
* now.
|
|
1469
|
+
*
|
|
1470
|
+
* Every caller is a gesture ending or a move being made, so where the two
|
|
1471
|
+
* stand now is what was stated. A restore never calls this.
|
|
1202
1472
|
*/
|
|
1203
1473
|
#storeLauncherPosition(): void {
|
|
1204
1474
|
const position = this.#launcherPos;
|
|
@@ -1206,10 +1476,13 @@ export class PanelPlacement {
|
|
|
1206
1476
|
return;
|
|
1207
1477
|
}
|
|
1208
1478
|
const panel = this.#panelPos;
|
|
1209
|
-
this.#
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1479
|
+
this.#writeCornerSpot(panel === null ? position : { ...position, panel });
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/** Make `spot` the stated one, and remember it. */
|
|
1483
|
+
#writeCornerSpot(spot: CornerSpot): void {
|
|
1484
|
+
this.#cornerSpot = spot;
|
|
1485
|
+
this.#host.writePreference(LAUNCHER_KEY, JSON.stringify(spot));
|
|
1213
1486
|
}
|
|
1214
1487
|
|
|
1215
1488
|
/**
|
|
@@ -1217,8 +1490,30 @@ export class PanelPlacement {
|
|
|
1217
1490
|
* whenever the window resizes, and before an expand. The viewport that
|
|
1218
1491
|
* stored a position may since have shrunk, and a launcher past the edge is
|
|
1219
1492
|
* unreachable -- it is the only way back to a collapsed conversation.
|
|
1493
|
+
*
|
|
1494
|
+
* The one gate on whether a stored position is this element's to honour: the
|
|
1495
|
+
* gestures check their own before they ever reach the placing below.
|
|
1220
1496
|
*/
|
|
1221
1497
|
restoreLauncherPosition(): void {
|
|
1498
|
+
this.#placeFromRecord(true);
|
|
1499
|
+
this.#stampHeader();
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* Re-apply it for a change in the panel's own layout, which needs no window or
|
|
1504
|
+
* reopen to happen: a line typed in the composer is one. A position waiting to
|
|
1505
|
+
* be handed back under data-launcher-drag="false" waits on, because a panel that leaves for the
|
|
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.
|
|
1509
|
+
*/
|
|
1510
|
+
settleLauncherPosition(): void {
|
|
1511
|
+
this.#placeFromRecord(false);
|
|
1512
|
+
this.#stampHeader();
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/** The placing behind {@link restoreLauncherPosition}. */
|
|
1516
|
+
#placeFromRecord(handBack: boolean): void {
|
|
1222
1517
|
// Before the launcher's record is read at all: it belongs to the corner
|
|
1223
1518
|
// surface, and seeding anything from it would put a spot dragged there on
|
|
1224
1519
|
// a panel that should stand on its dock.
|
|
@@ -1226,32 +1521,130 @@ export class PanelPlacement {
|
|
|
1226
1521
|
this.#placeDocked();
|
|
1227
1522
|
return;
|
|
1228
1523
|
}
|
|
1229
|
-
|
|
1230
|
-
const launcher = this.#launcherPos ?? stored;
|
|
1231
|
-
if (launcher === null) {
|
|
1524
|
+
if (!this.#surface().launcherDraggable) {
|
|
1232
1525
|
return;
|
|
1233
1526
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1527
|
+
// Under data-launcher-drag="false" the widget is wherever the host's CSS puts
|
|
1528
|
+
// it, so a position this element took before the attribute went on is handed
|
|
1529
|
+
// back here, at the first window resize or reopen, and the record stays for the
|
|
1530
|
+
// day the attribute comes off. Nothing watches the attribute: it is read per
|
|
1531
|
+
// interaction, like every other answer here.
|
|
1532
|
+
const draggable = this.#launcherDraggable();
|
|
1533
|
+
if (!draggable && this.#launcherPos !== null) {
|
|
1534
|
+
if (!handBack && !this.#offScreen()) {
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
this.releaseLauncherPosition();
|
|
1538
|
+
}
|
|
1539
|
+
const spot = draggable ? (this.#cornerSpot ?? this.#readLauncherPosition()) : null;
|
|
1540
|
+
if (spot === null) {
|
|
1541
|
+
this.#placeUnstated();
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (this.#standFullBleed(spot)) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if (spot.panel !== undefined) {
|
|
1236
1548
|
// Stated rather than derived: the launcher is only read here to keep the
|
|
1237
1549
|
// offset the two were left with, so it has to be seeded before the panel
|
|
1238
1550
|
// is placed around it.
|
|
1239
|
-
this.#launcherPos = { left:
|
|
1240
|
-
this.#restorePanelPosition(panel);
|
|
1551
|
+
this.#launcherPos = { left: spot.left, top: spot.top };
|
|
1552
|
+
this.#restorePanelPosition(spot.panel);
|
|
1241
1553
|
return;
|
|
1242
1554
|
}
|
|
1243
1555
|
const box = this.#launcherBox();
|
|
1244
1556
|
this.#applyLauncherPlacement(
|
|
1245
|
-
clampLauncher({ ...box, left:
|
|
1557
|
+
clampLauncher({ ...box, left: spot.left, top: spot.top }, this.#viewport()),
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* Leave a corner panel full-bleed if it has become the screen, and say whether
|
|
1563
|
+
* it has.
|
|
1564
|
+
*
|
|
1565
|
+
* The inline inset comes off, because the small-viewport block reads the
|
|
1566
|
+
* host's inset before its own: a spot written there holds the panel off the
|
|
1567
|
+
* edges it should fill, and past them by the same amount, since its size is
|
|
1568
|
+
* the screen's. What the panel covers is read before that, from the size
|
|
1569
|
+
* alone, which no inset changes.
|
|
1570
|
+
*
|
|
1571
|
+
* The launcher stays a place of its own. It is positioned from the host box,
|
|
1572
|
+
* so it is measured again from the box that has just moved, and it is held on
|
|
1573
|
+
* the screen, because it is the way back from a collapsed conversation. The
|
|
1574
|
+
* spot itself is untouched, for the next time there is room for it.
|
|
1575
|
+
*
|
|
1576
|
+
* Above the breakpoint nothing in the stylesheet takes the panel to the edges.
|
|
1577
|
+
* A host's own size makes it the screen while the resting inset still spends a
|
|
1578
|
+
* gutter on one corner, which hangs the box off the opposite one, so there it
|
|
1579
|
+
* stands at the start of the room the host left free and keeps that inset. A
|
|
1580
|
+
* box the placement already holds on the screen has no inset to keep, which is
|
|
1581
|
+
* every phone, and a host's rule that placed the box itself.
|
|
1582
|
+
*/
|
|
1583
|
+
#standFullBleed(spot: CornerSpot): boolean {
|
|
1584
|
+
const element = this.#host.element;
|
|
1585
|
+
const viewport = this.#viewport();
|
|
1586
|
+
if (!coversViewport(element.getBoundingClientRect(), viewport)) {
|
|
1587
|
+
return false;
|
|
1588
|
+
}
|
|
1589
|
+
element.style.removeProperty("--ag-ui-inset");
|
|
1590
|
+
const rest = element.getBoundingClientRect();
|
|
1591
|
+
const hanging = hangsOff(rest, viewport, this.#screen());
|
|
1592
|
+
const box = hanging ? atStartOf(viewport, rest) : rest;
|
|
1593
|
+
const launcher = { ...this.#launcherBox(), left: spot.left, top: spot.top };
|
|
1594
|
+
this.#placePanelAndLauncher(box, { ...launcher, ...clampLauncher(launcher, viewport) });
|
|
1595
|
+
if (!hanging) {
|
|
1596
|
+
element.style.removeProperty("--ag-ui-inset");
|
|
1597
|
+
}
|
|
1598
|
+
return true;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/**
|
|
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.
|
|
1605
|
+
*
|
|
1606
|
+
* Only an inset this element wrote comes off or goes back. One the host wrote
|
|
1607
|
+
* inline is a token a host may state on a phone as anywhere else, and with no
|
|
1608
|
+
* spot it never came from here.
|
|
1609
|
+
*/
|
|
1610
|
+
#placeUnstated(): void {
|
|
1611
|
+
const element = this.#host.element;
|
|
1612
|
+
const inline = element.style.getPropertyValue("--ag-ui-inset");
|
|
1613
|
+
const viewport = this.#viewport();
|
|
1614
|
+
if (!coversViewport(element.getBoundingClientRect(), this.#fillRoom())) {
|
|
1615
|
+
if (inline === "" || inline === this.#fillInset) {
|
|
1616
|
+
this.#restoreProperty("--ag-ui-inset", this.#gripInset ?? "");
|
|
1617
|
+
this.syncResizeAnchor();
|
|
1618
|
+
}
|
|
1619
|
+
this.#fillInset = null;
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
if (inline === this.#gripInset || inline === this.#fillInset) {
|
|
1623
|
+
element.style.removeProperty("--ag-ui-inset");
|
|
1624
|
+
}
|
|
1625
|
+
const rest = element.getBoundingClientRect();
|
|
1626
|
+
const screen = this.#screen();
|
|
1627
|
+
if (
|
|
1628
|
+
element.style.getPropertyValue("--ag-ui-inset") === "" &&
|
|
1629
|
+
hangsOff(rest, viewport, screen)
|
|
1630
|
+
) {
|
|
1631
|
+
this.#standAtStart(rest);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
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(),
|
|
1246
1641
|
);
|
|
1642
|
+
this.#host.element.style.setProperty("--ag-ui-inset", this.#fillInset);
|
|
1643
|
+
this.syncResizeAnchor();
|
|
1247
1644
|
}
|
|
1248
1645
|
|
|
1249
1646
|
/** The persisted position for this instance, or null. */
|
|
1250
|
-
#readLauncherPosition(): {
|
|
1251
|
-
readonly left: number;
|
|
1252
|
-
readonly top: number;
|
|
1253
|
-
readonly panel?: { readonly left: number; readonly top: number };
|
|
1254
|
-
} | null {
|
|
1647
|
+
#readLauncherPosition(): CornerSpot | null {
|
|
1255
1648
|
const raw = this.#host.readPreference(LAUNCHER_KEY);
|
|
1256
1649
|
if (raw === null) {
|
|
1257
1650
|
return null;
|
|
@@ -1293,9 +1686,16 @@ export class PanelPlacement {
|
|
|
1293
1686
|
if (this.#launcherDraggable()) {
|
|
1294
1687
|
return;
|
|
1295
1688
|
}
|
|
1689
|
+
this.#forgetCornerPosition();
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/** Take everything off that placed a corner panel, leaving its record alone. */
|
|
1693
|
+
#forgetCornerPosition(): void {
|
|
1296
1694
|
this.#launcherPos = null;
|
|
1297
1695
|
this.#panelPos = null;
|
|
1298
1696
|
this.#expandCorner = null;
|
|
1697
|
+
this.#gripInset = null;
|
|
1698
|
+
this.#fillInset = null;
|
|
1299
1699
|
this.#host.element.style.removeProperty("--ag-ui-inset");
|
|
1300
1700
|
this.#host.element.style.removeProperty("--ag-ui-launcher-inset");
|
|
1301
1701
|
this.#host.element.removeAttribute("data-expand-corner");
|
|
@@ -1339,28 +1739,33 @@ export class PanelPlacement {
|
|
|
1339
1739
|
// on a free edge, which leaves the inset alone.
|
|
1340
1740
|
this.#dockPos = { left: box.left, top: box.top };
|
|
1341
1741
|
}
|
|
1342
|
-
if (grip.x
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1742
|
+
if (grip.x === this.#anchor.x || grip.y === this.#anchor.y) {
|
|
1743
|
+
// The whole screen, not the usable box: a CSS inset on a fixed element is
|
|
1744
|
+
// measured from the real edges, so expressing a right or bottom against a
|
|
1745
|
+
// box the host has inset comes out short by exactly that inset.
|
|
1746
|
+
const anchor = this.#anchor;
|
|
1747
|
+
const inset = insetFrom(anchor, box, this.#screen());
|
|
1748
|
+
this.#host.element.style.setProperty("--ag-ui-inset", inset);
|
|
1749
|
+
this.#gripInset = this.#launcherPos === null && !this.#docked() ? inset : null;
|
|
1750
|
+
// The launcher lives at this corner of the panel, so a corner that moved
|
|
1751
|
+
// takes it along. Without this the next expand would re-derive the panel's
|
|
1752
|
+
// position from a launcher still standing where the panel used to be, and
|
|
1753
|
+
// undo the move.
|
|
1754
|
+
if (this.#launcherPos !== null) {
|
|
1755
|
+
const size = this.#host.launcher.offsetWidth;
|
|
1756
|
+
this.#launcherPos = {
|
|
1757
|
+
left: anchor.x === "left" ? box.left : box.right - size,
|
|
1758
|
+
top: anchor.y === "top" ? box.top : box.bottom - size,
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1360
1761
|
}
|
|
1361
|
-
// A stated panel
|
|
1362
|
-
|
|
1363
|
-
|
|
1762
|
+
// A stated corner panel's spot is a claim about this box, so it moves with
|
|
1763
|
+
// it, by a free edge as well as by a pinned one. Read from what was drawn,
|
|
1764
|
+
// not what the pointer asked for, because a host's maximum stops the box short
|
|
1765
|
+
// of the pointer. Not while docked: this position belongs to the corner.
|
|
1766
|
+
if (this.#panelPos !== null && !this.#docked()) {
|
|
1767
|
+
const drawn = this.#host.element.getBoundingClientRect();
|
|
1768
|
+
this.#panelPos = { left: drawn.left, top: drawn.top };
|
|
1364
1769
|
}
|
|
1365
1770
|
return box;
|
|
1366
1771
|
}
|
|
@@ -1372,6 +1777,14 @@ export class PanelPlacement {
|
|
|
1372
1777
|
// mount, which is the same disagreement between apply and commit that made
|
|
1373
1778
|
// the header drag jump on release.
|
|
1374
1779
|
const held = this.#applyResize(grip, box);
|
|
1780
|
+
// A grip on the free edges of a panel standing at the start of a filled window
|
|
1781
|
+
// leaves that inset alone, and the size it stated takes the panel off the
|
|
1782
|
+
// screen's shape. It has to stay where the grip left it, so the inset stops
|
|
1783
|
+
// being one that lasts only as long as the host's size does.
|
|
1784
|
+
if (this.#host.element.style.getPropertyValue("--ag-ui-inset") === this.#fillInset) {
|
|
1785
|
+
this.#gripInset = this.#fillInset;
|
|
1786
|
+
this.#fillInset = null;
|
|
1787
|
+
}
|
|
1375
1788
|
this.#persistSize({ width: held.right - held.left, height: held.bottom - held.top });
|
|
1376
1789
|
this.#storeLauncherPosition();
|
|
1377
1790
|
if (this.#dockPos !== null) {
|