@artooi/ag-ui-web-component 0.38.0 → 0.40.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 +515 -1
- package/README.md +232 -56
- package/dist/ag-ui-web-component.bundle.js +370 -139
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +17 -14
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/activity_registry.d.ts +46 -0
- package/dist/core/activity_registry.d.ts.map +1 -0
- package/dist/core/ag_ui_chat.d.ts +23 -10
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +28 -0
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/client_seed.d.ts +27 -0
- package/dist/core/client_seed.d.ts.map +1 -0
- package/dist/core/read_max_tool_rounds.d.ts +9 -0
- package/dist/core/read_max_tool_rounds.d.ts.map +1 -0
- package/dist/core/run_handlers.d.ts +80 -0
- package/dist/core/run_handlers.d.ts.map +1 -0
- package/dist/core/storage_scope.d.ts +110 -0
- package/dist/core/storage_scope.d.ts.map +1 -0
- package/dist/core/tool_outcome.d.ts +2 -3
- package/dist/core/tool_outcome.d.ts.map +1 -1
- package/dist/core/utils.d.ts +46 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/index.js +16268 -14747
- package/dist/index.js.map +4 -4
- package/dist/skills/skill_catalog.d.ts +67 -0
- package/dist/skills/skill_catalog.d.ts.map +1 -0
- package/dist/skills/skill_name_from.d.ts +12 -0
- package/dist/skills/skill_name_from.d.ts.map +1 -0
- package/dist/tools/tool_catalog.d.ts +79 -0
- package/dist/tools/tool_catalog.d.ts.map +1 -0
- package/dist/tools/tool_dispatch.d.ts +104 -0
- package/dist/tools/tool_dispatch.d.ts.map +1 -0
- package/dist/ui/composer/attachment_tray.d.ts.map +1 -1
- package/dist/ui/composer/auto_grow.d.ts +10 -0
- package/dist/ui/composer/auto_grow.d.ts.map +1 -0
- package/dist/ui/composer/composer_attachments.d.ts +57 -0
- package/dist/ui/composer/composer_attachments.d.ts.map +1 -0
- package/dist/ui/composer/composer_voice.d.ts +48 -0
- package/dist/ui/composer/composer_voice.d.ts.map +1 -0
- package/dist/ui/composer/voice_input.d.ts.map +1 -1
- package/dist/ui/excerpts/transcript_quote_offer.d.ts +57 -0
- package/dist/ui/excerpts/transcript_quote_offer.d.ts.map +1 -0
- package/dist/ui/fill_ui_string.d.ts +32 -0
- package/dist/ui/fill_ui_string.d.ts.map +1 -0
- package/dist/ui/history/conversation_history.d.ts +203 -0
- package/dist/ui/history/conversation_history.d.ts.map +1 -0
- package/dist/ui/history/relative_time.d.ts.map +1 -1
- package/dist/ui/history/thread_drawer.d.ts +0 -14
- package/dist/ui/history/thread_drawer.d.ts.map +1 -1
- package/dist/ui/interrupts/confirmation_card.d.ts.map +1 -1
- package/dist/ui/interrupts/pending_decision.d.ts +23 -0
- package/dist/ui/interrupts/pending_decision.d.ts.map +1 -0
- package/dist/ui/placement/is_collapsible_placement.d.ts +18 -0
- package/dist/ui/placement/is_collapsible_placement.d.ts.map +1 -0
- package/dist/ui/placement/is_draggable_placement.d.ts +9 -0
- package/dist/ui/placement/is_draggable_placement.d.ts.map +1 -0
- package/dist/ui/placement/launcher_drag.d.ts +6 -0
- package/dist/ui/placement/launcher_drag.d.ts.map +1 -1
- package/dist/ui/placement/panel_placement.d.ts +119 -0
- package/dist/ui/placement/panel_placement.d.ts.map +1 -0
- package/dist/ui/progress/run_announcer.d.ts +41 -0
- package/dist/ui/progress/run_announcer.d.ts.map +1 -0
- package/dist/ui/progress/subagent_progress.d.ts +58 -0
- package/dist/ui/progress/subagent_progress.d.ts.map +1 -0
- package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
- package/dist/ui/shell/adopt_styles.d.ts +23 -0
- package/dist/ui/shell/adopt_styles.d.ts.map +1 -0
- package/dist/ui/shell/glyph_slot.d.ts +7 -0
- package/dist/ui/shell/glyph_slot.d.ts.map +1 -0
- package/dist/ui/shell/header_button.d.ts +10 -0
- package/dist/ui/shell/header_button.d.ts.map +1 -0
- package/dist/ui/shell/icon_element.d.ts +12 -0
- package/dist/ui/shell/icon_element.d.ts.map +1 -0
- package/dist/ui/shell/is_unread_badge_enabled.d.ts +10 -0
- package/dist/ui/shell/is_unread_badge_enabled.d.ts.map +1 -0
- package/dist/ui/shell/read_launcher_icon_url.d.ts +8 -0
- package/dist/ui/shell/read_launcher_icon_url.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/transcript/answer_actions.d.ts +39 -0
- package/dist/ui/transcript/answer_actions.d.ts.map +1 -0
- package/dist/ui/transcript/answer_stream.d.ts +56 -0
- package/dist/ui/transcript/answer_stream.d.ts.map +1 -0
- package/dist/ui/transcript/render_or_warn.d.ts +16 -0
- package/dist/ui/transcript/render_or_warn.d.ts.map +1 -0
- package/dist/ui/transcript/starter_chips.d.ts +21 -0
- package/dist/ui/transcript/starter_chips.d.ts.map +1 -0
- package/dist/ui/transcript/transcript.d.ts +171 -0
- package/dist/ui/transcript/transcript.d.ts.map +1 -0
- package/dist/ui/ui_strings.d.ts +29 -1
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +17 -15
- package/src/core/activity_registry.ts +133 -0
- package/src/core/ag_ui_chat.ts +945 -4419
- package/src/core/agui_client.ts +172 -16
- package/src/core/client_seed.ts +27 -0
- package/src/core/read_max_tool_rounds.ts +13 -0
- package/src/core/remote_conversation_store.ts +1 -1
- package/src/core/run_handlers.ts +501 -0
- package/src/core/storage_scope.ts +279 -0
- package/src/core/tool_outcome.ts +5 -3
- package/src/core/utils.ts +109 -0
- package/src/skills/skill_catalog.ts +166 -0
- package/src/skills/skill_name_from.ts +19 -0
- package/src/tools/tool_catalog.ts +354 -0
- package/src/tools/tool_dispatch.ts +521 -0
- package/src/ui/composer/attachment_tray.ts +6 -5
- package/src/ui/composer/auto_grow.ts +12 -0
- package/src/ui/composer/composer_attachments.ts +324 -0
- package/src/ui/composer/composer_voice.ts +93 -0
- package/src/ui/composer/voice_input.ts +4 -4
- package/src/ui/excerpts/transcript_quote_offer.ts +198 -0
- package/src/ui/fill_ui_string.ts +41 -0
- package/src/ui/history/conversation_history.ts +699 -0
- package/src/ui/history/relative_time.ts +5 -4
- package/src/ui/history/thread_drawer.ts +3 -50
- package/src/ui/interrupts/confirmation_card.ts +4 -2
- package/src/ui/interrupts/pending_decision.ts +34 -0
- package/src/ui/placement/is_collapsible_placement.ts +19 -0
- package/src/ui/placement/is_draggable_placement.ts +23 -0
- package/src/ui/placement/launcher_drag.ts +104 -89
- package/src/ui/placement/panel_placement.ts +1130 -0
- package/src/ui/progress/run_announcer.ts +67 -0
- package/src/ui/progress/subagent_progress.ts +201 -0
- package/src/ui/progress/tool_call_card.ts +2 -0
- package/src/ui/shell/adopt_styles.ts +28 -0
- package/src/ui/shell/glyph_slot.ts +12 -0
- package/src/ui/shell/header_button.ts +21 -0
- package/src/ui/shell/icon_element.ts +33 -0
- package/src/ui/shell/is_unread_badge_enabled.ts +11 -0
- package/src/ui/shell/read_launcher_icon_url.ts +9 -0
- package/src/ui/styles.ts +301 -70
- package/src/ui/transcript/answer_actions.ts +139 -0
- package/src/ui/transcript/answer_stream.ts +123 -0
- package/src/ui/transcript/render_or_warn.ts +22 -0
- package/src/ui/transcript/starter_chips.ts +42 -0
- package/src/ui/transcript/transcript.ts +429 -0
- package/src/ui/ui_strings.ts +38 -1
- package/src/version.ts +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fillUiString } from "../fill_ui_string.js";
|
|
1
2
|
import { DEFAULT_UI_STRINGS, type UiStrings } from "../ui_strings.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -24,17 +25,17 @@ export function relativeTime(
|
|
|
24
25
|
}
|
|
25
26
|
const minutes = Math.round(seconds / 60);
|
|
26
27
|
if (minutes < 60) {
|
|
27
|
-
return strings.minutesAgo
|
|
28
|
+
return fillUiString(strings.minutesAgo, { n: minutes });
|
|
28
29
|
}
|
|
29
30
|
const hours = Math.round(minutes / 60);
|
|
30
31
|
if (hours < 24) {
|
|
31
|
-
return strings.hoursAgo
|
|
32
|
+
return fillUiString(strings.hoursAgo, { n: hours });
|
|
32
33
|
}
|
|
33
34
|
const days = Math.round(hours / 24);
|
|
34
35
|
if (days < 7) {
|
|
35
|
-
return strings.daysAgo
|
|
36
|
+
return fillUiString(strings.daysAgo, { n: days });
|
|
36
37
|
}
|
|
37
|
-
return strings.weeksAgo
|
|
38
|
+
return fillUiString(strings.weeksAgo, { n: Math.round(days / 7) });
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -20,15 +20,6 @@ export interface ThreadDrawerCallbacks {
|
|
|
20
20
|
readonly onRename: (threadId: string, title: string) => void;
|
|
21
21
|
/** A row was deleted (after the inline confirm). */
|
|
22
22
|
readonly onDelete: (threadId: string) => void;
|
|
23
|
-
/**
|
|
24
|
-
* The list opened or closed.
|
|
25
|
-
*
|
|
26
|
-
* There are five ways out of it -- the close control, the backdrop, Escape,
|
|
27
|
-
* picking a row and starting a new chat -- and only two of them go through
|
|
28
|
-
* the host. A docked list moves the transcript over, so the host has to know
|
|
29
|
-
* about all five or the transcript stays shifted around nothing.
|
|
30
|
-
*/
|
|
31
|
-
readonly onVisibility?: (open: boolean) => void;
|
|
32
23
|
}
|
|
33
24
|
|
|
34
25
|
/**
|
|
@@ -62,17 +53,6 @@ export class ThreadDrawer {
|
|
|
62
53
|
#filter: HTMLInputElement;
|
|
63
54
|
/** The current filter text, lowercased once rather than per row. */
|
|
64
55
|
#query = "";
|
|
65
|
-
/**
|
|
66
|
-
* Whether the list covers the conversation, or sits beside it.
|
|
67
|
-
*
|
|
68
|
-
* Everything a dialog implies follows from this and nothing else does: the
|
|
69
|
-
* modal role, the focus trap, and taking focus on open. A list docked beside
|
|
70
|
-
* a full-page conversation is a region of that page -- telling assistive
|
|
71
|
-
* technology to ignore the rest of the document while it is showing would be
|
|
72
|
-
* a lie, and stealing focus into it would interrupt someone mid-sentence to
|
|
73
|
-
* announce a list they can already see.
|
|
74
|
-
*/
|
|
75
|
-
#modal = true;
|
|
76
56
|
|
|
77
57
|
/** The element focused before the drawer opened, restored on close. */
|
|
78
58
|
#lastFocused: HTMLElement | null = null;
|
|
@@ -186,21 +166,6 @@ export class ThreadDrawer {
|
|
|
186
166
|
this.#renderList();
|
|
187
167
|
}
|
|
188
168
|
|
|
189
|
-
/**
|
|
190
|
-
* Whether the list is a dialog over the conversation or a rail beside it.
|
|
191
|
-
* The host decides, because only the placement knows whether there is room.
|
|
192
|
-
*/
|
|
193
|
-
setModal(modal: boolean): void {
|
|
194
|
-
this.#modal = modal;
|
|
195
|
-
if (modal) {
|
|
196
|
-
this.#panel.setAttribute("role", "dialog");
|
|
197
|
-
this.#panel.setAttribute("aria-modal", "true");
|
|
198
|
-
} else {
|
|
199
|
-
this.#panel.setAttribute("role", "region");
|
|
200
|
-
this.#panel.removeAttribute("aria-modal");
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
169
|
isOpen(): boolean {
|
|
205
170
|
return !this.element.hidden;
|
|
206
171
|
}
|
|
@@ -213,10 +178,7 @@ export class ThreadDrawer {
|
|
|
213
178
|
// the panel (its first control) so keyboard users land inside the dialog.
|
|
214
179
|
this.#lastFocused = this.#activeElement() as HTMLElement | null;
|
|
215
180
|
this.element.hidden = false;
|
|
216
|
-
|
|
217
|
-
this.#newButton.focus();
|
|
218
|
-
}
|
|
219
|
-
this.#callbacks.onVisibility?.(true);
|
|
181
|
+
this.#newButton.focus();
|
|
220
182
|
}
|
|
221
183
|
|
|
222
184
|
close(): void {
|
|
@@ -224,13 +186,8 @@ export class ThreadDrawer {
|
|
|
224
186
|
return;
|
|
225
187
|
}
|
|
226
188
|
this.element.hidden = true;
|
|
227
|
-
|
|
228
|
-
// back from wherever the user has since put it.
|
|
229
|
-
if (this.#modal) {
|
|
230
|
-
this.#lastFocused?.focus();
|
|
231
|
-
}
|
|
189
|
+
this.#lastFocused?.focus();
|
|
232
190
|
this.#lastFocused = null;
|
|
233
|
-
this.#callbacks.onVisibility?.(false);
|
|
234
191
|
}
|
|
235
192
|
|
|
236
193
|
toggle(): void {
|
|
@@ -253,11 +210,7 @@ export class ThreadDrawer {
|
|
|
253
210
|
this.close();
|
|
254
211
|
return;
|
|
255
212
|
}
|
|
256
|
-
|
|
257
|
-
// convention -- but nothing beyond this point applies to one. Tabbing out
|
|
258
|
-
// of a list that sits beside the conversation should reach the
|
|
259
|
-
// conversation, which is the whole difference.
|
|
260
|
-
if (event.key !== "Tab" || !this.#modal) {
|
|
213
|
+
if (event.key !== "Tab") {
|
|
261
214
|
return;
|
|
262
215
|
}
|
|
263
216
|
const focusables = Array.from(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fillUiString } from "../fill_ui_string.js";
|
|
1
2
|
import { DEFAULT_UI_STRINGS, type UiStrings } from "../ui_strings.js";
|
|
2
3
|
|
|
3
4
|
/** What the inline confirmation card displays. */
|
|
@@ -77,7 +78,8 @@ export function requestConfirmation(
|
|
|
77
78
|
const body = document.createElement("div");
|
|
78
79
|
body.className = "confirm-body";
|
|
79
80
|
body.setAttribute("part", "confirm-body");
|
|
80
|
-
body.textContent =
|
|
81
|
+
body.textContent =
|
|
82
|
+
request.message ?? fillUiString(strings.confirmRun, { tool: request.toolName });
|
|
81
83
|
|
|
82
84
|
const args = document.createElement("pre");
|
|
83
85
|
args.className = "confirm-args";
|
|
@@ -94,7 +96,7 @@ export function requestConfirmation(
|
|
|
94
96
|
const always =
|
|
95
97
|
options.onAlwaysAllow === undefined
|
|
96
98
|
? null
|
|
97
|
-
: actionButton("always", strings.confirmAlways
|
|
99
|
+
: actionButton("always", fillUiString(strings.confirmAlways, { tool: request.toolName }));
|
|
98
100
|
const confirm = actionButton("confirm", strings.confirm);
|
|
99
101
|
|
|
100
102
|
let settled = false;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The decision a run is suspended on, and the one way to abandon it.
|
|
3
|
+
*
|
|
4
|
+
* A run pauses on a person in three places -- a client-side confirmation, a
|
|
5
|
+
* batch of server interrupts, and the `ask_user` question -- and a Stop has to
|
|
6
|
+
* reach whichever of them is open. Each used to assign the element's own abort
|
|
7
|
+
* controller directly, so four places wrote one field and nothing said which
|
|
8
|
+
* of them owned it. Holding it here gives it one owner: a wait opens, the Stop
|
|
9
|
+
* aborts what is open, and the wait closes when it is answered.
|
|
10
|
+
*
|
|
11
|
+
* Owned one-to-one by an `<ag-ui-chat>`, and holding no state outside the
|
|
12
|
+
* instance.
|
|
13
|
+
*/
|
|
14
|
+
export class PendingDecision {
|
|
15
|
+
// Aborting this dismisses (declines) an open confirmation card when the run
|
|
16
|
+
// is cancelled while the card awaits a decision. One controller per wait.
|
|
17
|
+
#controller: AbortController | null = null;
|
|
18
|
+
|
|
19
|
+
/** Start waiting on a person; aborting the returned signal abandons the wait. */
|
|
20
|
+
open(): AbortSignal {
|
|
21
|
+
this.#controller = new AbortController();
|
|
22
|
+
return this.#controller.signal;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The wait was answered; a later Stop has nothing of this wait's to abandon. */
|
|
26
|
+
close(): void {
|
|
27
|
+
this.#controller = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Abandon whatever wait is open, resolving it as declined. A no-op when none is. */
|
|
31
|
+
abort(): void {
|
|
32
|
+
this.#controller?.abort();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `placement` has a collapsed state at all.
|
|
3
|
+
*
|
|
4
|
+
* `page` does not. It is a dedicated route rather than a panel sitting on
|
|
5
|
+
* someone else's page, so there is no "away" for it to go to: collapsing it
|
|
6
|
+
* left a strip of application chrome fixed over a route that no longer had an
|
|
7
|
+
* owner. It is also the placement that hides the launcher, so the usual way
|
|
8
|
+
* back does not exist here.
|
|
9
|
+
*
|
|
10
|
+
* The header hides its collapse control under this placement, but a control
|
|
11
|
+
* removed from the UI is not a state removed from the model -- the property,
|
|
12
|
+
* the attribute and a value restored from storage all still reach it. This is
|
|
13
|
+
* what the reachable paths are gated on; the stylesheet covers the one path
|
|
14
|
+
* that never passes through here, an attribute written straight onto the
|
|
15
|
+
* element.
|
|
16
|
+
*/
|
|
17
|
+
export function isCollapsiblePlacement(placement: string | null): boolean {
|
|
18
|
+
return placement !== "page";
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placements whose launcher can be dragged. The rest have nowhere to put it:
|
|
3
|
+
* a sidebar collapses to a full-height edge rail, "embedded" and "page" hide
|
|
4
|
+
* the launcher entirely and keep their header bar, and a full-bleed panel
|
|
5
|
+
* covers the screen it would be opening into.
|
|
6
|
+
*/
|
|
7
|
+
const DRAGGABLE_PLACEMENTS: ReadonlySet<string | null> = new Set([
|
|
8
|
+
null,
|
|
9
|
+
"",
|
|
10
|
+
"floating",
|
|
11
|
+
"bottom-left",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether `placement` is one whose launcher can be dragged.
|
|
16
|
+
*
|
|
17
|
+
* Asked by two owners: the placement, deciding whether a gesture may move the
|
|
18
|
+
* widget, and the element, deciding whether a first mount starts collapsed --
|
|
19
|
+
* the corner placements are the two with a launcher to collapse to.
|
|
20
|
+
*/
|
|
21
|
+
export function isDraggablePlacement(placement: string | null): boolean {
|
|
22
|
+
return DRAGGABLE_PLACEMENTS.has(placement);
|
|
23
|
+
}
|
|
@@ -21,6 +21,12 @@ export interface LauncherDragOptions {
|
|
|
21
21
|
* per pointer move, and never per key repeat.
|
|
22
22
|
*/
|
|
23
23
|
readonly commit: (left: number, top: number) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Ends the launcher's own listeners when aborted. The launcher outlives the
|
|
26
|
+
* element's connection to the document and this is called on every connect,
|
|
27
|
+
* so without it a re-inserted element moved one arrow press two steps.
|
|
28
|
+
*/
|
|
29
|
+
readonly signal: AbortSignal;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/**
|
|
@@ -55,6 +61,7 @@ const COARSE_STEP = 64;
|
|
|
55
61
|
*/
|
|
56
62
|
export function enableLauncherDrag(launcher: HTMLElement, options: LauncherDragOptions): void {
|
|
57
63
|
let suppressClick = false;
|
|
64
|
+
const { signal } = options;
|
|
58
65
|
|
|
59
66
|
launcher.addEventListener(
|
|
60
67
|
"click",
|
|
@@ -70,72 +77,76 @@ export function enableLauncherDrag(launcher: HTMLElement, options: LauncherDragO
|
|
|
70
77
|
event.stopPropagation();
|
|
71
78
|
event.preventDefault();
|
|
72
79
|
},
|
|
73
|
-
true,
|
|
80
|
+
{ capture: true, signal },
|
|
74
81
|
);
|
|
75
82
|
|
|
76
|
-
launcher.addEventListener(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const start = options.rect();
|
|
84
|
-
const originX = event.clientX;
|
|
85
|
-
const originY = event.clientY;
|
|
86
|
-
let dragging = false;
|
|
87
|
-
|
|
88
|
-
const onMove = (move: PointerEvent): void => {
|
|
89
|
-
const dx = move.clientX - originX;
|
|
90
|
-
const dy = move.clientY - originY;
|
|
91
|
-
if (!dragging && Math.hypot(dx, dy) < DRAG_THRESHOLD) {
|
|
83
|
+
launcher.addEventListener(
|
|
84
|
+
"pointerdown",
|
|
85
|
+
(event: PointerEvent) => {
|
|
86
|
+
// Any suppression left armed by a drag that ended elsewhere dies here,
|
|
87
|
+
// before it can reach a click belonging to this new gesture.
|
|
88
|
+
suppressClick = false;
|
|
89
|
+
if (!options.enabled()) {
|
|
92
90
|
return;
|
|
93
91
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// travel would compound.
|
|
99
|
-
const next = clampLauncher(
|
|
100
|
-
{ ...start, left: start.left + dx, top: start.top + dy },
|
|
101
|
-
options.viewport(),
|
|
102
|
-
);
|
|
103
|
-
options.apply(next.left, next.top);
|
|
104
|
-
};
|
|
92
|
+
const start = options.rect();
|
|
93
|
+
const originX = event.clientX;
|
|
94
|
+
const originY = event.clientY;
|
|
95
|
+
let dragging = false;
|
|
105
96
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
left: start.left +
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
options.
|
|
122
|
-
|
|
123
|
-
options.commit(next.left, next.top);
|
|
124
|
-
};
|
|
97
|
+
const onMove = (move: PointerEvent): void => {
|
|
98
|
+
const dx = move.clientX - originX;
|
|
99
|
+
const dy = move.clientY - originY;
|
|
100
|
+
if (!dragging && Math.hypot(dx, dy) < DRAG_THRESHOLD) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
dragging = true;
|
|
104
|
+
launcher.setAttribute("data-dragging", "true");
|
|
105
|
+
// Measured from the box the press started on, never from the live one:
|
|
106
|
+
// reading it each move would chase the launcher as it moves and the
|
|
107
|
+
// travel would compound.
|
|
108
|
+
const next = clampLauncher(
|
|
109
|
+
{ ...start, left: start.left + dx, top: start.top + dy },
|
|
110
|
+
options.viewport(),
|
|
111
|
+
);
|
|
112
|
+
options.apply(next.left, next.top);
|
|
113
|
+
};
|
|
125
114
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
115
|
+
const onUp = (up: PointerEvent): void => {
|
|
116
|
+
window.removeEventListener("pointermove", onMove);
|
|
117
|
+
window.removeEventListener("pointerup", onUp);
|
|
118
|
+
window.removeEventListener("pointercancel", onUp);
|
|
119
|
+
if (!dragging) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
launcher.removeAttribute("data-dragging");
|
|
123
|
+
suppressClick = true;
|
|
124
|
+
const next = clampLauncher(
|
|
125
|
+
{
|
|
126
|
+
...start,
|
|
127
|
+
left: start.left + (up.clientX - originX),
|
|
128
|
+
top: start.top + (up.clientY - originY),
|
|
129
|
+
},
|
|
130
|
+
options.viewport(),
|
|
131
|
+
);
|
|
132
|
+
options.commit(next.left, next.top);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Listeners on `window`, not the launcher: a fast drag outruns the pointer
|
|
136
|
+
// and would otherwise strand it mid-move with no pointerup.
|
|
137
|
+
//
|
|
138
|
+
// pointercancel matters on touch, where it is routine rather than
|
|
139
|
+
// exceptional: the browser takes the pointer back for a scroll or a system
|
|
140
|
+
// gesture and never sends pointerup. Without this the move listeners stay
|
|
141
|
+
// attached and the drag stamp never clears, which leaves the launcher
|
|
142
|
+
// following a finger that has stopped and the element believing a gesture
|
|
143
|
+
// is still in flight.
|
|
144
|
+
window.addEventListener("pointermove", onMove);
|
|
145
|
+
window.addEventListener("pointerup", onUp);
|
|
146
|
+
window.addEventListener("pointercancel", onUp);
|
|
147
|
+
},
|
|
148
|
+
{ signal },
|
|
149
|
+
);
|
|
139
150
|
|
|
140
151
|
// The position this key gesture has applied but not yet persisted. The
|
|
141
152
|
// pointer path can commit inline because a drag has one unambiguous end; a
|
|
@@ -155,37 +166,41 @@ export function enableLauncherDrag(launcher: HTMLElement, options: LauncherDragO
|
|
|
155
166
|
// Keyboard parity: a pointer-only move is unreachable without a mouse, and
|
|
156
167
|
// the launcher has no equivalent control elsewhere. Arrow keys are free on a
|
|
157
168
|
// button, so Enter and Space still expand.
|
|
158
|
-
launcher.addEventListener(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
moved
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
169
|
+
launcher.addEventListener(
|
|
170
|
+
"keydown",
|
|
171
|
+
(event: KeyboardEvent) => {
|
|
172
|
+
if (!options.enabled()) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const step = event.shiftKey ? COARSE_STEP : STEP;
|
|
176
|
+
const rect = options.rect();
|
|
177
|
+
let moved: { left: number; top: number } | null = null;
|
|
178
|
+
if (event.key === "ArrowLeft") {
|
|
179
|
+
moved = { left: rect.left - step, top: rect.top };
|
|
180
|
+
} else if (event.key === "ArrowRight") {
|
|
181
|
+
moved = { left: rect.left + step, top: rect.top };
|
|
182
|
+
} else if (event.key === "ArrowUp") {
|
|
183
|
+
moved = { left: rect.left, top: rect.top - step };
|
|
184
|
+
} else if (event.key === "ArrowDown") {
|
|
185
|
+
moved = { left: rect.left, top: rect.top + step };
|
|
186
|
+
}
|
|
187
|
+
if (moved === null) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
const next = clampLauncher({ ...rect, ...moved }, options.viewport());
|
|
192
|
+
// Live feedback per key event, persistence only when the gesture ends: a
|
|
193
|
+
// held arrow repeats at the OS rate, and committing here would put that
|
|
194
|
+
// many storage writes behind a single press.
|
|
195
|
+
options.apply(next.left, next.top);
|
|
196
|
+
pending = next;
|
|
197
|
+
},
|
|
198
|
+
{ signal },
|
|
199
|
+
);
|
|
185
200
|
|
|
186
201
|
// The key coming up ends the gesture, mirroring `pointerup`. `blur` closes
|
|
187
202
|
// one whose keyup never arrives here -- focus moved on mid-press -- because a
|
|
188
203
|
// position applied but never committed is a move the host silently forgets.
|
|
189
|
-
launcher.addEventListener("keyup", settle);
|
|
190
|
-
launcher.addEventListener("blur", settle);
|
|
204
|
+
launcher.addEventListener("keyup", settle, { signal });
|
|
205
|
+
launcher.addEventListener("blur", settle, { signal });
|
|
191
206
|
}
|