@artooi/ag-ui-web-component 0.32.0 → 0.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +194 -1
- package/README.md +182 -23
- package/dist/ag-ui-web-component.bundle.js +360 -121
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/index.js +1275 -390
- package/dist/index.js.map +4 -4
- package/dist/ui/clamp_launcher.d.ts +15 -0
- package/dist/ui/clamp_launcher.d.ts.map +1 -0
- package/dist/ui/copy_payload.d.ts +30 -0
- package/dist/ui/copy_payload.d.ts.map +1 -0
- package/dist/ui/launcher_drag.d.ts +43 -0
- package/dist/ui/launcher_drag.d.ts.map +1 -0
- package/dist/ui/launcher_placement.d.ts +64 -0
- package/dist/ui/launcher_placement.d.ts.map +1 -0
- package/dist/ui/message_actions.d.ts +30 -2
- package/dist/ui/message_actions.d.ts.map +1 -1
- package/dist/ui/resize_handle.d.ts +32 -26
- package/dist/ui/resize_handle.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +12 -0
- package/src/core/ag_ui_chat.ts +480 -24
- package/src/ui/clamp_launcher.ts +20 -0
- package/src/ui/copy_payload.ts +156 -0
- package/src/ui/launcher_drag.ts +182 -0
- package/src/ui/launcher_placement.ts +143 -0
- package/src/ui/message_actions.ts +93 -19
- package/src/ui/resize_handle.ts +109 -73
- package/src/ui/styles.ts +299 -82
- package/src/version.ts +1 -1
package/src/ui/resize_handle.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which edges a grip drags. An axis left out is an axis the grip does not
|
|
3
|
+
* move: the left-edge grip is `{ x: "left" }`, the top-right corner is
|
|
4
|
+
* `{ x: "right", y: "top" }`.
|
|
5
|
+
*/
|
|
6
|
+
export interface ResizeGrip {
|
|
7
|
+
readonly x?: "left" | "right";
|
|
8
|
+
readonly y?: "top" | "bottom";
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
/** Which edges the layout holds still while the panel changes size. */
|
|
2
12
|
export interface ResizeAnchor {
|
|
3
13
|
/** The horizontal edge that does not move. */
|
|
@@ -8,7 +18,7 @@ export interface ResizeAnchor {
|
|
|
8
18
|
|
|
9
19
|
/**
|
|
10
20
|
* What the current placement allows: both axes, width only, or nothing. Which
|
|
11
|
-
*
|
|
21
|
+
* edges a given grip drags is separate, and fixed when the grip is built.
|
|
12
22
|
*/
|
|
13
23
|
export type ResizeAxis = "none" | "width" | "both";
|
|
14
24
|
|
|
@@ -18,7 +28,7 @@ export interface ResizeSize {
|
|
|
18
28
|
readonly height?: number;
|
|
19
29
|
}
|
|
20
30
|
|
|
21
|
-
/** The panel's position on screen
|
|
31
|
+
/** The panel's position on screen, in viewport coordinates. */
|
|
22
32
|
export interface PanelRect {
|
|
23
33
|
readonly left: number;
|
|
24
34
|
readonly top: number;
|
|
@@ -34,23 +44,16 @@ export interface ResizeOptions {
|
|
|
34
44
|
* would survive the host switching to a docked or full-bleed layout.
|
|
35
45
|
*/
|
|
36
46
|
readonly axis: () => ResizeAxis;
|
|
37
|
-
/**
|
|
38
|
-
* Which edges the layout is holding still, measured at the moment of the drag
|
|
39
|
-
* rather than derived from `placement`: a floating panel is pinned
|
|
40
|
-
* bottom-right, while an embedded one goes wherever the host's CSS puts it,
|
|
41
|
-
* so `placement` alone cannot answer the question.
|
|
42
|
-
*/
|
|
43
|
-
readonly anchor: () => ResizeAnchor;
|
|
44
47
|
/** The panel's current bounding box. */
|
|
45
48
|
readonly rect: () => PanelRect;
|
|
46
|
-
/** Apply a
|
|
47
|
-
readonly apply: (
|
|
49
|
+
/** Apply a box (the host decides what that costs in properties). */
|
|
50
|
+
readonly apply: (box: PanelRect) => void;
|
|
48
51
|
/**
|
|
49
52
|
* Called once per completed resize, for persistence: on `pointerup` for a
|
|
50
53
|
* drag, and when the key comes up (or focus leaves the handle) for a key
|
|
51
54
|
* press. Never per pointer move, and never per key repeat.
|
|
52
55
|
*/
|
|
53
|
-
readonly commit: (
|
|
56
|
+
readonly commit: (box: PanelRect) => void;
|
|
54
57
|
/** Accessible label. */
|
|
55
58
|
readonly label: string;
|
|
56
59
|
}
|
|
@@ -59,68 +62,63 @@ export interface ResizeOptions {
|
|
|
59
62
|
const MIN_WIDTH = 280;
|
|
60
63
|
const MIN_HEIGHT = 240;
|
|
61
64
|
|
|
65
|
+
/** Keyboard step, and the larger one Shift asks for. */
|
|
66
|
+
const STEP = 16;
|
|
67
|
+
const COARSE_STEP = 64;
|
|
68
|
+
|
|
62
69
|
/**
|
|
63
|
-
* A drag handle that resizes the chat panel.
|
|
70
|
+
* A drag handle that resizes the chat panel from one edge or corner.
|
|
64
71
|
*
|
|
65
|
-
*
|
|
72
|
+
* The rule that keeps it correct is that **the edge a grip does not drag is the
|
|
73
|
+
* one that stays put**. That is the whole model, and it is what lets the same
|
|
74
|
+
* code serve all eight grips: the left-edge grip moves the left edge and holds
|
|
75
|
+
* the right, the right-edge grip does the reverse, a corner does both axes.
|
|
66
76
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* so an inline dimension would outrank and fight them — a sidebar would keep
|
|
74
|
-
* its dragged width after switching to fullscreen. Writing the property
|
|
75
|
-
* leaves placement the final say.
|
|
77
|
+
* Which edge the *layout* pins is a different question and is deliberately not
|
|
78
|
+
* asked here. It matters only to the host, which has to rewrite its own
|
|
79
|
+
* position when a grip drags the very edge the layout was holding still --
|
|
80
|
+
* dragging the pinned edge of a panel is a move as much as a resize. The
|
|
81
|
+
* handle reports the box it wants; what that costs in CSS properties is the
|
|
82
|
+
* host's problem.
|
|
76
83
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
84
|
+
* An earlier version took the anchor and derived the direction from it, which
|
|
85
|
+
* is where the asymmetries lived: whether an arrow key grew or shrank the
|
|
86
|
+
* panel depended on which corner the single grip had been placed on. With the
|
|
87
|
+
* grip stated outright, an arrow simply moves the edge it names.
|
|
79
88
|
*/
|
|
80
|
-
export function createResizeHandle(options: ResizeOptions): HTMLDivElement {
|
|
89
|
+
export function createResizeHandle(grip: ResizeGrip, options: ResizeOptions): HTMLDivElement {
|
|
81
90
|
const handle = document.createElement("div");
|
|
82
|
-
handle.className =
|
|
83
|
-
handle.setAttribute("part",
|
|
91
|
+
handle.className = `resize-handle resize-handle--${gripName(grip)}`;
|
|
92
|
+
handle.setAttribute("part", `resize-handle resize-handle-${gripName(grip)}`);
|
|
93
|
+
// A separator with an orientation: an edge grip splits along one axis, and a
|
|
94
|
+
// corner has no single one to report.
|
|
84
95
|
handle.setAttribute("role", "separator");
|
|
96
|
+
if (grip.x === undefined) {
|
|
97
|
+
handle.setAttribute("aria-orientation", "horizontal");
|
|
98
|
+
} else if (grip.y === undefined) {
|
|
99
|
+
handle.setAttribute("aria-orientation", "vertical");
|
|
100
|
+
}
|
|
85
101
|
handle.setAttribute("aria-label", options.label);
|
|
86
102
|
handle.tabIndex = 0;
|
|
87
103
|
|
|
88
|
-
/** The size implied by a pointer at (x, y), given which edges are pinned. */
|
|
89
|
-
const sizeAt = (
|
|
90
|
-
axis: ResizeAxis,
|
|
91
|
-
anchor: ResizeAnchor,
|
|
92
|
-
rect: PanelRect,
|
|
93
|
-
x: number,
|
|
94
|
-
y: number,
|
|
95
|
-
): ResizeSize => {
|
|
96
|
-
const width = anchor.x === "right" ? rect.right - x : x - rect.left;
|
|
97
|
-
const clamped: ResizeSize = { width: Math.max(MIN_WIDTH, width) };
|
|
98
|
-
if (axis !== "both") {
|
|
99
|
-
return clamped;
|
|
100
|
-
}
|
|
101
|
-
const height = anchor.y === "bottom" ? rect.bottom - y : y - rect.top;
|
|
102
|
-
return { ...clamped, height: Math.max(MIN_HEIGHT, height) };
|
|
103
|
-
};
|
|
104
|
-
|
|
105
104
|
handle.addEventListener("pointerdown", (event: PointerEvent) => {
|
|
106
105
|
const axis = options.axis();
|
|
107
|
-
if (axis === "none") {
|
|
106
|
+
if (axis === "none" || !movable(grip, axis)) {
|
|
108
107
|
return;
|
|
109
108
|
}
|
|
110
|
-
// Captured once: the
|
|
111
|
-
// them live would chase the panel as it resizes.
|
|
112
|
-
const anchor = options.anchor();
|
|
109
|
+
// Captured once: the edges this grip is not dragging cannot move during
|
|
110
|
+
// the drag, and reading them live would chase the panel as it resizes.
|
|
113
111
|
const rect = options.rect();
|
|
114
112
|
|
|
115
113
|
const onMove = (move: PointerEvent): void => {
|
|
116
|
-
options.apply(
|
|
114
|
+
options.apply(boxAt(grip, axis, rect, move.clientX, move.clientY));
|
|
117
115
|
};
|
|
118
116
|
|
|
119
117
|
const onUp = (up: PointerEvent): void => {
|
|
120
118
|
window.removeEventListener("pointermove", onMove);
|
|
121
119
|
window.removeEventListener("pointerup", onUp);
|
|
122
120
|
handle.removeAttribute("data-dragging");
|
|
123
|
-
options.commit(
|
|
121
|
+
options.commit(boxAt(grip, axis, rect, up.clientX, up.clientY));
|
|
124
122
|
};
|
|
125
123
|
|
|
126
124
|
handle.setAttribute("data-dragging", "true");
|
|
@@ -131,49 +129,44 @@ export function createResizeHandle(options: ResizeOptions): HTMLDivElement {
|
|
|
131
129
|
event.preventDefault();
|
|
132
130
|
});
|
|
133
131
|
|
|
134
|
-
// The
|
|
132
|
+
// The box the current key gesture has applied but not yet persisted. The
|
|
135
133
|
// pointer path can commit inline because a drag has one unambiguous end;
|
|
136
134
|
// a key press does not, so the gesture's result is held here until it does.
|
|
137
|
-
let pending:
|
|
135
|
+
let pending: PanelRect | null = null;
|
|
138
136
|
|
|
139
137
|
/** End a key gesture: persist what it applied, once. */
|
|
140
138
|
const settle = (): void => {
|
|
141
139
|
if (pending === null) {
|
|
142
140
|
return;
|
|
143
141
|
}
|
|
144
|
-
const
|
|
142
|
+
const box = pending;
|
|
145
143
|
pending = null;
|
|
146
|
-
options.commit(
|
|
144
|
+
options.commit(box);
|
|
147
145
|
};
|
|
148
146
|
|
|
149
147
|
// Keyboard parity: a pointer-only resize is unreachable without a mouse, and
|
|
150
|
-
// this control has no equivalent elsewhere in the UI.
|
|
148
|
+
// this control has no equivalent elsewhere in the UI. An arrow moves the edge
|
|
149
|
+
// this grip names, in the direction it names -- so the same key grows one
|
|
150
|
+
// side's grip and shrinks the opposite one, which is what a pointer does too.
|
|
151
151
|
handle.addEventListener("keydown", (event: KeyboardEvent) => {
|
|
152
152
|
const axis = options.axis();
|
|
153
|
-
if (axis === "none") {
|
|
153
|
+
if (axis === "none" || !movable(grip, axis)) {
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
|
-
const
|
|
156
|
+
const step = event.shiftKey ? COARSE_STEP : STEP;
|
|
157
157
|
const rect = options.rect();
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const outward = anchor.x === "right" ? -1 : 1;
|
|
162
|
-
const width = rect.right - rect.left;
|
|
163
|
-
const height = rect.bottom - rect.top;
|
|
164
|
-
let next: ResizeSize | null = null;
|
|
165
|
-
if (event.key === "ArrowLeft") {
|
|
166
|
-
next = { width: Math.max(MIN_WIDTH, width - step * outward) };
|
|
167
|
-
} else if (event.key === "ArrowRight") {
|
|
168
|
-
next = { width: Math.max(MIN_WIDTH, width + step * outward) };
|
|
169
|
-
} else if (axis === "both" && (event.key === "ArrowUp" || event.key === "ArrowDown")) {
|
|
170
|
-
const grow = event.key === (anchor.y === "bottom" ? "ArrowUp" : "ArrowDown");
|
|
171
|
-
next = { height: Math.max(MIN_HEIGHT, height + (grow ? step : -step)) };
|
|
158
|
+
const delta = ARROWS[event.key];
|
|
159
|
+
if (delta === undefined) {
|
|
160
|
+
return;
|
|
172
161
|
}
|
|
173
|
-
|
|
162
|
+
// An arrow across this grip's fixed axis has no edge to move.
|
|
163
|
+
if ((delta.x !== 0 && grip.x === undefined) || (delta.y !== 0 && grip.y === undefined)) {
|
|
174
164
|
return;
|
|
175
165
|
}
|
|
176
166
|
event.preventDefault();
|
|
167
|
+
const x = (grip.x === "left" ? rect.left : rect.right) + delta.x * step;
|
|
168
|
+
const y = (grip.y === "top" ? rect.top : rect.bottom) + delta.y * step;
|
|
169
|
+
const next = boxAt(grip, axis, rect, x, y);
|
|
177
170
|
// Live feedback per key event, persistence only when the gesture ends:
|
|
178
171
|
// `commit` promises one call per completed resize, and a held arrow key
|
|
179
172
|
// repeats at the OS rate (20-30 events a second), so committing here would
|
|
@@ -191,3 +184,46 @@ export function createResizeHandle(options: ResizeOptions): HTMLDivElement {
|
|
|
191
184
|
|
|
192
185
|
return handle;
|
|
193
186
|
}
|
|
187
|
+
|
|
188
|
+
/** Which way each arrow key pushes the edge under it. */
|
|
189
|
+
const ARROWS: Record<string, { x: number; y: number } | undefined> = {
|
|
190
|
+
ArrowLeft: { x: -1, y: 0 },
|
|
191
|
+
ArrowRight: { x: 1, y: 0 },
|
|
192
|
+
ArrowUp: { x: 0, y: -1 },
|
|
193
|
+
ArrowDown: { x: 0, y: 1 },
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** The hyphenated name of a grip, for its class and part. */
|
|
197
|
+
export function gripName(grip: ResizeGrip): string {
|
|
198
|
+
return [grip.y, grip.x].filter((side) => side !== undefined).join("-");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Whether the current placement leaves this grip anything to move. */
|
|
202
|
+
function movable(grip: ResizeGrip, axis: ResizeAxis): boolean {
|
|
203
|
+
// A docked panel owns its height, so a grip that only moves a horizontal
|
|
204
|
+
// edge has nothing to do and must not pretend otherwise.
|
|
205
|
+
return axis === "both" || grip.x !== undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The box a pointer at (x, y) implies for this grip.
|
|
210
|
+
*
|
|
211
|
+
* Each axis is clamped by pushing the *dragged* edge back to the minimum,
|
|
212
|
+
* never by moving the edge that is supposed to be standing still: clamping the
|
|
213
|
+
* wrong one is how a panel dragged past its minimum starts travelling.
|
|
214
|
+
*/
|
|
215
|
+
function boxAt(
|
|
216
|
+
grip: ResizeGrip,
|
|
217
|
+
axis: ResizeAxis,
|
|
218
|
+
rect: PanelRect,
|
|
219
|
+
x: number,
|
|
220
|
+
y: number,
|
|
221
|
+
): PanelRect {
|
|
222
|
+
const left = grip.x === "left" ? Math.min(x, rect.right - MIN_WIDTH) : rect.left;
|
|
223
|
+
const right = grip.x === "right" ? Math.max(x, rect.left + MIN_WIDTH) : rect.right;
|
|
224
|
+
// A placement that owns its height leaves the vertical edges where they are.
|
|
225
|
+
const vertical = axis === "both";
|
|
226
|
+
const top = vertical && grip.y === "top" ? Math.min(y, rect.bottom - MIN_HEIGHT) : rect.top;
|
|
227
|
+
const bottom = vertical && grip.y === "bottom" ? Math.max(y, rect.top + MIN_HEIGHT) : rect.bottom;
|
|
228
|
+
return { left, top, right, bottom };
|
|
229
|
+
}
|