@artooi/ag-ui-web-component 0.48.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +192 -1
- package/README.md +70 -7
- package/dist/ag-ui-web-component.bundle.js +171 -107
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/core/ag_ui_chat.d.ts +8 -2
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/index.js +591 -82
- package/dist/index.js.map +3 -3
- 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/dock_styles.d.ts.map +1 -1
- package/dist/ui/placement/panel_placement.d.ts +18 -1
- package/dist/ui/placement/panel_placement.d.ts.map +1 -1
- package/dist/ui/placement/surface_for.d.ts +15 -6
- package/dist/ui/placement/surface_for.d.ts.map +1 -1
- package/dist/ui/progress/tool_group_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/package.json +1 -1
- package/src/core/ag_ui_chat.ts +56 -8
- package/src/tools/chat_surface_tools.ts +11 -10
- package/src/ui/placement/dock_styles.ts +5 -1
- package/src/ui/placement/panel_placement.ts +587 -82
- package/src/ui/placement/surface_for.ts +33 -8
- package/src/ui/progress/tool_group_styles.ts +3 -2
- package/src/ui/shell/frame_styles.ts +22 -2
- package/src/ui/shell/token_styles.ts +31 -0
- 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 +6 -3
- package/src/ui/themes/soft_styles.ts +4 -3
- package/src/ui/themes/soft_tokens.ts +1 -0
- package/src/version.ts +1 -1
|
@@ -50,6 +50,25 @@ const DOCK_RIGHT = "--_dock-right";
|
|
|
50
50
|
/** Per-tab persistence key for a dragged launcher position. */
|
|
51
51
|
const LAUNCHER_KEY = "ag-ui-chat:launcher";
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Persistence key for where the user dragged a docked panel. Its own key, not
|
|
55
|
+
* the launcher's: a spot stored under the corner launcher describes another
|
|
56
|
+
* surface, and reading it here would open every panel away from the bar for a
|
|
57
|
+
* host that switched to the bar after its users had dragged their corner
|
|
58
|
+
* panels.
|
|
59
|
+
*/
|
|
60
|
+
const DOCK_KEY = "ag-ui-chat:dock";
|
|
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
|
+
|
|
53
72
|
/** A stored `{ left, top }` pair, or null for anything that is not one. */
|
|
54
73
|
function asPoint(value: unknown): { readonly left: number; readonly top: number } | null {
|
|
55
74
|
if (typeof value !== "object" || value === null) {
|
|
@@ -70,6 +89,34 @@ function coversViewport(box: Extent, viewport: Extent): boolean {
|
|
|
70
89
|
return box.width >= viewport.width - 1 && box.height >= viewport.height - 1;
|
|
71
90
|
}
|
|
72
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
|
+
|
|
73
120
|
/**
|
|
74
121
|
* The `--ag-ui-inset` value that pins `box` by `corner`.
|
|
75
122
|
*
|
|
@@ -156,8 +203,9 @@ export class PanelPlacement {
|
|
|
156
203
|
* header on screen, and the launcher is the handle then.
|
|
157
204
|
*/
|
|
158
205
|
enablePanelDrag(header: HTMLElement): void {
|
|
206
|
+
this.#header = header;
|
|
159
207
|
enablePanelDrag(header, {
|
|
160
|
-
enabled: () => !this.#host.collapsed() && this.#
|
|
208
|
+
enabled: () => !this.#host.collapsed() && this.#canDragPanel(),
|
|
161
209
|
rect: () => this.#host.element.getBoundingClientRect(),
|
|
162
210
|
apply: (box, from) => this.#movePanel(box, from),
|
|
163
211
|
commit: (box, from) => this.#commitPanel(box, from),
|
|
@@ -182,12 +230,22 @@ export class PanelPlacement {
|
|
|
182
230
|
|
|
183
231
|
/** Build the eight resize grips into `chat`, with one of them in the tab order. */
|
|
184
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;
|
|
185
237
|
for (const grip of RESIZE_GRIPS) {
|
|
186
238
|
const handle = createResizeHandle(grip, {
|
|
187
239
|
axis: () => this.#resizeAxis(),
|
|
188
240
|
rect: () => this.#host.element.getBoundingClientRect(),
|
|
189
|
-
apply: (box) =>
|
|
190
|
-
|
|
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
|
+
},
|
|
191
249
|
label: this.#host.strings().resizePanel,
|
|
192
250
|
});
|
|
193
251
|
// Only one of the eight is in the tab order. Eight separators between the
|
|
@@ -213,16 +271,72 @@ export class PanelPlacement {
|
|
|
213
271
|
* Drop the dragged size and its stored copy, so the placement's own size and
|
|
214
272
|
* the host's tokens apply again. For a host entering a sized state of its
|
|
215
273
|
* own, which a user's drag would otherwise outrank.
|
|
274
|
+
*
|
|
275
|
+
* A docked panel the user dragged off its dock goes back to it as well, and
|
|
276
|
+
* the stored spot with it: there the position is part of the same resting
|
|
277
|
+
* state the host is asking for. A corner panel keeps a dragged position, as
|
|
278
|
+
* it always has.
|
|
216
279
|
*/
|
|
217
280
|
resetSize(): void {
|
|
281
|
+
const sized = this.#host.element.getBoundingClientRect();
|
|
218
282
|
this.#host.element.style.removeProperty(DRAG_WIDTH);
|
|
219
283
|
this.#host.element.style.removeProperty(DRAG_HEIGHT);
|
|
220
284
|
this.#host.clearPreference(SIZE_KEY);
|
|
285
|
+
if (this.#docked()) {
|
|
286
|
+
if (this.#dockPos !== null) {
|
|
287
|
+
this.#host.element.style.removeProperty("--ag-ui-inset");
|
|
288
|
+
}
|
|
289
|
+
this.#dockPos = null;
|
|
290
|
+
this.#storeDockPosition(null);
|
|
291
|
+
} else {
|
|
292
|
+
this.#carrySpotWithSize(sized);
|
|
293
|
+
}
|
|
221
294
|
// The anchor was measured against the dragged box; the resting one may
|
|
222
295
|
// pin different edges.
|
|
223
296
|
this.syncResizeAnchor();
|
|
224
297
|
}
|
|
225
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Carry a stated corner spot along with a dragged size coming off.
|
|
301
|
+
*
|
|
302
|
+
* The size comes off toward the corner the layout holds still, so the top-left
|
|
303
|
+
* the spot is kept as moves with it, and a restore from the old one would swing
|
|
304
|
+
* the panel away from its launcher. Only that movement goes onto the spot, not
|
|
305
|
+
* where the box stands: in a window narrower than the spot the box is held
|
|
306
|
+
* short of it, and that is not something the user stated.
|
|
307
|
+
*
|
|
308
|
+
* Left alone while a host's own size is in force, because that cannot be told
|
|
309
|
+
* from the dragged size coming off, and it may be a state the host is about to
|
|
310
|
+
* leave: counting it would throw the spot off by as much as that state is
|
|
311
|
+
* bigger. A panel that is the screen needs no case of its own: the small-viewport
|
|
312
|
+
* block ignores a dragged size, so its box does not move.
|
|
313
|
+
*
|
|
314
|
+
* Left alone too under data-launcher-drag="false", where a spot left over from
|
|
315
|
+
* before the attribute is a record kept for the day it comes off, and nothing
|
|
316
|
+
* that happens to the box now is a statement about it.
|
|
317
|
+
*/
|
|
318
|
+
#carrySpotWithSize(sized: DOMRect): void {
|
|
319
|
+
const element = this.#host.element;
|
|
320
|
+
const box = element.getBoundingClientRect();
|
|
321
|
+
const spot = this.#cornerSpot ?? this.#readLauncherPosition();
|
|
322
|
+
const style = getComputedStyle(element);
|
|
323
|
+
if (
|
|
324
|
+
spot?.panel === undefined ||
|
|
325
|
+
!this.#launcherDraggable() ||
|
|
326
|
+
style.getPropertyValue("--ag-ui-width") !== "" ||
|
|
327
|
+
style.getPropertyValue("--ag-ui-height") !== ""
|
|
328
|
+
) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
this.#writeCornerSpot({
|
|
332
|
+
...spot,
|
|
333
|
+
panel: {
|
|
334
|
+
left: spot.panel.left + box.left - sized.left,
|
|
335
|
+
top: spot.panel.top + box.top - sized.top,
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
226
340
|
/**
|
|
227
341
|
* A zero-sized box carrying the host's viewport insets as padding, so they
|
|
228
342
|
* can be read back as used pixel lengths. See the `.viewport-probe` rule for
|
|
@@ -250,6 +364,54 @@ export class PanelPlacement {
|
|
|
250
364
|
*/
|
|
251
365
|
#panelPos: { readonly left: number; readonly top: number } | null = null;
|
|
252
366
|
|
|
367
|
+
/**
|
|
368
|
+
* The spot #launcherPos and #panelPos were last stated as, or null while
|
|
369
|
+
* nothing has been stated since this element connected.
|
|
370
|
+
*
|
|
371
|
+
* Those two say where the launcher and the panel stand now, held inside the
|
|
372
|
+
* viewport, because that is where a press finds them and a drag starts from.
|
|
373
|
+
* This is what the user asked for, kept apart so that a window narrower than
|
|
374
|
+
* the spot, or a phone that took the panel full-bleed, gives it back unchanged
|
|
375
|
+
* once there is room again. Only a gesture, a move or a dragged size coming off
|
|
376
|
+
* changes it; re-applying it against a viewport never does, which is the difference from a clamped
|
|
377
|
+
* position quietly becoming the record one resize at a time.
|
|
378
|
+
*/
|
|
379
|
+
#cornerSpot: CornerSpot | null = null;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The inset a grip wrote on a corner panel with no spot, or null. A grip on an
|
|
383
|
+
* edge the layout holds moves the panel without stating a spot, so this is the
|
|
384
|
+
* only way to tell its inline inset from one a host wrote there, which stays,
|
|
385
|
+
* and to give it back once a phone has taken the panel full-bleed.
|
|
386
|
+
*/
|
|
387
|
+
#gripInset: string | null = null;
|
|
388
|
+
|
|
389
|
+
/**
|
|
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
|
+
*/
|
|
394
|
+
#fillInset: string | null = null;
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Where the user dragged a docked panel, in viewport coordinates, or null
|
|
398
|
+
* while it stands on the bar or the anchor.
|
|
399
|
+
*
|
|
400
|
+
* Apart from the two corner positions above because it moves nothing else:
|
|
401
|
+
* a docked panel has no launcher to carry, and the corner it opens from is
|
|
402
|
+
* the dock's. Held as the user stated it and never as clamped, so a bar that
|
|
403
|
+
* grew, or a phone that took the panel full-bleed, gives the spot back
|
|
404
|
+
* unchanged once there is room for it again.
|
|
405
|
+
*/
|
|
406
|
+
#dockPos: { readonly left: number; readonly top: number } | null = null;
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The line a docked panel stays above, in viewport coordinates: the bar's
|
|
410
|
+
* top, or the anchor's. Infinite where neither is drawn. Measured by
|
|
411
|
+
* syncDock, which runs on every change either can make.
|
|
412
|
+
*/
|
|
413
|
+
#dockTop = Number.POSITIVE_INFINITY;
|
|
414
|
+
|
|
253
415
|
/**
|
|
254
416
|
* The corner the panel opens away from, once this element is placing itself.
|
|
255
417
|
* Null means the host's layout still decides, and the anchor is measured.
|
|
@@ -263,6 +425,12 @@ export class PanelPlacement {
|
|
|
263
425
|
*/
|
|
264
426
|
#anchor: ResizeAnchor = { x: "right", y: "bottom" };
|
|
265
427
|
|
|
428
|
+
/** The header the panel drag listens on, stamped by #stampHeader. */
|
|
429
|
+
#header: HTMLElement | null = null;
|
|
430
|
+
|
|
431
|
+
/** Whether a grip has applied a box it has not committed yet. */
|
|
432
|
+
#resizing = false;
|
|
433
|
+
|
|
266
434
|
/** The eight grips, by name, so the keyboard-reachable one can be moved. */
|
|
267
435
|
readonly #resizeHandles = new Map<string, HTMLDivElement>();
|
|
268
436
|
|
|
@@ -281,7 +449,10 @@ export class PanelPlacement {
|
|
|
281
449
|
* rather than collapsed to nothing.
|
|
282
450
|
*/
|
|
283
451
|
#withinViewport(box: PanelRect): PanelRect {
|
|
284
|
-
|
|
452
|
+
// A docked panel that moves has its room stop at the dock, so no grip
|
|
453
|
+
// pulls it over the bar it minimises to. A placement that owns its
|
|
454
|
+
// position keeps the bound it always had.
|
|
455
|
+
const viewport = this.#docked() && this.#panelMovable() ? this.#dockRoom() : this.#viewport();
|
|
285
456
|
// The same bound a drag stops at, so a grip pulled to the edge and a panel
|
|
286
457
|
// dragged to it come to rest on the same line. The inner Math.max/min pair
|
|
287
458
|
// keeps an already-inverted box from turning inside out.
|
|
@@ -302,12 +473,16 @@ export class PanelPlacement {
|
|
|
302
473
|
*
|
|
303
474
|
* Read from the stamp the drag helpers already set, rather than tracked
|
|
304
475
|
* separately: one source of truth, and it clears on `pointercancel` as well
|
|
305
|
-
* as `pointerup`, which is the end a touch gesture usually gets.
|
|
476
|
+
* as `pointerup`, which is the end a touch gesture usually gets. The one
|
|
477
|
+
* gesture that stamps nothing is an arrow key on a grip, which applies a box
|
|
478
|
+
* per key and states it only when the key comes up, hence #resizing.
|
|
306
479
|
*/
|
|
307
480
|
dragging(): boolean {
|
|
308
481
|
return (
|
|
482
|
+
this.#resizing ||
|
|
309
483
|
this.#host.launcher.hasAttribute("data-dragging") ||
|
|
310
|
-
this.#host.root.querySelector(".header[data-dragging]") !==
|
|
484
|
+
this.#host.root.querySelector(".header[data-dragging], .resize-handle[data-dragging]") !==
|
|
485
|
+
null
|
|
311
486
|
);
|
|
312
487
|
}
|
|
313
488
|
|
|
@@ -321,8 +496,13 @@ export class PanelPlacement {
|
|
|
321
496
|
collapseTo: this.#surface().collapseTo,
|
|
322
497
|
collapsed: this.#host.collapsed(),
|
|
323
498
|
collapsible: this.#host.collapsible(),
|
|
324
|
-
movable: this.#
|
|
325
|
-
|
|
499
|
+
movable: this.#panelMovable() && !fullBleed,
|
|
500
|
+
// The page's setting alone: true for every placement unless the host set
|
|
501
|
+
// `data-launcher-drag="false"`. Whether this panel moves is `movable`,
|
|
502
|
+
// and reading the launcher or the dock in here made the field change
|
|
503
|
+
// with `collapse-to`, so a docked sidebar reported that the page had
|
|
504
|
+
// turned moving off when the placement owned its position.
|
|
505
|
+
draggable: this.#host.element.getAttribute("data-launcher-drag") !== "false",
|
|
326
506
|
fullBleed,
|
|
327
507
|
box: {
|
|
328
508
|
left: Math.round(box.left),
|
|
@@ -341,7 +521,7 @@ export class PanelPlacement {
|
|
|
341
521
|
|
|
342
522
|
/** The move behind `AgUiChat.moveTo`, whose doc is the contract. */
|
|
343
523
|
moveTo(corner: ChatCorner, options: { readonly announce?: boolean } = {}): boolean {
|
|
344
|
-
if (!this.#
|
|
524
|
+
if (!this.#panelMovable()) {
|
|
345
525
|
return false;
|
|
346
526
|
}
|
|
347
527
|
const restore = options.announce === true ? this.#captureGeometry() : null;
|
|
@@ -351,23 +531,31 @@ export class PanelPlacement {
|
|
|
351
531
|
return false;
|
|
352
532
|
}
|
|
353
533
|
const [edgeY, edgeX] = corner.split("-");
|
|
534
|
+
// A docked panel's bottom corners are above its dock, not on the bar.
|
|
535
|
+
const docked = this.#docked();
|
|
536
|
+
const room = docked ? this.#dockRoom() : viewport;
|
|
354
537
|
// Every term is an absolute screen coordinate, because that is what the
|
|
355
538
|
// clamps and the insets both speak. The usable box carries an origin, so
|
|
356
539
|
// its near edge is `viewport.left`, not zero, and its far edge is
|
|
357
540
|
// `viewport.left + viewport.width` -- a margin applied to the extents
|
|
358
541
|
// alone would send the agent's own move under the chrome the host
|
|
359
542
|
// reserved, which is the failure the usable box exists to prevent.
|
|
360
|
-
const nearX =
|
|
361
|
-
const nearY =
|
|
543
|
+
const nearX = room.left + EDGE_MARGIN;
|
|
544
|
+
const nearY = room.top + EDGE_MARGIN;
|
|
362
545
|
const left =
|
|
363
|
-
edgeX === "left"
|
|
364
|
-
? nearX
|
|
365
|
-
: Math.max(nearX, viewport.left + viewport.width - EDGE_MARGIN - box.width);
|
|
546
|
+
edgeX === "left" ? nearX : Math.max(nearX, room.left + room.width - EDGE_MARGIN - box.width);
|
|
366
547
|
const top =
|
|
367
|
-
edgeY === "top"
|
|
368
|
-
? nearY
|
|
369
|
-
: Math.max(nearY, viewport.top + viewport.height - EDGE_MARGIN - box.height);
|
|
548
|
+
edgeY === "top" ? nearY : Math.max(nearY, room.top + room.height - EDGE_MARGIN - box.height);
|
|
370
549
|
const host = { left, top, right: left + box.width, bottom: top + box.height };
|
|
550
|
+
if (docked) {
|
|
551
|
+
this.#dockPos = { left, top };
|
|
552
|
+
this.#writeDockInset(host);
|
|
553
|
+
this.#storeDockPosition(this.#dockPos);
|
|
554
|
+
if (restore !== null) {
|
|
555
|
+
this.#host.announceSurfaceChange(this.#host.strings().chatMoved, restore);
|
|
556
|
+
}
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
371
559
|
// Both axes measured, rather than one read twice: a host can restyle the
|
|
372
560
|
// launcher as a pill, and squaring it here would put it off the corner.
|
|
373
561
|
const launcherWidth = this.#host.launcher.offsetWidth;
|
|
@@ -394,6 +582,13 @@ export class PanelPlacement {
|
|
|
394
582
|
* puts the box back and animates it out of the wrong side. Absent values are
|
|
395
583
|
* captured as absent and removed on the way back, rather than written as
|
|
396
584
|
* empty strings that would outrank the placement.
|
|
585
|
+
*
|
|
586
|
+
* The surface may have changed by the time of the undo, so the two halves are
|
|
587
|
+
* put back apart. Both records are, always, each to its own store, so the
|
|
588
|
+
* memory and the storage cannot disagree and the surface they belong to finds
|
|
589
|
+
* them when it comes back. The inline values only mean something to the surface
|
|
590
|
+
* they were read from: written on any other, they pin a sidebar to where a
|
|
591
|
+
* floating panel used to be.
|
|
397
592
|
*/
|
|
398
593
|
#captureGeometry(): () => void {
|
|
399
594
|
const inset = this.#host.element.style.getPropertyValue("--ag-ui-inset");
|
|
@@ -401,32 +596,52 @@ export class PanelPlacement {
|
|
|
401
596
|
const corner = this.#host.element.getAttribute("data-expand-corner");
|
|
402
597
|
const launcherPos = this.#launcherPos;
|
|
403
598
|
const panelPos = this.#panelPos;
|
|
599
|
+
const dockPos = this.#dockPos ?? this.#readDockPosition();
|
|
404
600
|
const expandCorner = this.#expandCorner;
|
|
601
|
+
const cornerSpot = this.#cornerSpot ?? this.#readLauncherPosition();
|
|
602
|
+
const owner = this.#positionOwner();
|
|
405
603
|
return () => {
|
|
406
|
-
this.#
|
|
407
|
-
this.#
|
|
408
|
-
|
|
409
|
-
|
|
604
|
+
this.#dockPos = dockPos;
|
|
605
|
+
this.#storeDockPosition(dockPos);
|
|
606
|
+
// Erased rather than rewritten when there was nothing to go back to,
|
|
607
|
+
// which would leave the move this is undoing sitting in storage -- and
|
|
608
|
+
// since that store outlives the tab, the next resize or reload would
|
|
609
|
+
// quietly put the panel back in the corner the user had just rejected.
|
|
610
|
+
if (cornerSpot === null) {
|
|
611
|
+
this.#cornerSpot = null;
|
|
612
|
+
this.#host.clearPreference(LAUNCHER_KEY);
|
|
410
613
|
} else {
|
|
411
|
-
this.#
|
|
614
|
+
this.#writeCornerSpot(cornerSpot);
|
|
412
615
|
}
|
|
413
|
-
this.#
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
this.#
|
|
616
|
+
if (owner === this.#positionOwner()) {
|
|
617
|
+
this.#restoreProperty("--ag-ui-inset", inset);
|
|
618
|
+
this.#restoreProperty("--ag-ui-launcher-inset", launcherInset);
|
|
619
|
+
if (corner === null) {
|
|
620
|
+
this.#host.element.removeAttribute("data-expand-corner");
|
|
621
|
+
} else {
|
|
622
|
+
this.#host.element.setAttribute("data-expand-corner", corner);
|
|
623
|
+
}
|
|
624
|
+
this.#launcherPos = launcherPos;
|
|
625
|
+
this.#panelPos = panelPos;
|
|
626
|
+
this.#expandCorner = expandCorner;
|
|
423
627
|
} else {
|
|
424
|
-
this.#
|
|
628
|
+
this.#forgetCornerPosition();
|
|
425
629
|
}
|
|
630
|
+
// Against the viewport there is now, which may be a phone's: the insets
|
|
631
|
+
// put back above were written for the one the move was made in.
|
|
632
|
+
this.restoreLauncherPosition();
|
|
426
633
|
this.syncResizeAnchor();
|
|
427
634
|
};
|
|
428
635
|
}
|
|
429
636
|
|
|
637
|
+
/** Which of the three kinds of surface decides where the panel stands. */
|
|
638
|
+
#positionOwner(): "dock" | "corner" | "placement" {
|
|
639
|
+
if (this.#docked()) {
|
|
640
|
+
return "dock";
|
|
641
|
+
}
|
|
642
|
+
return this.#surface().launcherDraggable ? "corner" : "placement";
|
|
643
|
+
}
|
|
644
|
+
|
|
430
645
|
/**
|
|
431
646
|
* Which axes the current placement allows.
|
|
432
647
|
*
|
|
@@ -518,6 +733,19 @@ export class PanelPlacement {
|
|
|
518
733
|
this.#anchor = anchor;
|
|
519
734
|
this.#host.element.setAttribute("data-resize-anchor", `${anchor.y}-${anchor.x}`);
|
|
520
735
|
this.#focusableGrip();
|
|
736
|
+
this.#stampHeader();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Tell the stylesheet whether the panel is the screen, for the header's cursor.
|
|
741
|
+
*
|
|
742
|
+
* Only this half of what refuses a header drag: the resting rules already answer
|
|
743
|
+
* for the placement and for data-launcher-drag="false", and neither of those
|
|
744
|
+
* re-applies anything when it changes, so a stamp taking them in would go on
|
|
745
|
+
* refusing, in the cursor, a drag the header would take.
|
|
746
|
+
*/
|
|
747
|
+
#stampHeader(): void {
|
|
748
|
+
this.#header?.toggleAttribute("data-fills-screen", this.#fillsScreen());
|
|
521
749
|
}
|
|
522
750
|
|
|
523
751
|
/** Put a custom property back to a previous value, or remove it if there was none. */
|
|
@@ -583,7 +811,10 @@ export class PanelPlacement {
|
|
|
583
811
|
return surfaceFor(element.getAttribute("placement"), element.getAttribute("collapse-to"));
|
|
584
812
|
}
|
|
585
813
|
|
|
586
|
-
/**
|
|
814
|
+
/**
|
|
815
|
+
* Whether the panel rises from the bottom bar or the host's anchor, until the
|
|
816
|
+
* user drags it somewhere else.
|
|
817
|
+
*/
|
|
587
818
|
#docked(): boolean {
|
|
588
819
|
const target = this.#surface().collapseTo;
|
|
589
820
|
return target === "bar" || target === "none";
|
|
@@ -617,9 +848,121 @@ export class PanelPlacement {
|
|
|
617
848
|
element.style.removeProperty(DOCK_RIGHT);
|
|
618
849
|
}
|
|
619
850
|
element.toggleAttribute("data-dock-anchored", target === "none" && anchor !== null);
|
|
851
|
+
this.#dockTop =
|
|
852
|
+
target === "bar" && height > 0
|
|
853
|
+
? bar.getBoundingClientRect().top
|
|
854
|
+
: target === "none" && anchor !== null
|
|
855
|
+
? anchor.getBoundingClientRect().top
|
|
856
|
+
: Number.POSITIVE_INFINITY;
|
|
857
|
+
// Every change the dock can make passes through here -- the bar's height
|
|
858
|
+
// arrives by a ResizeObserver after the first restore, and a bar that grew
|
|
859
|
+
// since the drag would otherwise cover the panel's foot -- so the dragged
|
|
860
|
+
// spot is held against it here rather than only when it is restored.
|
|
861
|
+
this.#placeDocked();
|
|
620
862
|
return height;
|
|
621
863
|
}
|
|
622
864
|
|
|
865
|
+
/**
|
|
866
|
+
* The part of the usable viewport a docked panel may stand in: all of it
|
|
867
|
+
* above the dock's line. The bar is what it minimises to, and the anchor is
|
|
868
|
+
* the host's own way back, so a dragged panel never covers either.
|
|
869
|
+
*/
|
|
870
|
+
#dockRoom(): ViewportBox {
|
|
871
|
+
const viewport = this.#viewport();
|
|
872
|
+
const bottom = Math.min(viewport.top + viewport.height, this.#dockTop);
|
|
873
|
+
return { ...viewport, height: Math.max(0, bottom - viewport.top) };
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Put a docked panel back where the user dragged it, held inside its room.
|
|
878
|
+
*
|
|
879
|
+
* The inline inset comes off first, which is both how the panel's resting
|
|
880
|
+
* box is measured and what leaves a phone full-bleed: the small-viewport
|
|
881
|
+
* block reads the host's inset before its own, so a dragged spot written
|
|
882
|
+
* there would hold the panel off the screen's edges. The spot itself stays,
|
|
883
|
+
* for the next time there is room for it.
|
|
884
|
+
*/
|
|
885
|
+
#placeDocked(): void {
|
|
886
|
+
if (!this.#docked() || !this.#panelMovable() || this.dragging()) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const at = this.#dockPos ?? this.#readDockPosition();
|
|
890
|
+
if (at === null) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
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())) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
// Measured at the dock, so the inset below is written from the corner the
|
|
900
|
+
// dock holds still and a resize grows the panel away from it.
|
|
901
|
+
this.syncResizeAnchor();
|
|
902
|
+
this.#writeDockInset(
|
|
903
|
+
clampPanel(
|
|
904
|
+
{ left: at.left, top: at.top, right: at.left + rest.width, bottom: at.top + rest.height },
|
|
905
|
+
this.#dockRoom(),
|
|
906
|
+
SCREEN_EDGE_MARGIN,
|
|
907
|
+
),
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/** Write the inset that stands a docked panel at `box`. */
|
|
912
|
+
#writeDockInset(box: PanelRect): void {
|
|
913
|
+
this.#host.element.style.setProperty(
|
|
914
|
+
"--ag-ui-inset",
|
|
915
|
+
insetFrom(this.#anchor, box, this.#screen()),
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Remember a docked panel's dragged spot, or forget it for null. */
|
|
920
|
+
#storeDockPosition(at: { readonly left: number; readonly top: number } | null): void {
|
|
921
|
+
if (at === null) {
|
|
922
|
+
this.#host.clearPreference(DOCK_KEY);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
this.#host.writePreference(DOCK_KEY, JSON.stringify({ left: at.left, top: at.top }));
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** The stored spot of a docked panel, or null for none or anything corrupt. */
|
|
929
|
+
#readDockPosition(): { readonly left: number; readonly top: number } | null {
|
|
930
|
+
const raw = this.#host.readPreference(DOCK_KEY);
|
|
931
|
+
if (raw === null) {
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
try {
|
|
935
|
+
return asPoint(JSON.parse(raw));
|
|
936
|
+
} catch {
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Whether the header drag and moveTo may move the open panel: the
|
|
943
|
+
* placement's answer, unless the host turned moving off.
|
|
944
|
+
*/
|
|
945
|
+
#panelMovable(): boolean {
|
|
946
|
+
return (
|
|
947
|
+
this.#host.element.getAttribute("data-launcher-drag") !== "false" &&
|
|
948
|
+
this.#surface().panelMovable
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Whether a header drag may start now. A panel that is the screen, on a phone
|
|
954
|
+
* or by a host's own sizing, has nowhere to go, and a drag there would pull it
|
|
955
|
+
* off the edges it fills.
|
|
956
|
+
*/
|
|
957
|
+
#canDragPanel(): boolean {
|
|
958
|
+
return this.#panelMovable() && !this.#fillsScreen();
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/** Whether the panel covers the usable viewport, as it stands now. */
|
|
962
|
+
#fillsScreen(): boolean {
|
|
963
|
+
return coversViewport(this.#host.element.getBoundingClientRect(), this.#viewport());
|
|
964
|
+
}
|
|
965
|
+
|
|
623
966
|
/**
|
|
624
967
|
* Whether this placement lets the launcher be dragged, read per interaction
|
|
625
968
|
* because `placement` is a live attribute. `data-launcher-drag="false"` opts
|
|
@@ -806,12 +1149,6 @@ export class PanelPlacement {
|
|
|
806
1149
|
* back rather than leaving a stale inline one behind.
|
|
807
1150
|
*/
|
|
808
1151
|
#applyLauncherPlacement(at: { readonly left: number; readonly top: number }): void {
|
|
809
|
-
// The single gate: callers hand over a position and this decides whether
|
|
810
|
-
// it is this element's to honour. Checking in both places instead would
|
|
811
|
-
// leave one of the two checks permanently unreachable.
|
|
812
|
-
if (!this.#launcherDraggable()) {
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
1152
|
this.#launcherPos = at;
|
|
816
1153
|
// Dropping the bubble hands the panel's position back to the placement.
|
|
817
1154
|
// Keeping a stated one would pin the panel where it was dragged and leave
|
|
@@ -839,6 +1176,9 @@ export class PanelPlacement {
|
|
|
839
1176
|
|
|
840
1177
|
/** Move the launcher live during a drag, without persisting. */
|
|
841
1178
|
#moveLauncher(left: number, top: number): void {
|
|
1179
|
+
// Stated from the first move, not the last: a key gesture only ends at its
|
|
1180
|
+
// keyup, and an Enter pressed before then expands by way of a restore.
|
|
1181
|
+
this.#cornerSpot = { left, top };
|
|
842
1182
|
this.#applyLauncherPlacement({ left, top });
|
|
843
1183
|
}
|
|
844
1184
|
|
|
@@ -863,6 +1203,13 @@ export class PanelPlacement {
|
|
|
863
1203
|
* other still names the old one would move the launcher for no reason.
|
|
864
1204
|
*/
|
|
865
1205
|
#movePanel(box: PanelRect, from: PanelRect): { held: PanelRect; launcher: LauncherBox | null } {
|
|
1206
|
+
if (this.#docked()) {
|
|
1207
|
+
// Inside the room above the dock, with nothing carried along.
|
|
1208
|
+
const held = clampPanel(box, this.#dockRoom(), SCREEN_EDGE_MARGIN);
|
|
1209
|
+
this.#dockPos = { left: held.left, top: held.top };
|
|
1210
|
+
this.#writeDockInset(held);
|
|
1211
|
+
return { held, launcher: null };
|
|
1212
|
+
}
|
|
866
1213
|
if (!this.#launcherDraggable()) {
|
|
867
1214
|
return { held: box, launcher: null };
|
|
868
1215
|
}
|
|
@@ -936,6 +1283,10 @@ export class PanelPlacement {
|
|
|
936
1283
|
// again. Recomputing it is how the two came apart: releasing the drag
|
|
937
1284
|
// moved the bubble by the panel's whole travel a second time.
|
|
938
1285
|
const { held, launcher } = this.#movePanel(box, from);
|
|
1286
|
+
if (this.#docked()) {
|
|
1287
|
+
this.#storeDockPosition({ left: held.left, top: held.top });
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
939
1290
|
if (launcher === null) {
|
|
940
1291
|
return;
|
|
941
1292
|
}
|
|
@@ -973,9 +1324,6 @@ export class PanelPlacement {
|
|
|
973
1324
|
* its own right.
|
|
974
1325
|
*/
|
|
975
1326
|
#restorePanelPosition(at: { readonly left: number; readonly top: number }): void {
|
|
976
|
-
if (!this.#launcherDraggable()) {
|
|
977
|
-
return;
|
|
978
|
-
}
|
|
979
1327
|
const rect = this.#host.element.getBoundingClientRect();
|
|
980
1328
|
// The same bound the drag itself used. Taking the default here instead is
|
|
981
1329
|
// what made a panel dragged to an edge jump a whole resting gutter inward
|
|
@@ -1006,6 +1354,9 @@ export class PanelPlacement {
|
|
|
1006
1354
|
* with it, the panel goes back where it was put; without it, the panel is
|
|
1007
1355
|
* re-derived from the launcher and opens into whatever room the viewport has
|
|
1008
1356
|
* now.
|
|
1357
|
+
*
|
|
1358
|
+
* Every caller is a gesture ending or a move being made, so where the two
|
|
1359
|
+
* stand now is what was stated. A restore never calls this.
|
|
1009
1360
|
*/
|
|
1010
1361
|
#storeLauncherPosition(): void {
|
|
1011
1362
|
const position = this.#launcherPos;
|
|
@@ -1013,10 +1364,13 @@ export class PanelPlacement {
|
|
|
1013
1364
|
return;
|
|
1014
1365
|
}
|
|
1015
1366
|
const panel = this.#panelPos;
|
|
1016
|
-
this.#
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1367
|
+
this.#writeCornerSpot(panel === null ? position : { ...position, panel });
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/** Make `spot` the stated one, and remember it. */
|
|
1371
|
+
#writeCornerSpot(spot: CornerSpot): void {
|
|
1372
|
+
this.#cornerSpot = spot;
|
|
1373
|
+
this.#host.writePreference(LAUNCHER_KEY, JSON.stringify(spot));
|
|
1020
1374
|
}
|
|
1021
1375
|
|
|
1022
1376
|
/**
|
|
@@ -1024,34 +1378,149 @@ export class PanelPlacement {
|
|
|
1024
1378
|
* whenever the window resizes, and before an expand. The viewport that
|
|
1025
1379
|
* stored a position may since have shrunk, and a launcher past the edge is
|
|
1026
1380
|
* unreachable -- it is the only way back to a collapsed conversation.
|
|
1381
|
+
*
|
|
1382
|
+
* The one gate on whether a stored position is this element's to honour: the
|
|
1383
|
+
* gestures check their own before they ever reach the placing below.
|
|
1027
1384
|
*/
|
|
1028
1385
|
restoreLauncherPosition(): void {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1386
|
+
this.#placeFromRecord(true);
|
|
1387
|
+
this.#stampHeader();
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* Re-apply it for a change in the panel's own layout, which needs no window or
|
|
1392
|
+
* reopen to happen: a line typed in the composer is one. A position waiting to
|
|
1393
|
+
* be handed back under data-launcher-drag="false" waits on, because a panel that leaves for the
|
|
1394
|
+
* host's corner as the reader starts typing is a jump with no cause they can see.
|
|
1395
|
+
*/
|
|
1396
|
+
settleLauncherPosition(): void {
|
|
1397
|
+
this.#placeFromRecord(false);
|
|
1398
|
+
this.#stampHeader();
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/** The placing behind {@link restoreLauncherPosition}. */
|
|
1402
|
+
#placeFromRecord(handBack: boolean): void {
|
|
1403
|
+
// Before the launcher's record is read at all: it belongs to the corner
|
|
1404
|
+
// surface, and seeding anything from it would put a spot dragged there on
|
|
1405
|
+
// a panel that should stand on its dock.
|
|
1406
|
+
if (this.#docked()) {
|
|
1407
|
+
this.#placeDocked();
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (!this.#surface().launcherDraggable) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
// Under data-launcher-drag="false" the widget is wherever the host's CSS puts
|
|
1414
|
+
// it, so a position this element took before the attribute went on is handed
|
|
1415
|
+
// back here, at the first window resize or reopen, and the record stays for the
|
|
1416
|
+
// day the attribute comes off. Nothing watches the attribute: it is read per
|
|
1417
|
+
// interaction, like every other answer here.
|
|
1418
|
+
const draggable = this.#launcherDraggable();
|
|
1419
|
+
if (!draggable && this.#launcherPos !== null) {
|
|
1420
|
+
if (!handBack) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
this.releaseLauncherPosition();
|
|
1424
|
+
}
|
|
1425
|
+
const spot = draggable ? (this.#cornerSpot ?? this.#readLauncherPosition()) : null;
|
|
1426
|
+
if (spot === null) {
|
|
1427
|
+
this.#placeUnstated();
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (this.#standFullBleed(spot)) {
|
|
1032
1431
|
return;
|
|
1033
1432
|
}
|
|
1034
|
-
|
|
1035
|
-
if (panel !== null) {
|
|
1433
|
+
if (spot.panel !== undefined) {
|
|
1036
1434
|
// Stated rather than derived: the launcher is only read here to keep the
|
|
1037
1435
|
// offset the two were left with, so it has to be seeded before the panel
|
|
1038
1436
|
// is placed around it.
|
|
1039
|
-
this.#launcherPos = { left:
|
|
1040
|
-
this.#restorePanelPosition(panel);
|
|
1437
|
+
this.#launcherPos = { left: spot.left, top: spot.top };
|
|
1438
|
+
this.#restorePanelPosition(spot.panel);
|
|
1041
1439
|
return;
|
|
1042
1440
|
}
|
|
1043
1441
|
const box = this.#launcherBox();
|
|
1044
1442
|
this.#applyLauncherPlacement(
|
|
1045
|
-
clampLauncher({ ...box, left:
|
|
1443
|
+
clampLauncher({ ...box, left: spot.left, top: spot.top }, this.#viewport()),
|
|
1046
1444
|
);
|
|
1047
1445
|
}
|
|
1048
1446
|
|
|
1447
|
+
/**
|
|
1448
|
+
* Leave a corner panel full-bleed if it has become the screen, and say whether
|
|
1449
|
+
* it has.
|
|
1450
|
+
*
|
|
1451
|
+
* The inline inset comes off, because the small-viewport block reads the
|
|
1452
|
+
* host's inset before its own: a spot written there holds the panel off the
|
|
1453
|
+
* edges it should fill, and past them by the same amount, since its size is
|
|
1454
|
+
* the screen's. What the panel covers is read before that, from the size
|
|
1455
|
+
* alone, which no inset changes.
|
|
1456
|
+
*
|
|
1457
|
+
* The launcher stays a place of its own. It is positioned from the host box,
|
|
1458
|
+
* so it is measured again from the box that has just moved, and it is held on
|
|
1459
|
+
* the screen, because it is the way back from a collapsed conversation. The
|
|
1460
|
+
* spot itself is untouched, for the next time there is room for it.
|
|
1461
|
+
*
|
|
1462
|
+
* Above the breakpoint nothing in the stylesheet takes the panel to the edges.
|
|
1463
|
+
* A host's own size makes it the screen while the resting inset still spends a
|
|
1464
|
+
* gutter on one corner, which hangs the box off the opposite one, so there it
|
|
1465
|
+
* stands at the start of the room the host left free and keeps that inset. A
|
|
1466
|
+
* box the placement already holds on the screen has no inset to keep, which is
|
|
1467
|
+
* every phone, and a host's rule that placed the box itself.
|
|
1468
|
+
*/
|
|
1469
|
+
#standFullBleed(spot: CornerSpot): boolean {
|
|
1470
|
+
const element = this.#host.element;
|
|
1471
|
+
const viewport = this.#viewport();
|
|
1472
|
+
if (!coversViewport(element.getBoundingClientRect(), viewport)) {
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
element.style.removeProperty("--ag-ui-inset");
|
|
1476
|
+
const rest = element.getBoundingClientRect();
|
|
1477
|
+
const hanging = hangsOff(rest, viewport, this.#screen());
|
|
1478
|
+
const box = hanging ? atStartOf(viewport, rest) : rest;
|
|
1479
|
+
const launcher = { ...this.#launcherBox(), left: spot.left, top: spot.top };
|
|
1480
|
+
this.#placePanelAndLauncher(box, { ...launcher, ...clampLauncher(launcher, viewport) });
|
|
1481
|
+
if (!hanging) {
|
|
1482
|
+
element.style.removeProperty("--ag-ui-inset");
|
|
1483
|
+
}
|
|
1484
|
+
return true;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
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.
|
|
1490
|
+
*
|
|
1491
|
+
* Only an inset this element wrote comes off or goes back. One the host wrote
|
|
1492
|
+
* inline is a token a host may state on a phone as anywhere else, and with no
|
|
1493
|
+
* spot it never came from here.
|
|
1494
|
+
*/
|
|
1495
|
+
#placeUnstated(): void {
|
|
1496
|
+
const element = this.#host.element;
|
|
1497
|
+
const inline = element.style.getPropertyValue("--ag-ui-inset");
|
|
1498
|
+
const viewport = this.#viewport();
|
|
1499
|
+
if (!coversViewport(element.getBoundingClientRect(), viewport)) {
|
|
1500
|
+
if (inline === "" || inline === this.#fillInset) {
|
|
1501
|
+
this.#restoreProperty("--ag-ui-inset", this.#gripInset ?? "");
|
|
1502
|
+
this.syncResizeAnchor();
|
|
1503
|
+
}
|
|
1504
|
+
this.#fillInset = null;
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
if (inline === this.#gripInset || inline === this.#fillInset) {
|
|
1508
|
+
element.style.removeProperty("--ag-ui-inset");
|
|
1509
|
+
}
|
|
1510
|
+
const rest = element.getBoundingClientRect();
|
|
1511
|
+
const screen = this.#screen();
|
|
1512
|
+
if (
|
|
1513
|
+
element.style.getPropertyValue("--ag-ui-inset") === "" &&
|
|
1514
|
+
hangsOff(rest, viewport, screen)
|
|
1515
|
+
) {
|
|
1516
|
+
this.#fillInset = insetFrom({ x: "left", y: "top" }, atStartOf(viewport, rest), screen);
|
|
1517
|
+
element.style.setProperty("--ag-ui-inset", this.#fillInset);
|
|
1518
|
+
this.syncResizeAnchor();
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1049
1522
|
/** The persisted position for this instance, or null. */
|
|
1050
|
-
#readLauncherPosition(): {
|
|
1051
|
-
readonly left: number;
|
|
1052
|
-
readonly top: number;
|
|
1053
|
-
readonly panel?: { readonly left: number; readonly top: number };
|
|
1054
|
-
} | null {
|
|
1523
|
+
#readLauncherPosition(): CornerSpot | null {
|
|
1055
1524
|
const raw = this.#host.readPreference(LAUNCHER_KEY);
|
|
1056
1525
|
if (raw === null) {
|
|
1057
1526
|
return null;
|
|
@@ -1083,12 +1552,26 @@ export class PanelPlacement {
|
|
|
1083
1552
|
* the switch inline and pins a sidebar to wherever the floating launcher was.
|
|
1084
1553
|
*/
|
|
1085
1554
|
releaseLauncherPosition(): void {
|
|
1555
|
+
// A dragged dock spot belongs to a docked panel that moves. Any other
|
|
1556
|
+
// surface hands it back; a docked one keeps it, and syncDock holds it
|
|
1557
|
+
// against the new dock.
|
|
1558
|
+
if (this.#dockPos !== null && !(this.#docked() && this.#panelMovable())) {
|
|
1559
|
+
this.#dockPos = null;
|
|
1560
|
+
this.#host.element.style.removeProperty("--ag-ui-inset");
|
|
1561
|
+
}
|
|
1086
1562
|
if (this.#launcherDraggable()) {
|
|
1087
1563
|
return;
|
|
1088
1564
|
}
|
|
1565
|
+
this.#forgetCornerPosition();
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/** Take everything off that placed a corner panel, leaving its record alone. */
|
|
1569
|
+
#forgetCornerPosition(): void {
|
|
1089
1570
|
this.#launcherPos = null;
|
|
1090
1571
|
this.#panelPos = null;
|
|
1091
1572
|
this.#expandCorner = null;
|
|
1573
|
+
this.#gripInset = null;
|
|
1574
|
+
this.#fillInset = null;
|
|
1092
1575
|
this.#host.element.style.removeProperty("--ag-ui-inset");
|
|
1093
1576
|
this.#host.element.style.removeProperty("--ag-ui-launcher-inset");
|
|
1094
1577
|
this.#host.element.removeAttribute("data-expand-corner");
|
|
@@ -1110,11 +1593,12 @@ export class PanelPlacement {
|
|
|
1110
1593
|
*/
|
|
1111
1594
|
#applyResize(grip: ResizeGrip, box: PanelRect): PanelRect {
|
|
1112
1595
|
box = this.#withinViewport(box);
|
|
1113
|
-
if (this.#docked()) {
|
|
1114
|
-
// A
|
|
1596
|
+
if (this.#docked() && this.#dockPos === null) {
|
|
1597
|
+
// A panel standing on its dock keeps it through a resize: the edges it
|
|
1115
1598
|
// stands on are the bar's or the anchor's, and a grip on one of them
|
|
1116
1599
|
// would write the position below and take the panel off its dock. So
|
|
1117
1600
|
// the pinned edges stay where they are and only the free ones follow.
|
|
1601
|
+
// Once dragged off, it resizes like a corner panel, below.
|
|
1118
1602
|
const now = this.#host.element.getBoundingClientRect();
|
|
1119
1603
|
box = {
|
|
1120
1604
|
left: grip.x === this.#anchor.x ? now.left : box.left,
|
|
@@ -1126,28 +1610,38 @@ export class PanelPlacement {
|
|
|
1126
1610
|
return box;
|
|
1127
1611
|
}
|
|
1128
1612
|
this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
|
|
1129
|
-
if (
|
|
1130
|
-
|
|
1613
|
+
if (this.#docked()) {
|
|
1614
|
+
// Any grip moves the top-left corner the spot is kept as, including one
|
|
1615
|
+
// on a free edge, which leaves the inset alone.
|
|
1616
|
+
this.#dockPos = { left: box.left, top: box.top };
|
|
1131
1617
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1618
|
+
if (grip.x === this.#anchor.x || grip.y === this.#anchor.y) {
|
|
1619
|
+
// The whole screen, not the usable box: a CSS inset on a fixed element is
|
|
1620
|
+
// measured from the real edges, so expressing a right or bottom against a
|
|
1621
|
+
// box the host has inset comes out short by exactly that inset.
|
|
1622
|
+
const anchor = this.#anchor;
|
|
1623
|
+
const inset = insetFrom(anchor, box, this.#screen());
|
|
1624
|
+
this.#host.element.style.setProperty("--ag-ui-inset", inset);
|
|
1625
|
+
this.#gripInset = this.#launcherPos === null && !this.#docked() ? inset : null;
|
|
1626
|
+
// The launcher lives at this corner of the panel, so a corner that moved
|
|
1627
|
+
// takes it along. Without this the next expand would re-derive the panel's
|
|
1628
|
+
// position from a launcher still standing where the panel used to be, and
|
|
1629
|
+
// undo the move.
|
|
1630
|
+
if (this.#launcherPos !== null) {
|
|
1631
|
+
const size = this.#host.launcher.offsetWidth;
|
|
1632
|
+
this.#launcherPos = {
|
|
1633
|
+
left: anchor.x === "left" ? box.left : box.right - size,
|
|
1634
|
+
top: anchor.y === "top" ? box.top : box.bottom - size,
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1147
1637
|
}
|
|
1148
|
-
// A stated panel
|
|
1149
|
-
|
|
1150
|
-
|
|
1638
|
+
// A stated corner panel's spot is a claim about this box, so it moves with
|
|
1639
|
+
// it, by a free edge as well as by a pinned one. Read from what was drawn,
|
|
1640
|
+
// not what the pointer asked for, because a host's maximum stops the box short
|
|
1641
|
+
// of the pointer. Not while docked: this position belongs to the corner.
|
|
1642
|
+
if (this.#panelPos !== null && !this.#docked()) {
|
|
1643
|
+
const drawn = this.#host.element.getBoundingClientRect();
|
|
1644
|
+
this.#panelPos = { left: drawn.left, top: drawn.top };
|
|
1151
1645
|
}
|
|
1152
1646
|
return box;
|
|
1153
1647
|
}
|
|
@@ -1159,8 +1653,19 @@ export class PanelPlacement {
|
|
|
1159
1653
|
// mount, which is the same disagreement between apply and commit that made
|
|
1160
1654
|
// the header drag jump on release.
|
|
1161
1655
|
const held = this.#applyResize(grip, box);
|
|
1656
|
+
// A grip on the free edges of a panel standing at the start of a filled window
|
|
1657
|
+
// leaves that inset alone, and the size it stated takes the panel off the
|
|
1658
|
+
// screen's shape. It has to stay where the grip left it, so the inset stops
|
|
1659
|
+
// being one that lasts only as long as the host's size does.
|
|
1660
|
+
if (this.#host.element.style.getPropertyValue("--ag-ui-inset") === this.#fillInset) {
|
|
1661
|
+
this.#gripInset = this.#fillInset;
|
|
1662
|
+
this.#fillInset = null;
|
|
1663
|
+
}
|
|
1162
1664
|
this.#persistSize({ width: held.right - held.left, height: held.bottom - held.top });
|
|
1163
1665
|
this.#storeLauncherPosition();
|
|
1666
|
+
if (this.#dockPos !== null) {
|
|
1667
|
+
this.#storeDockPosition(this.#dockPos);
|
|
1668
|
+
}
|
|
1164
1669
|
// Re-stamp after the drag: a host whose layout changed underneath us would
|
|
1165
1670
|
// otherwise keep the tab-reachable grip in the old corner, which reads as
|
|
1166
1671
|
// the control being in the wrong place even though the drag was right.
|