@artooi/ag-ui-web-component 0.47.0 → 0.49.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 +205 -1
- package/README.md +235 -16
- package/dist/ag-ui-web-component.bundle.js +647 -181
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +27 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +44 -5
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/events/bar_height_detail.d.ts +6 -0
- package/dist/core/events/bar_height_detail.d.ts.map +1 -0
- package/dist/dom/highlight_overlay.d.ts +2 -1
- package/dist/dom/highlight_overlay.d.ts.map +1 -1
- package/dist/dom/ring_color.d.ts +8 -0
- package/dist/dom/ring_color.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1184 -69
- package/dist/index.js.map +4 -4
- package/dist/tools/chat_surface_tools.d.ts +8 -1
- package/dist/tools/chat_surface_tools.d.ts.map +1 -1
- package/dist/ui/history/conversation_history.d.ts +16 -3
- package/dist/ui/history/conversation_history.d.ts.map +1 -1
- package/dist/ui/placement/collapse_styles.d.ts.map +1 -1
- package/dist/ui/placement/dock_styles.d.ts +2 -0
- package/dist/ui/placement/dock_styles.d.ts.map +1 -0
- package/dist/ui/placement/panel_placement.d.ts +16 -0
- package/dist/ui/placement/panel_placement.d.ts.map +1 -1
- package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -1
- package/dist/ui/placement/surface_for.d.ts +32 -19
- 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/progress/tool_step_styles.d.ts.map +1 -1
- package/dist/ui/shell/build_launcher.d.ts +15 -4
- package/dist/ui/shell/build_launcher.d.ts.map +1 -1
- package/dist/ui/shell/token_styles.d.ts.map +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/themes/quiet_styles.d.ts.map +1 -1
- package/dist/ui/themes/scheme_styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +30 -2
- package/src/core/ag_ui_chat.ts +335 -28
- package/src/core/events/bar_height_detail.ts +5 -0
- package/src/dom/highlight_overlay.ts +2 -1
- package/src/dom/ring_color.ts +141 -4
- package/src/index.ts +4 -0
- package/src/tools/chat_surface_tools.ts +13 -5
- package/src/ui/history/conversation_history.ts +51 -7
- package/src/ui/placement/collapse_styles.ts +248 -2
- package/src/ui/placement/dock_styles.ts +95 -0
- package/src/ui/placement/panel_placement.ts +300 -16
- package/src/ui/placement/small_viewport_styles.ts +9 -0
- package/src/ui/placement/surface_for.ts +58 -22
- package/src/ui/progress/tool_group_styles.ts +52 -2
- package/src/ui/progress/tool_step_styles.ts +5 -3
- package/src/ui/shell/build_launcher.ts +32 -5
- package/src/ui/shell/token_styles.ts +58 -1
- package/src/ui/styles.ts +4 -0
- package/src/ui/themes/quiet_styles.ts +19 -3
- package/src/ui/themes/scheme_styles.ts +10 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Where a docked panel stands: above the bottom bar under collapse-to="bar",
|
|
2
|
+
// above the host's anchor under collapse-to="none". Token restatements only;
|
|
3
|
+
// the bar's own shape is in the collapse module, after the launcher it
|
|
4
|
+
// restyles.
|
|
5
|
+
export const DOCK_STYLES: string = `
|
|
6
|
+
/* ── Dock ────────────────────────────────────────────────────────────────────
|
|
7
|
+
A docked panel rises from the bar along the bottom edge, or from the host's
|
|
8
|
+
own control, instead of resting in a corner of its own. The element stamps
|
|
9
|
+
what it collapses to as data-collapsed-to, and writes where the dock is as
|
|
10
|
+
two private properties: --_dock-bottom, the bar's height or the distance
|
|
11
|
+
from the layout viewport's bottom to the anchor's top, and --_dock-right,
|
|
12
|
+
the anchor's distance from the right edge. Both are read with a 0px
|
|
13
|
+
fallback, so a panel measured before the bar has laid out stands where a
|
|
14
|
+
bar of no height would put it.
|
|
15
|
+
|
|
16
|
+
The cap follows the token block's rule: what the host left free less the
|
|
17
|
+
one gutter the anchored edge spends, so a panel grown to the cap runs from
|
|
18
|
+
just above the bar to the far edge. A host therefore needs no inset to keep
|
|
19
|
+
the panel off its bar; --ag-ui-inset still wins when set. A panel the user
|
|
20
|
+
dragged off its dock is written there inline, which is how the dragged spot
|
|
21
|
+
outranks every rule below, the anchor gap included; the element holds that
|
|
22
|
+
spot above the bar or the anchor itself, and drops it on a phone, where the
|
|
23
|
+
small-viewport block reads the same property first.
|
|
24
|
+
|
|
25
|
+
These blocks restate tokens and nothing else, and sit ahead of the phone
|
|
26
|
+
block at no more weight than it has, so on a phone the panel is the screen
|
|
27
|
+
as every other placement is there. The bar measures 0 while it is not drawn
|
|
28
|
+
(a phone with the panel open), which is what lets the side and full shapes
|
|
29
|
+
below fall back to the whole height without a phone rule of their own.
|
|
30
|
+
|
|
31
|
+
Each placement keeps its own side and shape; only the bottom edge moves. */
|
|
32
|
+
:host([data-collapsed-to="bar"]) {
|
|
33
|
+
--_inset: var(
|
|
34
|
+
--ag-ui-inset,
|
|
35
|
+
auto calc(var(--_edge-gutter) + var(--_viewport-inset-right))
|
|
36
|
+
calc(
|
|
37
|
+
var(--_edge-gutter) + var(--_viewport-inset-bottom) +
|
|
38
|
+
max(var(--_dock-bottom, 0px), var(--_keyboard-inset))
|
|
39
|
+
)
|
|
40
|
+
auto
|
|
41
|
+
);
|
|
42
|
+
--_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - var(--_edge-gutter)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
:host([data-collapsed-to="bar"][placement="bottom-left"]) {
|
|
46
|
+
--_inset: var(
|
|
47
|
+
--ag-ui-inset,
|
|
48
|
+
auto auto
|
|
49
|
+
calc(
|
|
50
|
+
var(--_edge-gutter) + var(--_viewport-inset-bottom) +
|
|
51
|
+
max(var(--_dock-bottom, 0px), var(--_keyboard-inset))
|
|
52
|
+
)
|
|
53
|
+
calc(var(--_edge-gutter) + var(--_viewport-inset-left))
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* The column and the full-screen panel stop at the bar rather than covering
|
|
58
|
+
it, so the bar stays where the user minimises from. */
|
|
59
|
+
:host([data-collapsed-to="bar"][placement="side"]) {
|
|
60
|
+
--_inset: var(
|
|
61
|
+
--ag-ui-inset,
|
|
62
|
+
calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
|
|
63
|
+
calc(var(--_viewport-inset-bottom) + var(--_dock-bottom, 0px)) auto
|
|
64
|
+
);
|
|
65
|
+
--_height: var(--ag-ui-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
|
|
66
|
+
--_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
:host([data-collapsed-to="bar"][placement="full"]) {
|
|
70
|
+
--_inset: var(
|
|
71
|
+
--ag-ui-inset,
|
|
72
|
+
calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
|
|
73
|
+
calc(var(--_viewport-inset-bottom) + var(--_dock-bottom, 0px)) var(--_viewport-inset-left)
|
|
74
|
+
);
|
|
75
|
+
--_height: var(--ag-ui-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
|
|
76
|
+
--_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* The host's own bar: the panel stands the anchor gap (8px unless the host
|
|
80
|
+
says otherwise) above the anchor and lines up with its right edge. Only
|
|
81
|
+
once the element found the anchor; until then, and when it never does, the
|
|
82
|
+
placement's own corner stands.
|
|
83
|
+
|
|
84
|
+
The cap spends the larger of the gap and the gutter. The gutter alone is
|
|
85
|
+
what this always spent, so the default is unchanged; but a host that opens
|
|
86
|
+
the gap past the gutter to clear a tall toolbar would otherwise push a
|
|
87
|
+
panel grown to the cap off the top of the window by the difference. */
|
|
88
|
+
:host([data-collapsed-to="none"][data-dock-anchored]) {
|
|
89
|
+
--_inset: var(--ag-ui-inset, auto var(--_dock-right, 0px) calc(var(--_dock-bottom, 0px) + var(--_anchor-gap)) auto);
|
|
90
|
+
--_max-height: var(
|
|
91
|
+
--ag-ui-max-height,
|
|
92
|
+
calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - max(var(--_edge-gutter), var(--_anchor-gap)))
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type ResizeGrip,
|
|
23
23
|
type ResizeSize,
|
|
24
24
|
} from "./resize_handle.js";
|
|
25
|
-
import { surfaceFor } from "./surface_for.js";
|
|
25
|
+
import { type Surface, surfaceFor } from "./surface_for.js";
|
|
26
26
|
|
|
27
27
|
/** Per-tab persistence key for a dragged panel size. */
|
|
28
28
|
const SIZE_KEY = "ag-ui-chat:size";
|
|
@@ -37,10 +37,28 @@ const SIZE_KEY = "ag-ui-chat:size";
|
|
|
37
37
|
*/
|
|
38
38
|
const DRAG_WIDTH = "--_drag-width";
|
|
39
39
|
const DRAG_HEIGHT = "--_drag-height";
|
|
40
|
+
/**
|
|
41
|
+
* How far above the bottom of the layout viewport a docked panel stands, and
|
|
42
|
+
* how far in from its right edge: the bar's height, or the host anchor's top
|
|
43
|
+
* and right. Private and written inline, like the dragged size, and read only
|
|
44
|
+
* by the dock rules; never an inline --_inset, which would outrank the phone
|
|
45
|
+
* block and keep a docked panel off full-bleed on a phone.
|
|
46
|
+
*/
|
|
47
|
+
const DOCK_BOTTOM = "--_dock-bottom";
|
|
48
|
+
const DOCK_RIGHT = "--_dock-right";
|
|
40
49
|
|
|
41
50
|
/** Per-tab persistence key for a dragged launcher position. */
|
|
42
51
|
const LAUNCHER_KEY = "ag-ui-chat:launcher";
|
|
43
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
|
+
|
|
44
62
|
/** A stored `{ left, top }` pair, or null for anything that is not one. */
|
|
45
63
|
function asPoint(value: unknown): { readonly left: number; readonly top: number } | null {
|
|
46
64
|
if (typeof value !== "object" || value === null) {
|
|
@@ -148,7 +166,7 @@ export class PanelPlacement {
|
|
|
148
166
|
*/
|
|
149
167
|
enablePanelDrag(header: HTMLElement): void {
|
|
150
168
|
enablePanelDrag(header, {
|
|
151
|
-
enabled: () => !this.#host.collapsed() && this.#
|
|
169
|
+
enabled: () => !this.#host.collapsed() && this.#canDragPanel(),
|
|
152
170
|
rect: () => this.#host.element.getBoundingClientRect(),
|
|
153
171
|
apply: (box, from) => this.#movePanel(box, from),
|
|
154
172
|
commit: (box, from) => this.#commitPanel(box, from),
|
|
@@ -204,11 +222,23 @@ export class PanelPlacement {
|
|
|
204
222
|
* Drop the dragged size and its stored copy, so the placement's own size and
|
|
205
223
|
* the host's tokens apply again. For a host entering a sized state of its
|
|
206
224
|
* own, which a user's drag would otherwise outrank.
|
|
225
|
+
*
|
|
226
|
+
* A docked panel the user dragged off its dock goes back to it as well, and
|
|
227
|
+
* the stored spot with it: there the position is part of the same resting
|
|
228
|
+
* state the host is asking for. A corner panel keeps a dragged position, as
|
|
229
|
+
* it always has.
|
|
207
230
|
*/
|
|
208
231
|
resetSize(): void {
|
|
209
232
|
this.#host.element.style.removeProperty(DRAG_WIDTH);
|
|
210
233
|
this.#host.element.style.removeProperty(DRAG_HEIGHT);
|
|
211
234
|
this.#host.clearPreference(SIZE_KEY);
|
|
235
|
+
if (this.#docked()) {
|
|
236
|
+
if (this.#dockPos !== null) {
|
|
237
|
+
this.#host.element.style.removeProperty("--ag-ui-inset");
|
|
238
|
+
}
|
|
239
|
+
this.#dockPos = null;
|
|
240
|
+
this.#storeDockPosition(null);
|
|
241
|
+
}
|
|
212
242
|
// The anchor was measured against the dragged box; the resting one may
|
|
213
243
|
// pin different edges.
|
|
214
244
|
this.syncResizeAnchor();
|
|
@@ -241,6 +271,25 @@ export class PanelPlacement {
|
|
|
241
271
|
*/
|
|
242
272
|
#panelPos: { readonly left: number; readonly top: number } | null = null;
|
|
243
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Where the user dragged a docked panel, in viewport coordinates, or null
|
|
276
|
+
* while it stands on the bar or the anchor.
|
|
277
|
+
*
|
|
278
|
+
* Apart from the two corner positions above because it moves nothing else:
|
|
279
|
+
* a docked panel has no launcher to carry, and the corner it opens from is
|
|
280
|
+
* the dock's. Held as the user stated it and never as clamped, so a bar that
|
|
281
|
+
* grew, or a phone that took the panel full-bleed, gives the spot back
|
|
282
|
+
* unchanged once there is room for it again.
|
|
283
|
+
*/
|
|
284
|
+
#dockPos: { readonly left: number; readonly top: number } | null = null;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The line a docked panel stays above, in viewport coordinates: the bar's
|
|
288
|
+
* top, or the anchor's. Infinite where neither is drawn. Measured by
|
|
289
|
+
* syncDock, which runs on every change either can make.
|
|
290
|
+
*/
|
|
291
|
+
#dockTop = Number.POSITIVE_INFINITY;
|
|
292
|
+
|
|
244
293
|
/**
|
|
245
294
|
* The corner the panel opens away from, once this element is placing itself.
|
|
246
295
|
* Null means the host's layout still decides, and the anchor is measured.
|
|
@@ -272,7 +321,10 @@ export class PanelPlacement {
|
|
|
272
321
|
* rather than collapsed to nothing.
|
|
273
322
|
*/
|
|
274
323
|
#withinViewport(box: PanelRect): PanelRect {
|
|
275
|
-
|
|
324
|
+
// A docked panel that moves has its room stop at the dock, so no grip
|
|
325
|
+
// pulls it over the bar it minimises to. A placement that owns its
|
|
326
|
+
// position keeps the bound it always had.
|
|
327
|
+
const viewport = this.#docked() && this.#panelMovable() ? this.#dockRoom() : this.#viewport();
|
|
276
328
|
// The same bound a drag stops at, so a grip pulled to the edge and a panel
|
|
277
329
|
// dragged to it come to rest on the same line. The inner Math.max/min pair
|
|
278
330
|
// keeps an already-inverted box from turning inside out.
|
|
@@ -309,10 +361,17 @@ export class PanelPlacement {
|
|
|
309
361
|
const fullBleed = coversViewport(box, viewport);
|
|
310
362
|
return {
|
|
311
363
|
placement: this.#host.element.getAttribute("placement"),
|
|
364
|
+
collapseTo: this.#surface().collapseTo,
|
|
312
365
|
collapsed: this.#host.collapsed(),
|
|
313
366
|
collapsible: this.#host.collapsible(),
|
|
314
|
-
movable: this.#
|
|
315
|
-
|
|
367
|
+
movable: this.#panelMovable() && !fullBleed,
|
|
368
|
+
// Whether the page left moving on, which is not the same question as
|
|
369
|
+
// whether this panel moves: a docked sidebar's position is owned by its
|
|
370
|
+
// placement, and reporting that as the host having turned moving off is
|
|
371
|
+
// the sentence the move refusal would then get wrong.
|
|
372
|
+
draggable:
|
|
373
|
+
this.#host.element.getAttribute("data-launcher-drag") !== "false" &&
|
|
374
|
+
(this.#surface().launcherDraggable || this.#docked()),
|
|
316
375
|
fullBleed,
|
|
317
376
|
box: {
|
|
318
377
|
left: Math.round(box.left),
|
|
@@ -331,7 +390,7 @@ export class PanelPlacement {
|
|
|
331
390
|
|
|
332
391
|
/** The move behind `AgUiChat.moveTo`, whose doc is the contract. */
|
|
333
392
|
moveTo(corner: ChatCorner, options: { readonly announce?: boolean } = {}): boolean {
|
|
334
|
-
if (!this.#
|
|
393
|
+
if (!this.#panelMovable()) {
|
|
335
394
|
return false;
|
|
336
395
|
}
|
|
337
396
|
const restore = options.announce === true ? this.#captureGeometry() : null;
|
|
@@ -341,23 +400,31 @@ export class PanelPlacement {
|
|
|
341
400
|
return false;
|
|
342
401
|
}
|
|
343
402
|
const [edgeY, edgeX] = corner.split("-");
|
|
403
|
+
// A docked panel's bottom corners are above its dock, not on the bar.
|
|
404
|
+
const docked = this.#docked();
|
|
405
|
+
const room = docked ? this.#dockRoom() : viewport;
|
|
344
406
|
// Every term is an absolute screen coordinate, because that is what the
|
|
345
407
|
// clamps and the insets both speak. The usable box carries an origin, so
|
|
346
408
|
// its near edge is `viewport.left`, not zero, and its far edge is
|
|
347
409
|
// `viewport.left + viewport.width` -- a margin applied to the extents
|
|
348
410
|
// alone would send the agent's own move under the chrome the host
|
|
349
411
|
// reserved, which is the failure the usable box exists to prevent.
|
|
350
|
-
const nearX =
|
|
351
|
-
const nearY =
|
|
412
|
+
const nearX = room.left + EDGE_MARGIN;
|
|
413
|
+
const nearY = room.top + EDGE_MARGIN;
|
|
352
414
|
const left =
|
|
353
|
-
edgeX === "left"
|
|
354
|
-
? nearX
|
|
355
|
-
: Math.max(nearX, viewport.left + viewport.width - EDGE_MARGIN - box.width);
|
|
415
|
+
edgeX === "left" ? nearX : Math.max(nearX, room.left + room.width - EDGE_MARGIN - box.width);
|
|
356
416
|
const top =
|
|
357
|
-
edgeY === "top"
|
|
358
|
-
? nearY
|
|
359
|
-
: Math.max(nearY, viewport.top + viewport.height - EDGE_MARGIN - box.height);
|
|
417
|
+
edgeY === "top" ? nearY : Math.max(nearY, room.top + room.height - EDGE_MARGIN - box.height);
|
|
360
418
|
const host = { left, top, right: left + box.width, bottom: top + box.height };
|
|
419
|
+
if (docked) {
|
|
420
|
+
this.#dockPos = { left, top };
|
|
421
|
+
this.#writeDockInset(host);
|
|
422
|
+
this.#storeDockPosition(this.#dockPos);
|
|
423
|
+
if (restore !== null) {
|
|
424
|
+
this.#host.announceSurfaceChange(this.#host.strings().chatMoved, restore);
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
361
428
|
// Both axes measured, rather than one read twice: a host can restyle the
|
|
362
429
|
// launcher as a pill, and squaring it here would put it off the corner.
|
|
363
430
|
const launcherWidth = this.#host.launcher.offsetWidth;
|
|
@@ -391,6 +458,7 @@ export class PanelPlacement {
|
|
|
391
458
|
const corner = this.#host.element.getAttribute("data-expand-corner");
|
|
392
459
|
const launcherPos = this.#launcherPos;
|
|
393
460
|
const panelPos = this.#panelPos;
|
|
461
|
+
const dockPos = this.#dockPos;
|
|
394
462
|
const expandCorner = this.#expandCorner;
|
|
395
463
|
return () => {
|
|
396
464
|
this.#restoreProperty("--ag-ui-inset", inset);
|
|
@@ -402,7 +470,15 @@ export class PanelPlacement {
|
|
|
402
470
|
}
|
|
403
471
|
this.#launcherPos = launcherPos;
|
|
404
472
|
this.#panelPos = panelPos;
|
|
473
|
+
this.#dockPos = dockPos;
|
|
405
474
|
this.#expandCorner = expandCorner;
|
|
475
|
+
if (this.#docked()) {
|
|
476
|
+
// The dock's own record, and only that: the launcher's belongs to the
|
|
477
|
+
// corner surface and has nothing to undo here.
|
|
478
|
+
this.#storeDockPosition(dockPos);
|
|
479
|
+
this.syncResizeAnchor();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
406
482
|
// Erased rather than rewritten when there was nothing to go back to.
|
|
407
483
|
// #storeLauncherPosition returns early for a null position, which would
|
|
408
484
|
// leave the move this is undoing sitting in storage -- and since that
|
|
@@ -425,7 +501,7 @@ export class PanelPlacement {
|
|
|
425
501
|
* per interaction, because `placement` is a live attribute.
|
|
426
502
|
*/
|
|
427
503
|
#resizeAxis(): ResizeAxis {
|
|
428
|
-
return
|
|
504
|
+
return this.#surface().resizeAxis;
|
|
429
505
|
}
|
|
430
506
|
|
|
431
507
|
/**
|
|
@@ -567,6 +643,165 @@ export class PanelPlacement {
|
|
|
567
643
|
}
|
|
568
644
|
}
|
|
569
645
|
|
|
646
|
+
/** The surface the live placement and collapse-to attributes describe. */
|
|
647
|
+
#surface(): Surface {
|
|
648
|
+
const element = this.#host.element;
|
|
649
|
+
return surfaceFor(element.getAttribute("placement"), element.getAttribute("collapse-to"));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Whether the panel rises from the bottom bar or the host's anchor, until the
|
|
654
|
+
* user drags it somewhere else.
|
|
655
|
+
*/
|
|
656
|
+
#docked(): boolean {
|
|
657
|
+
const target = this.#surface().collapseTo;
|
|
658
|
+
return target === "bar" || target === "none";
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Write where a docked panel stands, and return the bar's height.
|
|
663
|
+
*
|
|
664
|
+
* Under the bar it is the strip's border box, which is 0 whenever the strip
|
|
665
|
+
* is not drawn (a phone with the panel open, where the panel is the screen).
|
|
666
|
+
* Under a host anchor it is the anchor's top and right, measured against the
|
|
667
|
+
* layout viewport a fixed element is placed in; an anchor of null leaves the
|
|
668
|
+
* panel in its placement's own corner. Every other surface carries neither
|
|
669
|
+
* property, so nothing stale is left behind by a live switch.
|
|
670
|
+
*/
|
|
671
|
+
syncDock(bar: HTMLElement, anchor: Element | null): number {
|
|
672
|
+
const element = this.#host.element;
|
|
673
|
+
const target = this.#surface().collapseTo;
|
|
674
|
+
const height = target === "bar" ? bar.offsetHeight : 0;
|
|
675
|
+
if (target === "bar") {
|
|
676
|
+
element.style.setProperty(DOCK_BOTTOM, `${height}px`);
|
|
677
|
+
} else if (target === "none" && anchor !== null) {
|
|
678
|
+
const rect = anchor.getBoundingClientRect();
|
|
679
|
+
const screen = this.#screen();
|
|
680
|
+
element.style.setProperty(DOCK_BOTTOM, `${Math.max(0, screen.height - rect.top)}px`);
|
|
681
|
+
element.style.setProperty(DOCK_RIGHT, `${Math.max(0, screen.width - rect.right)}px`);
|
|
682
|
+
} else {
|
|
683
|
+
element.style.removeProperty(DOCK_BOTTOM);
|
|
684
|
+
}
|
|
685
|
+
if (target !== "none" || anchor === null) {
|
|
686
|
+
element.style.removeProperty(DOCK_RIGHT);
|
|
687
|
+
}
|
|
688
|
+
element.toggleAttribute("data-dock-anchored", target === "none" && anchor !== null);
|
|
689
|
+
this.#dockTop =
|
|
690
|
+
target === "bar" && height > 0
|
|
691
|
+
? bar.getBoundingClientRect().top
|
|
692
|
+
: target === "none" && anchor !== null
|
|
693
|
+
? anchor.getBoundingClientRect().top
|
|
694
|
+
: Number.POSITIVE_INFINITY;
|
|
695
|
+
// Every change the dock can make passes through here -- the bar's height
|
|
696
|
+
// arrives by a ResizeObserver after the first restore, and a bar that grew
|
|
697
|
+
// since the drag would otherwise cover the panel's foot -- so the dragged
|
|
698
|
+
// spot is held against it here rather than only when it is restored.
|
|
699
|
+
this.#placeDocked();
|
|
700
|
+
return height;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* The part of the usable viewport a docked panel may stand in: all of it
|
|
705
|
+
* above the dock's line. The bar is what it minimises to, and the anchor is
|
|
706
|
+
* the host's own way back, so a dragged panel never covers either.
|
|
707
|
+
*/
|
|
708
|
+
#dockRoom(): ViewportBox {
|
|
709
|
+
const viewport = this.#viewport();
|
|
710
|
+
const bottom = Math.min(viewport.top + viewport.height, this.#dockTop);
|
|
711
|
+
return { ...viewport, height: Math.max(0, bottom - viewport.top) };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Put a docked panel back where the user dragged it, held inside its room.
|
|
716
|
+
*
|
|
717
|
+
* The inline inset comes off first, which is both how the panel's resting
|
|
718
|
+
* box is measured and what leaves a phone full-bleed: the small-viewport
|
|
719
|
+
* block reads the host's inset before its own, so a dragged spot written
|
|
720
|
+
* there would hold the panel off the screen's edges. The spot itself stays,
|
|
721
|
+
* for the next time there is room for it.
|
|
722
|
+
*/
|
|
723
|
+
#placeDocked(): void {
|
|
724
|
+
if (!this.#docked() || !this.#panelMovable() || this.dragging()) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const at = this.#dockPos ?? this.#readDockPosition();
|
|
728
|
+
if (at === null) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
this.#dockPos = at;
|
|
732
|
+
this.#host.element.style.removeProperty("--ag-ui-inset");
|
|
733
|
+
const rest = this.#host.element.getBoundingClientRect();
|
|
734
|
+
if (coversViewport(rest, this.#viewport())) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
// Measured at the dock, so the inset below is written from the corner the
|
|
738
|
+
// dock holds still and a resize grows the panel away from it.
|
|
739
|
+
this.syncResizeAnchor();
|
|
740
|
+
this.#writeDockInset(
|
|
741
|
+
clampPanel(
|
|
742
|
+
{ left: at.left, top: at.top, right: at.left + rest.width, bottom: at.top + rest.height },
|
|
743
|
+
this.#dockRoom(),
|
|
744
|
+
SCREEN_EDGE_MARGIN,
|
|
745
|
+
),
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** Write the inset that stands a docked panel at `box`. */
|
|
750
|
+
#writeDockInset(box: PanelRect): void {
|
|
751
|
+
this.#host.element.style.setProperty(
|
|
752
|
+
"--ag-ui-inset",
|
|
753
|
+
insetFrom(this.#anchor, box, this.#screen()),
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Remember a docked panel's dragged spot, or forget it for null. */
|
|
758
|
+
#storeDockPosition(at: { readonly left: number; readonly top: number } | null): void {
|
|
759
|
+
if (at === null) {
|
|
760
|
+
this.#host.clearPreference(DOCK_KEY);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
this.#host.writePreference(DOCK_KEY, JSON.stringify({ left: at.left, top: at.top }));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** The stored spot of a docked panel, or null for none or anything corrupt. */
|
|
767
|
+
#readDockPosition(): { readonly left: number; readonly top: number } | null {
|
|
768
|
+
const raw = this.#host.readPreference(DOCK_KEY);
|
|
769
|
+
if (raw === null) {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
return asPoint(JSON.parse(raw));
|
|
774
|
+
} catch {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Whether the header drag and moveTo may move the open panel: the
|
|
781
|
+
* placement's answer, unless the host turned moving off.
|
|
782
|
+
*/
|
|
783
|
+
#panelMovable(): boolean {
|
|
784
|
+
return (
|
|
785
|
+
this.#host.element.getAttribute("data-launcher-drag") !== "false" &&
|
|
786
|
+
this.#surface().panelMovable
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Whether a header drag may start now. A docked panel on a phone is the
|
|
792
|
+
* screen, and a drag there would pull it off the edges it fills; a corner
|
|
793
|
+
* panel's header is left as it was.
|
|
794
|
+
*/
|
|
795
|
+
#canDragPanel(): boolean {
|
|
796
|
+
if (!this.#panelMovable()) {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
return (
|
|
800
|
+
!this.#docked() ||
|
|
801
|
+
!coversViewport(this.#host.element.getBoundingClientRect(), this.#viewport())
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
570
805
|
/**
|
|
571
806
|
* Whether this placement lets the launcher be dragged, read per interaction
|
|
572
807
|
* because `placement` is a live attribute. `data-launcher-drag="false"` opts
|
|
@@ -575,7 +810,7 @@ export class PanelPlacement {
|
|
|
575
810
|
#launcherDraggable(): boolean {
|
|
576
811
|
return (
|
|
577
812
|
this.#host.element.getAttribute("data-launcher-drag") !== "false" &&
|
|
578
|
-
|
|
813
|
+
this.#surface().launcherDraggable
|
|
579
814
|
);
|
|
580
815
|
}
|
|
581
816
|
|
|
@@ -810,6 +1045,13 @@ export class PanelPlacement {
|
|
|
810
1045
|
* other still names the old one would move the launcher for no reason.
|
|
811
1046
|
*/
|
|
812
1047
|
#movePanel(box: PanelRect, from: PanelRect): { held: PanelRect; launcher: LauncherBox | null } {
|
|
1048
|
+
if (this.#docked()) {
|
|
1049
|
+
// Inside the room above the dock, with nothing carried along.
|
|
1050
|
+
const held = clampPanel(box, this.#dockRoom(), SCREEN_EDGE_MARGIN);
|
|
1051
|
+
this.#dockPos = { left: held.left, top: held.top };
|
|
1052
|
+
this.#writeDockInset(held);
|
|
1053
|
+
return { held, launcher: null };
|
|
1054
|
+
}
|
|
813
1055
|
if (!this.#launcherDraggable()) {
|
|
814
1056
|
return { held: box, launcher: null };
|
|
815
1057
|
}
|
|
@@ -883,6 +1125,10 @@ export class PanelPlacement {
|
|
|
883
1125
|
// again. Recomputing it is how the two came apart: releasing the drag
|
|
884
1126
|
// moved the bubble by the panel's whole travel a second time.
|
|
885
1127
|
const { held, launcher } = this.#movePanel(box, from);
|
|
1128
|
+
if (this.#docked()) {
|
|
1129
|
+
this.#storeDockPosition({ left: held.left, top: held.top });
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
886
1132
|
if (launcher === null) {
|
|
887
1133
|
return;
|
|
888
1134
|
}
|
|
@@ -973,6 +1219,13 @@ export class PanelPlacement {
|
|
|
973
1219
|
* unreachable -- it is the only way back to a collapsed conversation.
|
|
974
1220
|
*/
|
|
975
1221
|
restoreLauncherPosition(): void {
|
|
1222
|
+
// Before the launcher's record is read at all: it belongs to the corner
|
|
1223
|
+
// surface, and seeding anything from it would put a spot dragged there on
|
|
1224
|
+
// a panel that should stand on its dock.
|
|
1225
|
+
if (this.#docked()) {
|
|
1226
|
+
this.#placeDocked();
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
976
1229
|
const stored = this.#readLauncherPosition();
|
|
977
1230
|
const launcher = this.#launcherPos ?? stored;
|
|
978
1231
|
if (launcher === null) {
|
|
@@ -1030,6 +1283,13 @@ export class PanelPlacement {
|
|
|
1030
1283
|
* the switch inline and pins a sidebar to wherever the floating launcher was.
|
|
1031
1284
|
*/
|
|
1032
1285
|
releaseLauncherPosition(): void {
|
|
1286
|
+
// A dragged dock spot belongs to a docked panel that moves. Any other
|
|
1287
|
+
// surface hands it back; a docked one keeps it, and syncDock holds it
|
|
1288
|
+
// against the new dock.
|
|
1289
|
+
if (this.#dockPos !== null && !(this.#docked() && this.#panelMovable())) {
|
|
1290
|
+
this.#dockPos = null;
|
|
1291
|
+
this.#host.element.style.removeProperty("--ag-ui-inset");
|
|
1292
|
+
}
|
|
1033
1293
|
if (this.#launcherDraggable()) {
|
|
1034
1294
|
return;
|
|
1035
1295
|
}
|
|
@@ -1057,7 +1317,28 @@ export class PanelPlacement {
|
|
|
1057
1317
|
*/
|
|
1058
1318
|
#applyResize(grip: ResizeGrip, box: PanelRect): PanelRect {
|
|
1059
1319
|
box = this.#withinViewport(box);
|
|
1320
|
+
if (this.#docked() && this.#dockPos === null) {
|
|
1321
|
+
// A panel standing on its dock keeps it through a resize: the edges it
|
|
1322
|
+
// stands on are the bar's or the anchor's, and a grip on one of them
|
|
1323
|
+
// would write the position below and take the panel off its dock. So
|
|
1324
|
+
// the pinned edges stay where they are and only the free ones follow.
|
|
1325
|
+
// Once dragged off, it resizes like a corner panel, below.
|
|
1326
|
+
const now = this.#host.element.getBoundingClientRect();
|
|
1327
|
+
box = {
|
|
1328
|
+
left: grip.x === this.#anchor.x ? now.left : box.left,
|
|
1329
|
+
right: grip.x === this.#anchor.x ? now.right : box.right,
|
|
1330
|
+
top: grip.y === this.#anchor.y ? now.top : box.top,
|
|
1331
|
+
bottom: grip.y === this.#anchor.y ? now.bottom : box.bottom,
|
|
1332
|
+
};
|
|
1333
|
+
this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
|
|
1334
|
+
return box;
|
|
1335
|
+
}
|
|
1060
1336
|
this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
|
|
1337
|
+
if (this.#docked()) {
|
|
1338
|
+
// Any grip moves the top-left corner the spot is kept as, including one
|
|
1339
|
+
// on a free edge, which leaves the inset alone.
|
|
1340
|
+
this.#dockPos = { left: box.left, top: box.top };
|
|
1341
|
+
}
|
|
1061
1342
|
if (grip.x !== this.#anchor.x && grip.y !== this.#anchor.y) {
|
|
1062
1343
|
return box;
|
|
1063
1344
|
}
|
|
@@ -1093,6 +1374,9 @@ export class PanelPlacement {
|
|
|
1093
1374
|
const held = this.#applyResize(grip, box);
|
|
1094
1375
|
this.#persistSize({ width: held.right - held.left, height: held.bottom - held.top });
|
|
1095
1376
|
this.#storeLauncherPosition();
|
|
1377
|
+
if (this.#dockPos !== null) {
|
|
1378
|
+
this.#storeDockPosition(this.#dockPos);
|
|
1379
|
+
}
|
|
1096
1380
|
// Re-stamp after the drag: a host whose layout changed underneath us would
|
|
1097
1381
|
// otherwise keep the tab-reachable grip in the old corner, which reads as
|
|
1098
1382
|
// the control being in the wrong place even though the drag was right.
|
|
@@ -57,5 +57,14 @@ export const SMALL_VIEWPORT_STYLES: string = `
|
|
|
57
57
|
:host([placement=""]:not([data-small-viewport="off"])) .resize-handle {
|
|
58
58
|
display: none;
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
/* The bottom bar shows only while collapsed. With the panel open the panel
|
|
62
|
+
is the screen, its header's minimise control is the way back, and a
|
|
63
|
+
second strip fixed to the bottom edge would sit under the keyboard while
|
|
64
|
+
the composer has focus. Hidden by display, so the bar measures 0 and the
|
|
65
|
+
host's padding goes with it. */
|
|
66
|
+
:host([data-collapsed-to="bar"]:not([collapsed]):not([data-small-viewport="off"])) .dock-bar {
|
|
67
|
+
display: none;
|
|
68
|
+
}
|
|
60
69
|
}
|
|
61
70
|
`;
|