@artooi/ag-ui-web-component 0.46.0 → 0.48.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 +221 -1
- package/README.md +298 -31
- package/dist/ag-ui-web-component.bundle.js +597 -177
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +37 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +73 -2
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/chat_registry.d.ts +23 -0
- package/dist/core/chat_registry.d.ts.map +1 -0
- 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/core/events/run_finished_detail.d.ts +6 -0
- package/dist/core/events/run_finished_detail.d.ts.map +1 -1
- package/dist/core/events/thread_detail.d.ts +27 -0
- package/dist/core/events/thread_detail.d.ts.map +1 -0
- package/dist/core/run_handlers.d.ts +6 -1
- package/dist/core/run_handlers.d.ts.map +1 -1
- package/dist/dom/animations.d.ts +21 -6
- package/dist/dom/animations.d.ts.map +1 -1
- package/dist/dom/highlight_overlay.d.ts +8 -1
- package/dist/dom/highlight_overlay.d.ts.map +1 -1
- package/dist/dom/ring_color.d.ts +22 -0
- package/dist/dom/ring_color.d.ts.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1164 -95
- 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 +24 -2
- 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 +17 -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 +21 -17
- package/dist/ui/placement/surface_for.d.ts.map +1 -1
- package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
- package/dist/ui/progress/tool_group_styles.d.ts.map +1 -1
- package/dist/ui/progress/tool_step_group.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/scheme_styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +41 -2
- package/src/core/ag_ui_chat.ts +430 -27
- package/src/core/chat_registry.ts +53 -0
- package/src/core/events/bar_height_detail.ts +5 -0
- package/src/core/events/run_finished_detail.ts +6 -0
- package/src/core/events/thread_detail.ts +26 -0
- package/src/core/run_handlers.ts +9 -4
- package/src/dom/animations.ts +30 -31
- package/src/dom/highlight_overlay.ts +10 -5
- package/src/dom/ring_color.ts +222 -0
- package/src/index.ts +7 -0
- package/src/tools/chat_surface_tools.ts +18 -8
- package/src/ui/history/conversation_history.ts +75 -7
- package/src/ui/placement/collapse_styles.ts +248 -2
- package/src/ui/placement/dock_styles.ts +91 -0
- package/src/ui/placement/embedded_styles.ts +2 -2
- package/src/ui/placement/panel_placement.ts +105 -12
- package/src/ui/placement/preset_styles.ts +1 -1
- package/src/ui/placement/sidebar_styles.ts +1 -1
- package/src/ui/placement/small_viewport_styles.ts +14 -0
- package/src/ui/placement/surface_for.ts +29 -18
- package/src/ui/progress/tool_call_card.ts +18 -8
- package/src/ui/progress/tool_group_styles.ts +49 -0
- package/src/ui/progress/tool_step_group.ts +3 -0
- 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 +30 -3
- package/src/ui/styles.ts +4 -0
- package/src/ui/themes/scheme_styles.ts +10 -1
- package/src/version.ts +1 -1
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
ClientConversationStore,
|
|
10
10
|
NavigationCheckpoint,
|
|
11
11
|
} from "../../core/conversation_store.js";
|
|
12
|
+
import type { ThreadDetail } from "../../core/events/thread_detail.js";
|
|
12
13
|
import type { MessageRole } from "../../core/message_role.js";
|
|
13
14
|
import { RunIndex } from "../../core/run_index.js";
|
|
14
15
|
import { toolStatusFromOutcome } from "../../core/tool_outcome.js";
|
|
@@ -119,6 +120,11 @@ export interface ConversationHistoryHost {
|
|
|
119
120
|
readonly resetState: () => void;
|
|
120
121
|
/** Swap the composer between Send and Stop, which the composer owns. */
|
|
121
122
|
readonly setRunning: (running: boolean) => void;
|
|
123
|
+
/**
|
|
124
|
+
* Say the conversation on screen changed, once its transcript is replaced.
|
|
125
|
+
* The element fills in the thread it replaced, which it last reported.
|
|
126
|
+
*/
|
|
127
|
+
readonly threadChanged: (threadId: string, reason: ThreadDetail["reason"]) => void;
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
/**
|
|
@@ -210,6 +216,8 @@ export class ConversationHistory {
|
|
|
210
216
|
* Also what tells a pick refused for a load from one refused for a run.
|
|
211
217
|
*/
|
|
212
218
|
#loading: (() => void) | null = null;
|
|
219
|
+
/** The thread switch under way and the promise that settles it, or `null`. */
|
|
220
|
+
#switching: { readonly threadId: string; readonly done: Promise<void> } | null = null;
|
|
213
221
|
|
|
214
222
|
constructor(host: ConversationHistoryHost) {
|
|
215
223
|
this.#host = host;
|
|
@@ -325,17 +333,56 @@ export class ConversationHistory {
|
|
|
325
333
|
this.#host.checkpoints.open();
|
|
326
334
|
}
|
|
327
335
|
|
|
328
|
-
/**
|
|
329
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Switch the active conversation to an existing thread and replay it.
|
|
338
|
+
*
|
|
339
|
+
* A call for the thread a switch is already loading is handed that switch's
|
|
340
|
+
* own promise, so it settles when the thread is drawn rather than at once,
|
|
341
|
+
* before the replay.
|
|
342
|
+
*
|
|
343
|
+
* A store that rejects the load leaves the element on the thread it was on:
|
|
344
|
+
* the switch has already cleared the transcript, so it points back at the
|
|
345
|
+
* old thread and draws it again, saying nothing, and then rejects with the
|
|
346
|
+
* store's error. Left as it was, the element named a thread it was not
|
|
347
|
+
* showing and never reported it.
|
|
348
|
+
*/
|
|
349
|
+
switchThread(threadId: string): Promise<void> {
|
|
330
350
|
if (threadId === this.#threadId) {
|
|
331
|
-
return;
|
|
351
|
+
return this.#switching?.threadId === threadId ? this.#switching.done : Promise.resolve();
|
|
332
352
|
}
|
|
353
|
+
const previous = this.#threadId;
|
|
354
|
+
const done = this.#switchTo(threadId, previous).finally(() => {
|
|
355
|
+
if (this.#switching === entry) {
|
|
356
|
+
this.#switching = null;
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
const entry = { threadId, done };
|
|
360
|
+
this.#switching = entry;
|
|
361
|
+
return done;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async #switchTo(threadId: string, previous: string): Promise<void> {
|
|
333
365
|
this.#host.cancelRun();
|
|
334
366
|
this.#host.resetState();
|
|
335
367
|
this.#host.conversationStore().setActiveThread(threadId);
|
|
336
368
|
this.#threadId = threadId;
|
|
337
369
|
this.#host.setRunning(false);
|
|
338
|
-
|
|
370
|
+
try {
|
|
371
|
+
await this.rehydrate("switch");
|
|
372
|
+
} catch (error) {
|
|
373
|
+
// Only while this switch is still the one on screen: a newer switch, a
|
|
374
|
+
// new chat or a delete has moved the thread on, and undoing that would
|
|
375
|
+
// put the old one back over it.
|
|
376
|
+
if (this.#threadId === threadId) {
|
|
377
|
+
this.#threadId = previous;
|
|
378
|
+
this.#host.conversationStore().setActiveThread(previous);
|
|
379
|
+
// Drawn again without a word: the thread on screen is the one the last
|
|
380
|
+
// event reported, so there is nothing to tell a host. A store that
|
|
381
|
+
// cannot answer for it either leaves the screen as it is.
|
|
382
|
+
await this.rehydrate(null).catch(() => undefined);
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
339
386
|
}
|
|
340
387
|
|
|
341
388
|
/** Rename a thread, then reload the list that shows its title. */
|
|
@@ -355,6 +402,8 @@ export class ConversationHistory {
|
|
|
355
402
|
this.#host.resetState();
|
|
356
403
|
this.adoptActiveThread();
|
|
357
404
|
this.#host.setRunning(false);
|
|
405
|
+
// No replay to wait for: the thread that took its place is a fresh one.
|
|
406
|
+
this.#host.threadChanged(this.#threadId, "delete");
|
|
358
407
|
}
|
|
359
408
|
void this.refreshDrawer();
|
|
360
409
|
}
|
|
@@ -628,8 +677,12 @@ export class ConversationHistory {
|
|
|
628
677
|
* for the run it would have resumed -- unless what replaced it is a restore
|
|
629
678
|
* of the same conversation, still current, which answers for that run
|
|
630
679
|
* instead; see `#forgetStopped`.
|
|
680
|
+
*
|
|
681
|
+
* `reason` is what the thread event this restore ends with reports as why
|
|
682
|
+
* the conversation changed. A restore that is stopped reports nothing, and
|
|
683
|
+
* neither does one given `null`, which draws the thread already reported.
|
|
631
684
|
*/
|
|
632
|
-
async rehydrate(): Promise<void> {
|
|
685
|
+
async rehydrate(reason: ThreadDetail["reason"] | null): Promise<void> {
|
|
633
686
|
// Claimed before awaiting, and checked by `#restoring` wherever the restore
|
|
634
687
|
// has waited or run host code. With a slow remote store, picking thread B
|
|
635
688
|
// then C interleaved both replays into one transcript, and New chat drew
|
|
@@ -727,11 +780,26 @@ export class ConversationHistory {
|
|
|
727
780
|
}
|
|
728
781
|
}
|
|
729
782
|
}
|
|
783
|
+
if (checkpoint === null) {
|
|
784
|
+
this.#noticeIfRunUnfinished(messages);
|
|
785
|
+
}
|
|
786
|
+
// Said once the transcript is the restored one, and only by a restore
|
|
787
|
+
// still current: one that was stopped returned above, so two quick
|
|
788
|
+
// switches report the second alone. Before a resume, because the resume
|
|
789
|
+
// is a run, and waiting for it reported the change a whole answer late.
|
|
790
|
+
if (reason !== null) {
|
|
791
|
+
this.#host.threadChanged(threadId, reason);
|
|
792
|
+
}
|
|
730
793
|
if (checkpoint !== null) {
|
|
794
|
+
// A listener is host code, and can stop the restore as a replayed
|
|
795
|
+
// renderer can. Resuming then answered the call into the conversation
|
|
796
|
+
// that replaced it, and left the checkpoint for a later visit to resume.
|
|
797
|
+
if (!this.#restoring(generation)) {
|
|
798
|
+
this.#forgetStopped(store, threadId, checkpoint);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
731
801
|
await this.#resumeFrom(checkpoint, generation);
|
|
732
|
-
return;
|
|
733
802
|
}
|
|
734
|
-
this.#noticeIfRunUnfinished(messages);
|
|
735
803
|
} finally {
|
|
736
804
|
// The resumed run starting has let go of the hold already, as a send's
|
|
737
805
|
// does, and a request that then fails still started one: the client
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Collapsing and expanding: the animation toward the launcher,
|
|
2
|
-
//
|
|
1
|
+
// Collapsing and expanding: the animation toward the launcher, the in-flow
|
|
2
|
+
// and page placements that collapse differently or not at all, and the two
|
|
3
|
+
// collapse-to targets: the bottom bar, and the host's own (hidden here).
|
|
3
4
|
export const COLLAPSE_STYLES: string = `
|
|
4
5
|
/* ── Collapse ───────────────────────────────────────────────────────────────
|
|
5
6
|
Collapsing shrinks the widget to the round floating launcher: the panel
|
|
@@ -101,12 +102,18 @@ export const COLLAPSE_STYLES: string = `
|
|
|
101
102
|
:host([collapsed][placement="embedded"]) .messages-wrap,
|
|
102
103
|
:host([collapsed][placement="embedded"]) .messages,
|
|
103
104
|
:host([collapsed][placement="embedded"]) .input-row,
|
|
105
|
+
:host([collapsed][placement="embedded"]) slot[name="above-composer"],
|
|
104
106
|
:host([collapsed][placement="embedded"]) .skill-chips,
|
|
105
107
|
:host([collapsed][placement="embedded"]) .skill-palette,
|
|
106
108
|
:host([collapsed][placement="embedded"]) .skill-hint {
|
|
107
109
|
display: none;
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
/* The above-composer slot is in the list above because it is the host's content
|
|
113
|
+
against the composer: a bare slot has no box of its own, so hiding the input
|
|
114
|
+
row alone left the slotted row showing under the header bar. Its sibling slot
|
|
115
|
+
below the composer is a footer and is left to the host. */
|
|
116
|
+
|
|
110
117
|
/* The page placement has no collapsed state, so it offers no control for one.
|
|
111
118
|
A dedicated route has no "away" to go to: shrinking it left a strip of
|
|
112
119
|
application chrome fixed over a route that no longer had an owner, and this
|
|
@@ -134,4 +141,243 @@ export const COLLAPSE_STYLES: string = `
|
|
|
134
141
|
transform: none;
|
|
135
142
|
visibility: visible;
|
|
136
143
|
}
|
|
144
|
+
|
|
145
|
+
/* ── Bottom bar (collapse-to="bar") ──────────────────────────────────────────
|
|
146
|
+
The round launcher drawn as a strip along the bottom edge: the title, the
|
|
147
|
+
unread badge and a running mark, with a history button beside it. The
|
|
148
|
+
wrapper draws no box anywhere else, so the launcher lays out against the
|
|
149
|
+
host as it always has; under the bar it is fixed to the bottom of the
|
|
150
|
+
viewport, outside the host's box, which sits where the panel opens.
|
|
151
|
+
|
|
152
|
+
Here rather than with the launcher because it restyles the launcher's
|
|
153
|
+
collapsed and pressed states, which this module follows in the cascade.
|
|
154
|
+
The strip stays drawn while the panel is open on a wide viewport, as the
|
|
155
|
+
way to minimise it; the phone block hides it then. */
|
|
156
|
+
.dock-bar {
|
|
157
|
+
display: contents;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.bar-history {
|
|
161
|
+
display: none;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
:host([data-collapsed-to="bar"]) .dock-bar {
|
|
165
|
+
position: fixed;
|
|
166
|
+
inset: auto var(--_viewport-inset-right) var(--_viewport-inset-bottom) var(--_viewport-inset-left);
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
justify-content: flex-end;
|
|
170
|
+
gap: 4px;
|
|
171
|
+
box-sizing: border-box;
|
|
172
|
+
min-height: 40px;
|
|
173
|
+
padding: 4px 8px;
|
|
174
|
+
border-top: 1px solid var(--_border);
|
|
175
|
+
background: var(--_bg);
|
|
176
|
+
color: var(--_fg);
|
|
177
|
+
pointer-events: auto;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* A left-hand chat keeps its side: its controls and its panel at the left end. */
|
|
181
|
+
:host([data-collapsed-to="bar"][placement="bottom-left"]) .dock-bar {
|
|
182
|
+
justify-content: flex-start;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* The launcher as the strip's title button. Its resting state hides it behind
|
|
186
|
+
an open panel; the bar keeps it on screen, so every part of that state is
|
|
187
|
+
restated, and the hover and press scales are cancelled at the weight of the
|
|
188
|
+
rules that set them. The title truncates rather than taking a fixed width,
|
|
189
|
+
leaving room beside it for whatever a later release adds to the row. */
|
|
190
|
+
:host([data-collapsed-to="bar"]) .launcher {
|
|
191
|
+
position: static;
|
|
192
|
+
inset: auto;
|
|
193
|
+
flex: 0 1 auto;
|
|
194
|
+
justify-content: flex-start;
|
|
195
|
+
gap: 8px;
|
|
196
|
+
width: auto;
|
|
197
|
+
min-width: 0;
|
|
198
|
+
max-width: 100%;
|
|
199
|
+
height: auto;
|
|
200
|
+
min-height: 32px;
|
|
201
|
+
padding: 4px 10px;
|
|
202
|
+
border-radius: 8px;
|
|
203
|
+
background: transparent;
|
|
204
|
+
color: var(--_fg);
|
|
205
|
+
box-shadow: none;
|
|
206
|
+
touch-action: manipulation;
|
|
207
|
+
opacity: 1;
|
|
208
|
+
transform: none;
|
|
209
|
+
visibility: visible;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
:host([data-collapsed-to="bar"]) .launcher:hover {
|
|
213
|
+
background: var(--_wash);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Bare at rest, like the history button beside it. The themes frame the
|
|
217
|
+
round launcher with a border or a shadow at the weight of the rule above
|
|
218
|
+
and later in the cascade, so the child combinator on the bar is there for
|
|
219
|
+
weight, not scope: the launcher is always the bar's direct child. */
|
|
220
|
+
:host([data-collapsed-to="bar"]) .dock-bar > .launcher {
|
|
221
|
+
border: 0;
|
|
222
|
+
box-shadow: none;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
:host([data-collapsed-to="bar"][collapsed]) .launcher:hover,
|
|
226
|
+
:host([data-collapsed-to="bar"][collapsed]) .launcher:active {
|
|
227
|
+
transform: none;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
:host([data-collapsed-to="bar"]) .launcher .icon-holder {
|
|
231
|
+
flex: none;
|
|
232
|
+
width: 18px;
|
|
233
|
+
height: 18px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
:host([data-collapsed-to="bar"]) .rail-label {
|
|
237
|
+
display: block;
|
|
238
|
+
min-width: 0;
|
|
239
|
+
overflow: hidden;
|
|
240
|
+
font-size: 0.9em;
|
|
241
|
+
font-weight: 600;
|
|
242
|
+
white-space: nowrap;
|
|
243
|
+
text-overflow: ellipsis;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
:host([data-collapsed-to="bar"]) .launcher-badge {
|
|
247
|
+
position: static;
|
|
248
|
+
flex: none;
|
|
249
|
+
box-shadow: none;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* The running mark: the element already reflects a run as data-running, so
|
|
253
|
+
the bar shows one without a state of its own. */
|
|
254
|
+
:host([data-collapsed-to="bar"][data-running]) .launcher::after {
|
|
255
|
+
content: "";
|
|
256
|
+
flex: none;
|
|
257
|
+
width: 8px;
|
|
258
|
+
height: 8px;
|
|
259
|
+
border-radius: 50%;
|
|
260
|
+
background: var(--_accent);
|
|
261
|
+
animation: ag-ui-dock-running 1.2s ease-in-out infinite;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@keyframes ag-ui-dock-running {
|
|
265
|
+
50% {
|
|
266
|
+
opacity: 0.35;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@media (prefers-reduced-motion: reduce) {
|
|
271
|
+
:host([data-collapsed-to="bar"][data-running]) .launcher::after {
|
|
272
|
+
animation: none;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
:host([data-collapsed-to="bar"]) .bar-history {
|
|
277
|
+
display: inline-flex;
|
|
278
|
+
flex: none;
|
|
279
|
+
align-items: center;
|
|
280
|
+
justify-content: center;
|
|
281
|
+
width: 32px;
|
|
282
|
+
height: 32px;
|
|
283
|
+
padding: 0;
|
|
284
|
+
border: 0;
|
|
285
|
+
border-radius: 8px;
|
|
286
|
+
background: transparent;
|
|
287
|
+
color: var(--_muted);
|
|
288
|
+
font: inherit;
|
|
289
|
+
cursor: pointer;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
:host([data-collapsed-to="bar"]) .bar-history:hover {
|
|
293
|
+
background: var(--_wash);
|
|
294
|
+
color: var(--_fg);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* Both declare a display of their own, so each needs its own hidden rule. */
|
|
298
|
+
:host([data-collapsed-to]) .dock-bar[hidden],
|
|
299
|
+
:host([data-collapsed-to]) .bar-history[hidden] {
|
|
300
|
+
display: none;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* History from the bar: the panel's own drawer, moved beside the bar while
|
|
304
|
+
the panel is away, drawn as a popover standing on the bar's end. There is
|
|
305
|
+
no backdrop; a press outside the bar closes it, as Escape does.
|
|
306
|
+
|
|
307
|
+
Its outer edge keeps the panel's gutter, so the popover and the panel line
|
|
308
|
+
up on the side they share. The bar is already inset by the host's reserved
|
|
309
|
+
edges, so the gutter alone is the whole offset from it, and the width cap
|
|
310
|
+
leaves the gutter free on both sides. A phone keeps a narrow margin
|
|
311
|
+
instead, in the block below: the panel there is the whole screen, so there
|
|
312
|
+
is no edge to line up with. */
|
|
313
|
+
:host([data-collapsed-to="bar"]) .dock-bar > .drawer {
|
|
314
|
+
inset: auto var(--_edge-gutter) calc(100% + 8px) auto;
|
|
315
|
+
width: min(320px, calc(var(--_viewport-width) - 2 * var(--_edge-gutter)));
|
|
316
|
+
height: min(440px, calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - 16px));
|
|
317
|
+
border-radius: var(--_panel-radius);
|
|
318
|
+
box-shadow: var(--_shadow);
|
|
319
|
+
overflow: hidden;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
:host([data-collapsed-to="bar"][placement="bottom-left"]) .dock-bar > .drawer {
|
|
323
|
+
inset: auto auto calc(100% + 8px) var(--_edge-gutter);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@media (max-width: 600px) {
|
|
327
|
+
:host([data-collapsed-to="bar"]:not([data-small-viewport="off"])) .dock-bar > .drawer {
|
|
328
|
+
inset: auto 8px calc(100% + 8px) auto;
|
|
329
|
+
width: min(320px, calc(100vw - 16px));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
:host([data-collapsed-to="bar"][placement="bottom-left"]:not([data-small-viewport="off"])) .dock-bar > .drawer {
|
|
333
|
+
inset: auto auto calc(100% + 8px) 8px;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
:host([data-collapsed-to="bar"]) .dock-bar > .drawer .drawer-backdrop {
|
|
338
|
+
display: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* The popover closes in one step. The in-panel drawer slides its panel out
|
|
342
|
+
and holds the overlay's visibility until the slide ends; here the overlay
|
|
343
|
+
is the popover's own box, rounded and clipped, so the slide moved the list
|
|
344
|
+
inside a box that stayed put (read as a shrink) and the box went only once
|
|
345
|
+
the slide had finished. This outweighs the drawer's own hidden rule, so the
|
|
346
|
+
box hides at once, and whatever the list inside does is out of sight. The
|
|
347
|
+
way in needs no rule: the drawer is moved into the bar as it opens, and a
|
|
348
|
+
moved element starts no transition. Nothing is left for reduced motion. */
|
|
349
|
+
:host([data-collapsed-to="bar"]) .dock-bar > .drawer {
|
|
350
|
+
transition: none;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* Border-box, so the list's own border sits inside the popover's clipped box
|
|
354
|
+
rather than 2px past its bottom edge. */
|
|
355
|
+
:host([data-collapsed-to="bar"]) .dock-bar > .drawer .drawer-panel {
|
|
356
|
+
box-sizing: border-box;
|
|
357
|
+
width: 100%;
|
|
358
|
+
border: 1px solid var(--_border);
|
|
359
|
+
border-radius: inherit;
|
|
360
|
+
box-shadow: none;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* ── The host's own bar (collapse-to="none") ─────────────────────────────────
|
|
364
|
+
Collapsed is hidden: no launcher, no rail, no header bar. The host's control
|
|
365
|
+
calls the public API, and the panel rises from its anchor (the dock module)
|
|
366
|
+
or from the placement's corner. Each placement's own collapsed rule is
|
|
367
|
+
overridden at its weight or above, later in the cascade. */
|
|
368
|
+
:host([data-collapsed-to="none"][collapsed]) {
|
|
369
|
+
pointer-events: none;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
:host([data-collapsed-to="none"][collapsed]) .launcher {
|
|
373
|
+
display: none;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
:host([data-collapsed-to="none"][collapsed][placement="sidebar"]) {
|
|
377
|
+
width: 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
:host([data-collapsed-to="none"][collapsed][placement="embedded"]) {
|
|
381
|
+
display: none;
|
|
382
|
+
}
|
|
137
383
|
`;
|
|
@@ -0,0 +1,91 @@
|
|
|
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.
|
|
20
|
+
|
|
21
|
+
These blocks restate tokens and nothing else, and sit ahead of the phone
|
|
22
|
+
block at no more weight than it has, so on a phone the panel is the screen
|
|
23
|
+
as every other placement is there. The bar measures 0 while it is not drawn
|
|
24
|
+
(a phone with the panel open), which is what lets the side and full shapes
|
|
25
|
+
below fall back to the whole height without a phone rule of their own.
|
|
26
|
+
|
|
27
|
+
Each placement keeps its own side and shape; only the bottom edge moves. */
|
|
28
|
+
:host([data-collapsed-to="bar"]) {
|
|
29
|
+
--_inset: var(
|
|
30
|
+
--ag-ui-inset,
|
|
31
|
+
auto calc(var(--_edge-gutter) + var(--_viewport-inset-right))
|
|
32
|
+
calc(
|
|
33
|
+
var(--_edge-gutter) + var(--_viewport-inset-bottom) +
|
|
34
|
+
max(var(--_dock-bottom, 0px), var(--_keyboard-inset))
|
|
35
|
+
)
|
|
36
|
+
auto
|
|
37
|
+
);
|
|
38
|
+
--_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - var(--_edge-gutter)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
:host([data-collapsed-to="bar"][placement="bottom-left"]) {
|
|
42
|
+
--_inset: var(
|
|
43
|
+
--ag-ui-inset,
|
|
44
|
+
auto auto
|
|
45
|
+
calc(
|
|
46
|
+
var(--_edge-gutter) + var(--_viewport-inset-bottom) +
|
|
47
|
+
max(var(--_dock-bottom, 0px), var(--_keyboard-inset))
|
|
48
|
+
)
|
|
49
|
+
calc(var(--_edge-gutter) + var(--_viewport-inset-left))
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* The column and the full-screen panel stop at the bar rather than covering
|
|
54
|
+
it, so the bar stays where the user minimises from. */
|
|
55
|
+
:host([data-collapsed-to="bar"][placement="side"]) {
|
|
56
|
+
--_inset: var(
|
|
57
|
+
--ag-ui-inset,
|
|
58
|
+
calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
|
|
59
|
+
calc(var(--_viewport-inset-bottom) + var(--_dock-bottom, 0px)) auto
|
|
60
|
+
);
|
|
61
|
+
--_height: var(--ag-ui-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
|
|
62
|
+
--_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
:host([data-collapsed-to="bar"][placement="full"]) {
|
|
66
|
+
--_inset: var(
|
|
67
|
+
--ag-ui-inset,
|
|
68
|
+
calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
|
|
69
|
+
calc(var(--_viewport-inset-bottom) + var(--_dock-bottom, 0px)) var(--_viewport-inset-left)
|
|
70
|
+
);
|
|
71
|
+
--_height: var(--ag-ui-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
|
|
72
|
+
--_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_dock-bottom, 0px)));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* The host's own bar: the panel stands the anchor gap (8px unless the host
|
|
76
|
+
says otherwise) above the anchor and lines up with its right edge. Only
|
|
77
|
+
once the element found the anchor; until then, and when it never does, the
|
|
78
|
+
placement's own corner stands.
|
|
79
|
+
|
|
80
|
+
The cap spends the larger of the gap and the gutter. The gutter alone is
|
|
81
|
+
what this always spent, so the default is unchanged; but a host that opens
|
|
82
|
+
the gap past the gutter to clear a tall toolbar would otherwise push a
|
|
83
|
+
panel grown to the cap off the top of the window by the difference. */
|
|
84
|
+
:host([data-collapsed-to="none"][data-dock-anchored]) {
|
|
85
|
+
--_inset: var(--ag-ui-inset, auto var(--_dock-right, 0px) calc(var(--_dock-bottom, 0px) + var(--_anchor-gap)) auto);
|
|
86
|
+
--_max-height: var(
|
|
87
|
+
--ag-ui-max-height,
|
|
88
|
+
calc(var(--_viewport-height) - var(--_dock-bottom, 0px) - max(var(--_edge-gutter), var(--_anchor-gap)))
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
@@ -4,8 +4,8 @@ export const EMBEDDED_STYLES: string = `
|
|
|
4
4
|
the widget lives in the host's own layout (fixes overlay/z-index clashes). */
|
|
5
5
|
:host([placement="embedded"]) {
|
|
6
6
|
--_position: var(--ag-ui-position, static);
|
|
7
|
-
--_width: var(--ag-ui-width, 100%);
|
|
8
|
-
--_height: var(--ag-ui-height, 100%);
|
|
7
|
+
--_width: var(--_drag-width, var(--ag-ui-width, 100%));
|
|
8
|
+
--_height: var(--_drag-height, var(--ag-ui-height, 100%));
|
|
9
9
|
--_max-width: var(--ag-ui-max-width, 100%);
|
|
10
10
|
--_max-height: var(--ag-ui-max-height, 100%);
|
|
11
11
|
--_shadow: var(--ag-ui-shadow, none);
|