@agent-native/core 0.85.6 → 0.85.7
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/use-pinch-zoom.ts +76 -11
- package/corpus/core/src/collab/presence.ts +63 -3
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +245 -77
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +190 -13
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +16 -4
- package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +46 -3
- package/corpus/templates/design/AGENTS.md +33 -1
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +20 -73
- package/corpus/templates/design/actions/apply-motion-edit.ts +29 -2
- package/corpus/templates/design/actions/apply-visual-edit.ts +4 -2
- package/corpus/templates/design/actions/connect-localhost.ts +25 -18
- package/corpus/templates/design/actions/export-pdf.ts +8 -1
- package/corpus/templates/design/actions/export-zip.ts +10 -1
- package/corpus/templates/design/actions/get-motion-timeline.ts +26 -20
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +10 -0
- package/corpus/templates/design/actions/insert-asset.ts +190 -7
- package/corpus/templates/design/actions/open-visual-edit.ts +27 -5
- package/corpus/templates/design/actions/present-design-variants.ts +2 -2
- package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +18 -14
- package/corpus/templates/design/actions/run-design-audit.ts +7 -4
- package/corpus/templates/design/actions/write-local-file.ts +47 -22
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +221 -66
- package/corpus/templates/design/app/components/design/EditPanel.tsx +650 -237
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +526 -127
- package/corpus/templates/design/app/components/design/MotionDock.tsx +234 -46
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1497 -408
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +25 -2
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +640 -72
- package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +605 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +50 -26
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +16 -4
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +90 -103
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +111 -5
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -2
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +42 -2
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +11 -2
- package/corpus/templates/design/app/components/design/types.ts +16 -0
- package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +24 -6
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +15 -1
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +77 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +18 -0
- package/corpus/templates/design/app/lib/design-import.ts +95 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +4013 -838
- package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-inspector-fixes-mixed-selections-show-mixed-instead-of-wrong.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-keyframe-timeline-works-reliably-drag-keyframes-smoothly-pic.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-much-faster-editor-smooth-dragging-zooming-and-panel-updates.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-pen-tool-refinements-click-the-first-point-to-close-with-a-d.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-text-editing-enter-adds-a-line-break-international-ime-typin.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-undo-and-redo-are-dependable-across-agent-edits-screen-switc.md +6 -0
- package/corpus/templates/design/server/lib/design-export.ts +43 -1
- package/corpus/templates/design/shared/board-file.ts +25 -5
- package/corpus/templates/design/shared/canvas-math.ts +754 -9
- package/corpus/templates/design/shared/code-layer.ts +304 -26
- package/corpus/templates/design/shared/color-utils.ts +84 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +16 -7
- package/corpus/templates/design/shared/motion-compiler.ts +75 -31
- package/corpus/templates/design/shared/motion-timeline.ts +335 -0
- package/corpus/templates/design/shared/pen-path.ts +200 -23
- package/dist/client/use-pinch-zoom.d.ts.map +1 -1
- package/dist/client/use-pinch-zoom.js +76 -11
- package/dist/client/use-pinch-zoom.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/presence.d.ts.map +1 -1
- package/dist/collab/presence.js +54 -3
- package/dist/collab/presence.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -27,10 +27,27 @@ export function createCornerNode(point: PenPoint): PenNode {
|
|
|
27
27
|
return { point: { ...point } };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Creates a smooth (symmetric-handle) anchor by default: `handleIn` mirrors
|
|
32
|
+
* `handleOut` across `anchor`, giving the anchor a continuous tangent on
|
|
33
|
+
* both sides — this is what dragging a fresh pen anchor normally produces.
|
|
34
|
+
*
|
|
35
|
+
* Pass `{ breakSymmetry: true }` (Figma: hold Alt/Option while dragging a
|
|
36
|
+
* newly placed anchor) to break that symmetry into a cusp: `handleOut`
|
|
37
|
+
* still follows the drag, but no mirrored `handleIn` is created, so the
|
|
38
|
+
* incoming segment stays a plain corner while the outgoing segment curves
|
|
39
|
+
* independently.
|
|
40
|
+
*/
|
|
41
|
+
export function createSmoothNode(
|
|
42
|
+
anchor: PenPoint,
|
|
43
|
+
handleOut: PenPoint,
|
|
44
|
+
options?: { breakSymmetry?: boolean },
|
|
45
|
+
): PenNode {
|
|
31
46
|
return {
|
|
32
47
|
point: { ...anchor },
|
|
33
|
-
handleIn:
|
|
48
|
+
handleIn: options?.breakSymmetry
|
|
49
|
+
? undefined
|
|
50
|
+
: mirrorPoint(anchor, handleOut),
|
|
34
51
|
handleOut: { ...handleOut },
|
|
35
52
|
};
|
|
36
53
|
}
|
|
@@ -62,17 +79,58 @@ export function constrainPointTo45Degrees(
|
|
|
62
79
|
): PenPoint {
|
|
63
80
|
const dx = point.x - origin.x;
|
|
64
81
|
const dy = point.y - origin.y;
|
|
65
|
-
|
|
66
|
-
if (distance === 0) return { ...point };
|
|
82
|
+
if (dx === 0 && dy === 0) return { ...point };
|
|
67
83
|
|
|
68
84
|
const angle = Math.atan2(dy, dx);
|
|
69
85
|
const snappedAngle = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4);
|
|
86
|
+
const axisX = Math.cos(snappedAngle);
|
|
87
|
+
const axisY = Math.sin(snappedAngle);
|
|
88
|
+
|
|
89
|
+
// Project the drag vector onto the snapped axis component-wise (like
|
|
90
|
+
// Figma), rather than preserving the raw radial distance. For an
|
|
91
|
+
// axis-aligned snap (0/90/180/270) this reduces to keeping the dominant
|
|
92
|
+
// component and zeroing the other; for diagonal snaps it reduces to the
|
|
93
|
+
// usual equal-magnitude diagonal.
|
|
94
|
+
const projection = dx * axisX + dy * axisY;
|
|
70
95
|
return {
|
|
71
|
-
x: origin.x +
|
|
72
|
-
y: origin.y +
|
|
96
|
+
x: origin.x + axisX * projection,
|
|
97
|
+
y: origin.y + axisY * projection,
|
|
73
98
|
};
|
|
74
99
|
}
|
|
75
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Light pen-anchor snapping (P15): snap a candidate new-anchor point to any
|
|
103
|
+
* existing anchor point of the path currently being drawn (Figma snaps new
|
|
104
|
+
* anchors onto other anchors of the same path so you can precisely re-hit a
|
|
105
|
+
* prior point), and otherwise round to the nearest integer canvas px once
|
|
106
|
+
* the user is zoomed in to 100% or more (where sub-pixel placement is
|
|
107
|
+
* rarely intentional and hairline anti-aliasing becomes visible).
|
|
108
|
+
*
|
|
109
|
+
* This intentionally does not snap to *other* shapes/frames on the canvas —
|
|
110
|
+
* that's the existing computeMoveSnap/grid machinery's job for whole-object
|
|
111
|
+
* moves, not a per-anchor pen concern.
|
|
112
|
+
*/
|
|
113
|
+
export function snapPenAnchorPoint(
|
|
114
|
+
point: PenPoint,
|
|
115
|
+
path: PenPath | null,
|
|
116
|
+
options: { hitRadius: number; zoom: number },
|
|
117
|
+
): PenPoint {
|
|
118
|
+
const existingAnchor = path?.nodes.find(
|
|
119
|
+
(node) =>
|
|
120
|
+
Math.hypot(node.point.x - point.x, node.point.y - point.y) <=
|
|
121
|
+
options.hitRadius,
|
|
122
|
+
);
|
|
123
|
+
if (existingAnchor) {
|
|
124
|
+
return { ...existingAnchor.point };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (options.zoom >= 100) {
|
|
128
|
+
return { x: Math.round(point.x), y: Math.round(point.y) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return point;
|
|
132
|
+
}
|
|
133
|
+
|
|
76
134
|
export function isPenCloseTarget(
|
|
77
135
|
path: PenPath | null,
|
|
78
136
|
point: PenPoint,
|
|
@@ -84,25 +142,151 @@ export function isPenCloseTarget(
|
|
|
84
142
|
}
|
|
85
143
|
|
|
86
144
|
export function getPenPathGeometry(path: PenPath): PenGeometry {
|
|
87
|
-
|
|
88
|
-
[node.point, node.handleIn, node.handleOut].filter(isPenPoint),
|
|
89
|
-
);
|
|
90
|
-
if (points.length === 0) {
|
|
145
|
+
if (path.nodes.length === 0) {
|
|
91
146
|
return { x: 0, y: 0, width: MIN_PATH_SIZE, height: MIN_PATH_SIZE };
|
|
92
147
|
}
|
|
93
148
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
149
|
+
// Tight bounds: rather than bounding all anchors *and* control handles
|
|
150
|
+
// (which over-counts — a handle that pulls a curve's tangent can sit well
|
|
151
|
+
// outside the curve's actual extent), walk each rendered segment and
|
|
152
|
+
// bound the real curve geometry: anchor endpoints plus any local extrema
|
|
153
|
+
// found by solving the cubic Bezier derivative per axis. This matches
|
|
154
|
+
// what `serializePenPath` actually draws (an `L` segment when handles
|
|
155
|
+
// coincide with their anchors, otherwise a `C` segment).
|
|
156
|
+
let left = Infinity;
|
|
157
|
+
let top = Infinity;
|
|
158
|
+
let right = -Infinity;
|
|
159
|
+
let bottom = -Infinity;
|
|
160
|
+
|
|
161
|
+
const include = (point: PenPoint) => {
|
|
162
|
+
if (point.x < left) left = point.x;
|
|
163
|
+
if (point.x > right) right = point.x;
|
|
164
|
+
if (point.y < top) top = point.y;
|
|
165
|
+
if (point.y > bottom) bottom = point.y;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const { nodes, closed } = path;
|
|
169
|
+
include(nodes[0].point);
|
|
170
|
+
|
|
171
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
172
|
+
includeSegmentBounds(nodes[i - 1], nodes[i], include);
|
|
173
|
+
}
|
|
174
|
+
if (closed && nodes.length > 1) {
|
|
175
|
+
includeSegmentBounds(nodes[nodes.length - 1], nodes[0], include);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!Number.isFinite(left)) {
|
|
179
|
+
return { x: 0, y: 0, width: MIN_PATH_SIZE, height: MIN_PATH_SIZE };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const width = right - left;
|
|
183
|
+
const height = bottom - top;
|
|
184
|
+
// Degenerate (zero-area) paths — e.g. a single anchor, or a perfectly
|
|
185
|
+
// straight horizontal/vertical two-point path — still need a visible
|
|
186
|
+
// selection box, so floor to a minimum size in that case only.
|
|
187
|
+
if (width <= 0 && height <= 0) {
|
|
188
|
+
return { x: left, y: top, width: MIN_PATH_SIZE, height: MIN_PATH_SIZE };
|
|
189
|
+
}
|
|
98
190
|
return {
|
|
99
191
|
x: left,
|
|
100
192
|
y: top,
|
|
101
|
-
width:
|
|
102
|
-
height:
|
|
193
|
+
width: width > 0 ? width : MIN_PATH_SIZE,
|
|
194
|
+
height: height > 0 ? height : MIN_PATH_SIZE,
|
|
103
195
|
};
|
|
104
196
|
}
|
|
105
197
|
|
|
198
|
+
function includeSegmentBounds(
|
|
199
|
+
from: PenNode,
|
|
200
|
+
to: PenNode,
|
|
201
|
+
include: (point: PenPoint) => void,
|
|
202
|
+
) {
|
|
203
|
+
const c1 = from.handleOut ?? from.point;
|
|
204
|
+
const c2 = to.handleIn ?? to.point;
|
|
205
|
+
include(to.point);
|
|
206
|
+
|
|
207
|
+
// Straight segment (serializePenPath emits `L` in this case) — the two
|
|
208
|
+
// anchors already bound it, no interior extrema to solve for.
|
|
209
|
+
if (samePoint(c1, from.point) && samePoint(c2, to.point)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
include(c1);
|
|
214
|
+
include(c2);
|
|
215
|
+
for (const t of cubicBezierExtremaTs(from.point.x, c1.x, c2.x, to.point.x)) {
|
|
216
|
+
include({
|
|
217
|
+
x: cubicBezierValue(from.point.x, c1.x, c2.x, to.point.x, t),
|
|
218
|
+
y: cubicBezierValue(from.point.y, c1.y, c2.y, to.point.y, t),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
for (const t of cubicBezierExtremaTs(from.point.y, c1.y, c2.y, to.point.y)) {
|
|
222
|
+
include({
|
|
223
|
+
x: cubicBezierValue(from.point.x, c1.x, c2.x, to.point.x, t),
|
|
224
|
+
y: cubicBezierValue(from.point.y, c1.y, c2.y, to.point.y, t),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Roots of B'(t) = 0 for a single-axis cubic Bezier with control points
|
|
231
|
+
* p0..p3, restricted to t in (0, 1) (endpoints are already included by the
|
|
232
|
+
* caller via the anchor points).
|
|
233
|
+
*
|
|
234
|
+
* B(t) = (1-t)^3 p0 + 3(1-t)^2 t p1 + 3(1-t) t^2 p2 + t^3 p3
|
|
235
|
+
* B'(t) = 3(1-t)^2 (p1-p0) + 6(1-t)t (p2-p1) + 3t^2 (p3-p2)
|
|
236
|
+
* = a t^2 + b t + c, with:
|
|
237
|
+
* a = 3 * (-p0 + 3p1 - 3p2 + p3)
|
|
238
|
+
* b = 6 * (p0 - 2p1 + p2)
|
|
239
|
+
* c = 3 * (p1 - p0)
|
|
240
|
+
*/
|
|
241
|
+
function cubicBezierExtremaTs(
|
|
242
|
+
p0: number,
|
|
243
|
+
p1: number,
|
|
244
|
+
p2: number,
|
|
245
|
+
p3: number,
|
|
246
|
+
): number[] {
|
|
247
|
+
const a = 3 * (-p0 + 3 * p1 - 3 * p2 + p3);
|
|
248
|
+
const b = 6 * (p0 - 2 * p1 + p2);
|
|
249
|
+
const c = 3 * (p1 - p0);
|
|
250
|
+
|
|
251
|
+
const roots: number[] = [];
|
|
252
|
+
const EPS = 1e-9;
|
|
253
|
+
|
|
254
|
+
if (Math.abs(a) < EPS) {
|
|
255
|
+
// Linear derivative: at most one root.
|
|
256
|
+
if (Math.abs(b) >= EPS) {
|
|
257
|
+
const t = -c / b;
|
|
258
|
+
if (t > 0 && t < 1) roots.push(t);
|
|
259
|
+
}
|
|
260
|
+
return roots;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const discriminant = b * b - 4 * a * c;
|
|
264
|
+
if (discriminant < 0) return roots;
|
|
265
|
+
|
|
266
|
+
const sqrtDisc = Math.sqrt(discriminant);
|
|
267
|
+
const t1 = (-b + sqrtDisc) / (2 * a);
|
|
268
|
+
const t2 = (-b - sqrtDisc) / (2 * a);
|
|
269
|
+
if (t1 > 0 && t1 < 1) roots.push(t1);
|
|
270
|
+
if (t2 > 0 && t2 < 1) roots.push(t2);
|
|
271
|
+
return roots;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function cubicBezierValue(
|
|
275
|
+
p0: number,
|
|
276
|
+
p1: number,
|
|
277
|
+
p2: number,
|
|
278
|
+
p3: number,
|
|
279
|
+
t: number,
|
|
280
|
+
): number {
|
|
281
|
+
const mt = 1 - t;
|
|
282
|
+
return (
|
|
283
|
+
mt * mt * mt * p0 +
|
|
284
|
+
3 * mt * mt * t * p1 +
|
|
285
|
+
3 * mt * t * t * p2 +
|
|
286
|
+
t * t * t * p3
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
106
290
|
export function serializePenPath(path: PenPath): string {
|
|
107
291
|
const [first, ...rest] = path.nodes;
|
|
108
292
|
if (!first) return "";
|
|
@@ -121,13 +305,6 @@ export function serializePenPath(path: PenPath): string {
|
|
|
121
305
|
return commands.join(" ");
|
|
122
306
|
}
|
|
123
307
|
|
|
124
|
-
export function offsetPenPath(path: PenPath, offset: PenPoint): PenPath {
|
|
125
|
-
return transformPenPath(path, (point) => ({
|
|
126
|
-
x: point.x - offset.x,
|
|
127
|
-
y: point.y - offset.y,
|
|
128
|
-
}));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
308
|
export function translatePenPath(
|
|
132
309
|
path: PenPath,
|
|
133
310
|
dx: number,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-pinch-zoom.d.ts","sourceRoot":"","sources":["../../src/client/use-pinch-zoom.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC;oCACgC;IAChC,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAClD,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,2CAA2C;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;kDAI8C;IAC9C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,uDAAuD;IACvD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,EAC3B,YAAY,EACZ,IAAI,EACJ,OAAO,EACP,GAAQ,EACR,GAAS,EACT,YAAmB,EACnB,OAAc,GACf,EAAE,mBAAmB,
|
|
1
|
+
{"version":3,"file":"use-pinch-zoom.d.ts","sourceRoot":"","sources":["../../src/client/use-pinch-zoom.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC;oCACgC;IAChC,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAClD,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,2CAA2C;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;kDAI8C;IAC9C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,uDAAuD;IACvD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,EAC3B,YAAY,EACZ,IAAI,EACJ,OAAO,EACP,GAAQ,EACR,GAAS,EACT,YAAmB,EACnB,OAAc,GACf,EAAE,mBAAmB,QA6JrB"}
|
|
@@ -23,32 +23,90 @@ export function usePinchZoom({ containerRef, zoom, setZoom, min = 25, max = 400,
|
|
|
23
23
|
if (!container)
|
|
24
24
|
return;
|
|
25
25
|
const clamp = (n) => Math.max(min, Math.min(max, n));
|
|
26
|
+
// rAF coalescing: multiple wheel/pointermove events can fire per frame
|
|
27
|
+
// (trackpad pinch and touch pinch both deliver many events between
|
|
28
|
+
// paints). Instead of calling setZoom() synchronously per event — which
|
|
29
|
+
// schedules a React re-render per event — stash the latest pending zoom
|
|
30
|
+
// (and its cursor-anchored scroll delta) in a ref and flush once per
|
|
31
|
+
// animation frame with the last-wins value. This preserves the exact
|
|
32
|
+
// zoom-to-cursor math; it just applies it at most once per frame.
|
|
33
|
+
//
|
|
34
|
+
// Within a burst the DOM's real scrollLeft/scrollTop do NOT move until
|
|
35
|
+
// flush() runs, so per-event math must not read them directly — every
|
|
36
|
+
// event after the first in the same frame would anchor against the
|
|
37
|
+
// pre-burst scroll position instead of where the (not-yet-committed)
|
|
38
|
+
// previous events in the burst would have scrolled to. Track a simulated
|
|
39
|
+
// running scroll position (`simScrollLeft`/`simScrollTop`, seeded from the
|
|
40
|
+
// real scroll position when a new burst starts) and use that as the
|
|
41
|
+
// anchor base, so each event's math composes exactly as if the prior
|
|
42
|
+
// events in the burst had already been applied — matching the
|
|
43
|
+
// pre-coalescing, one-setZoom-per-event behavior.
|
|
44
|
+
let pendingZoom = null;
|
|
45
|
+
let pendingScrollDelta = null;
|
|
46
|
+
let simScrollLeft = 0;
|
|
47
|
+
let simScrollTop = 0;
|
|
48
|
+
let rafId = null;
|
|
49
|
+
const flush = () => {
|
|
50
|
+
rafId = null;
|
|
51
|
+
if (pendingZoom === null)
|
|
52
|
+
return;
|
|
53
|
+
const nextZoom = pendingZoom;
|
|
54
|
+
const scrollDelta = pendingScrollDelta;
|
|
55
|
+
pendingZoom = null;
|
|
56
|
+
pendingScrollDelta = null;
|
|
57
|
+
setZoomRef.current(nextZoom);
|
|
58
|
+
if (scrollDelta) {
|
|
59
|
+
container.scrollLeft += scrollDelta.dx;
|
|
60
|
+
container.scrollTop += scrollDelta.dy;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const scheduleFlush = () => {
|
|
64
|
+
if (rafId !== null)
|
|
65
|
+
return;
|
|
66
|
+
rafId = requestAnimationFrame(flush);
|
|
67
|
+
};
|
|
26
68
|
const handleWheel = (e) => {
|
|
27
69
|
if (!(e.ctrlKey || e.metaKey))
|
|
28
70
|
return;
|
|
29
71
|
e.preventDefault();
|
|
30
|
-
|
|
72
|
+
// Use the latest not-yet-applied zoom (if a flush is pending) so rapid
|
|
73
|
+
// wheel events within the same frame compound correctly instead of
|
|
74
|
+
// each computing off the last-committed React state.
|
|
75
|
+
const currentZoom = pendingZoom ?? zoomRef.current;
|
|
31
76
|
const clampedDelta = Math.max(-50, Math.min(50, e.deltaY));
|
|
32
77
|
const factor = Math.exp(-clampedDelta * 0.01);
|
|
33
78
|
const nextZoom = clamp(currentZoom * factor);
|
|
34
79
|
if (nextZoom === currentZoom)
|
|
35
80
|
return;
|
|
36
81
|
if (zoomToCursor) {
|
|
82
|
+
// Starting a new burst (nothing pending yet): seed the simulated
|
|
83
|
+
// scroll position from the container's real, currently-committed
|
|
84
|
+
// scroll offset.
|
|
85
|
+
if (pendingScrollDelta === null) {
|
|
86
|
+
simScrollLeft = container.scrollLeft;
|
|
87
|
+
simScrollTop = container.scrollTop;
|
|
88
|
+
}
|
|
37
89
|
const rect = container.getBoundingClientRect();
|
|
38
|
-
const cx = e.clientX - rect.left +
|
|
39
|
-
const cy = e.clientY - rect.top +
|
|
90
|
+
const cx = e.clientX - rect.left + simScrollLeft;
|
|
91
|
+
const cy = e.clientY - rect.top + simScrollTop;
|
|
40
92
|
const ratio = nextZoom / currentZoom;
|
|
41
93
|
const dx = cx * (ratio - 1);
|
|
42
94
|
const dy = cy * (ratio - 1);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
95
|
+
// Advance the simulated scroll position so the next event in this
|
|
96
|
+
// same burst anchors against where this event would have left it.
|
|
97
|
+
simScrollLeft += dx;
|
|
98
|
+
simScrollTop += dy;
|
|
99
|
+
pendingZoom = nextZoom;
|
|
100
|
+
const prevDelta = pendingScrollDelta;
|
|
101
|
+
pendingScrollDelta = {
|
|
102
|
+
dx: (prevDelta?.dx ?? 0) + dx,
|
|
103
|
+
dy: (prevDelta?.dy ?? 0) + dy,
|
|
104
|
+
};
|
|
48
105
|
}
|
|
49
106
|
else {
|
|
50
|
-
|
|
107
|
+
pendingZoom = nextZoom;
|
|
51
108
|
}
|
|
109
|
+
scheduleFlush();
|
|
52
110
|
};
|
|
53
111
|
const activePointers = new Map();
|
|
54
112
|
let initialDistance = 0;
|
|
@@ -73,8 +131,11 @@ export function usePinchZoom({ containerRef, zoom, setZoom, min = 25, max = 400,
|
|
|
73
131
|
const [p1, p2] = Array.from(activePointers.values());
|
|
74
132
|
const distance = Math.hypot(p2.x - p1.x, p2.y - p1.y);
|
|
75
133
|
const nextZoom = clamp(initialZoom * (distance / initialDistance));
|
|
76
|
-
if (nextZoom !== zoomRef.current) {
|
|
77
|
-
|
|
134
|
+
if (nextZoom !== (pendingZoom ?? zoomRef.current)) {
|
|
135
|
+
// Touch pinch has no cursor-anchoring math, so last-wins is simply
|
|
136
|
+
// the newest zoom value — no scroll delta to accumulate.
|
|
137
|
+
pendingZoom = nextZoom;
|
|
138
|
+
scheduleFlush();
|
|
78
139
|
}
|
|
79
140
|
e.preventDefault();
|
|
80
141
|
}
|
|
@@ -99,6 +160,10 @@ export function usePinchZoom({ containerRef, zoom, setZoom, min = 25, max = 400,
|
|
|
99
160
|
container.removeEventListener("pointermove", handlePointerMove);
|
|
100
161
|
container.removeEventListener("pointerup", handlePointerEnd);
|
|
101
162
|
container.removeEventListener("pointercancel", handlePointerEnd);
|
|
163
|
+
if (rafId !== null)
|
|
164
|
+
cancelAnimationFrame(rafId);
|
|
165
|
+
pendingZoom = null;
|
|
166
|
+
pendingScrollDelta = null;
|
|
102
167
|
};
|
|
103
168
|
}, [containerRef, enabled, min, max, zoomToCursor]);
|
|
104
169
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-pinch-zoom.js","sourceRoot":"","sources":["../../src/client/use-pinch-zoom.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef } from \"react\";\n\nexport interface UsePinchZoomOptions {\n /** Scrolling viewport that receives the gesture. The scaled content should\n * live inside this element. */\n containerRef: React.RefObject<HTMLElement | null>;\n /** Current zoom as a percentage (100 = 100%). */\n zoom: number;\n /** Setter for the zoom value (called with the next percentage). */\n setZoom: (next: number) => void;\n /** Minimum zoom percentage. Default 25. */\n min?: number;\n /** Maximum zoom percentage. Default 400. */\n max?: number;\n /** When true (default), adjusts container scroll so the point under the\n * cursor stays under the cursor during wheel-zoom. Assumes the scaled\n * content uses `transform-origin: top left` (or equivalent — e.g. resizing\n * the inner container's width proportionally to zoom). Disable for layouts\n * with `transform-origin: center center`. */\n zoomToCursor?: boolean;\n /** Disable the hook entirely without unmounting it. */\n enabled?: boolean;\n}\n\n/**\n * Pinch-to-zoom for canvas-style editors. Wires the trackpad pinch / Cmd+scroll\n * wheel gesture and 2-pointer touchscreen pinch onto a scrolling container.\n *\n * Trackpad pinch is detected via `wheel` events with `ctrlKey: true` — browsers\n * have synthesized that since ~2015 specifically so web apps can intercept the\n * gesture. `metaKey` is also accepted so Cmd+scroll on Mac feels native.\n *\n * The hook only calls `setZoom(next)` — it doesn't render anything. Templates\n * decide how to translate the zoom percentage into visual scaling (CSS\n * `transform: scale()`, width/height, etc.).\n */\nexport function usePinchZoom({\n containerRef,\n zoom,\n setZoom,\n min = 25,\n max = 400,\n zoomToCursor = true,\n enabled = true,\n}: UsePinchZoomOptions) {\n const zoomRef = useRef(zoom);\n const setZoomRef = useRef(setZoom);\n zoomRef.current = zoom;\n setZoomRef.current = setZoom;\n\n useEffect(() => {\n if (!enabled) return;\n const container = containerRef.current;\n if (!container) return;\n\n const clamp = (n: number) => Math.max(min, Math.min(max, n));\n\n const handleWheel = (e: WheelEvent) => {\n if (!(e.ctrlKey || e.metaKey)) return;\n e.preventDefault();\n\n const currentZoom = zoomRef.current;\n const clampedDelta = Math.max(-50, Math.min(50, e.deltaY));\n const factor = Math.exp(-clampedDelta * 0.01);\n const nextZoom = clamp(currentZoom * factor);\n\n if (nextZoom === currentZoom) return;\n\n if (zoomToCursor) {\n const rect = container.getBoundingClientRect();\n const cx = e.clientX - rect.left + container.scrollLeft;\n const cy = e.clientY - rect.top + container.scrollTop;\n const ratio = nextZoom / currentZoom;\n const dx = cx * (ratio - 1);\n const dy = cy * (ratio - 1);\n setZoomRef.current(nextZoom);\n requestAnimationFrame(() => {\n container.scrollLeft += dx;\n container.scrollTop += dy;\n });\n } else {\n setZoomRef.current(nextZoom);\n }\n };\n\n const activePointers = new Map<number, { x: number; y: number }>();\n let initialDistance = 0;\n let initialZoom = 0;\n\n const handlePointerDown = (e: PointerEvent) => {\n if (e.pointerType !== \"touch\") return;\n activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (activePointers.size === 2) {\n const [p1, p2] = Array.from(activePointers.values());\n initialDistance = Math.hypot(p2.x - p1.x, p2.y - p1.y);\n initialZoom = zoomRef.current;\n }\n };\n\n const handlePointerMove = (e: PointerEvent) => {\n if (e.pointerType !== \"touch\") return;\n if (!activePointers.has(e.pointerId)) return;\n activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (activePointers.size === 2 && initialDistance > 0) {\n const [p1, p2] = Array.from(activePointers.values());\n const distance = Math.hypot(p2.x - p1.x, p2.y - p1.y);\n const nextZoom = clamp(initialZoom * (distance / initialDistance));\n if (nextZoom !== zoomRef.current) {\n setZoomRef.current(nextZoom);\n }\n e.preventDefault();\n }\n };\n\n const handlePointerEnd = (e: PointerEvent) => {\n if (e.pointerType !== \"touch\") return;\n activePointers.delete(e.pointerId);\n if (activePointers.size < 2) initialDistance = 0;\n };\n\n container.addEventListener(\"wheel\", handleWheel, { passive: false });\n container.addEventListener(\"pointerdown\", handlePointerDown);\n container.addEventListener(\"pointermove\", handlePointerMove, {\n passive: false,\n });\n container.addEventListener(\"pointerup\", handlePointerEnd);\n container.addEventListener(\"pointercancel\", handlePointerEnd);\n\n return () => {\n container.removeEventListener(\"wheel\", handleWheel);\n container.removeEventListener(\"pointerdown\", handlePointerDown);\n container.removeEventListener(\"pointermove\", handlePointerMove);\n container.removeEventListener(\"pointerup\", handlePointerEnd);\n container.removeEventListener(\"pointercancel\", handlePointerEnd);\n };\n }, [containerRef, enabled, min, max, zoomToCursor]);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"use-pinch-zoom.js","sourceRoot":"","sources":["../../src/client/use-pinch-zoom.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef } from \"react\";\n\nexport interface UsePinchZoomOptions {\n /** Scrolling viewport that receives the gesture. The scaled content should\n * live inside this element. */\n containerRef: React.RefObject<HTMLElement | null>;\n /** Current zoom as a percentage (100 = 100%). */\n zoom: number;\n /** Setter for the zoom value (called with the next percentage). */\n setZoom: (next: number) => void;\n /** Minimum zoom percentage. Default 25. */\n min?: number;\n /** Maximum zoom percentage. Default 400. */\n max?: number;\n /** When true (default), adjusts container scroll so the point under the\n * cursor stays under the cursor during wheel-zoom. Assumes the scaled\n * content uses `transform-origin: top left` (or equivalent — e.g. resizing\n * the inner container's width proportionally to zoom). Disable for layouts\n * with `transform-origin: center center`. */\n zoomToCursor?: boolean;\n /** Disable the hook entirely without unmounting it. */\n enabled?: boolean;\n}\n\n/**\n * Pinch-to-zoom for canvas-style editors. Wires the trackpad pinch / Cmd+scroll\n * wheel gesture and 2-pointer touchscreen pinch onto a scrolling container.\n *\n * Trackpad pinch is detected via `wheel` events with `ctrlKey: true` — browsers\n * have synthesized that since ~2015 specifically so web apps can intercept the\n * gesture. `metaKey` is also accepted so Cmd+scroll on Mac feels native.\n *\n * The hook only calls `setZoom(next)` — it doesn't render anything. Templates\n * decide how to translate the zoom percentage into visual scaling (CSS\n * `transform: scale()`, width/height, etc.).\n */\nexport function usePinchZoom({\n containerRef,\n zoom,\n setZoom,\n min = 25,\n max = 400,\n zoomToCursor = true,\n enabled = true,\n}: UsePinchZoomOptions) {\n const zoomRef = useRef(zoom);\n const setZoomRef = useRef(setZoom);\n zoomRef.current = zoom;\n setZoomRef.current = setZoom;\n\n useEffect(() => {\n if (!enabled) return;\n const container = containerRef.current;\n if (!container) return;\n\n const clamp = (n: number) => Math.max(min, Math.min(max, n));\n\n // rAF coalescing: multiple wheel/pointermove events can fire per frame\n // (trackpad pinch and touch pinch both deliver many events between\n // paints). Instead of calling setZoom() synchronously per event — which\n // schedules a React re-render per event — stash the latest pending zoom\n // (and its cursor-anchored scroll delta) in a ref and flush once per\n // animation frame with the last-wins value. This preserves the exact\n // zoom-to-cursor math; it just applies it at most once per frame.\n //\n // Within a burst the DOM's real scrollLeft/scrollTop do NOT move until\n // flush() runs, so per-event math must not read them directly — every\n // event after the first in the same frame would anchor against the\n // pre-burst scroll position instead of where the (not-yet-committed)\n // previous events in the burst would have scrolled to. Track a simulated\n // running scroll position (`simScrollLeft`/`simScrollTop`, seeded from the\n // real scroll position when a new burst starts) and use that as the\n // anchor base, so each event's math composes exactly as if the prior\n // events in the burst had already been applied — matching the\n // pre-coalescing, one-setZoom-per-event behavior.\n let pendingZoom: number | null = null;\n let pendingScrollDelta: { dx: number; dy: number } | null = null;\n let simScrollLeft = 0;\n let simScrollTop = 0;\n let rafId: number | null = null;\n\n const flush = () => {\n rafId = null;\n if (pendingZoom === null) return;\n const nextZoom = pendingZoom;\n const scrollDelta = pendingScrollDelta;\n pendingZoom = null;\n pendingScrollDelta = null;\n setZoomRef.current(nextZoom);\n if (scrollDelta) {\n container.scrollLeft += scrollDelta.dx;\n container.scrollTop += scrollDelta.dy;\n }\n };\n\n const scheduleFlush = () => {\n if (rafId !== null) return;\n rafId = requestAnimationFrame(flush);\n };\n\n const handleWheel = (e: WheelEvent) => {\n if (!(e.ctrlKey || e.metaKey)) return;\n e.preventDefault();\n\n // Use the latest not-yet-applied zoom (if a flush is pending) so rapid\n // wheel events within the same frame compound correctly instead of\n // each computing off the last-committed React state.\n const currentZoom = pendingZoom ?? zoomRef.current;\n const clampedDelta = Math.max(-50, Math.min(50, e.deltaY));\n const factor = Math.exp(-clampedDelta * 0.01);\n const nextZoom = clamp(currentZoom * factor);\n\n if (nextZoom === currentZoom) return;\n\n if (zoomToCursor) {\n // Starting a new burst (nothing pending yet): seed the simulated\n // scroll position from the container's real, currently-committed\n // scroll offset.\n if (pendingScrollDelta === null) {\n simScrollLeft = container.scrollLeft;\n simScrollTop = container.scrollTop;\n }\n const rect = container.getBoundingClientRect();\n const cx = e.clientX - rect.left + simScrollLeft;\n const cy = e.clientY - rect.top + simScrollTop;\n const ratio = nextZoom / currentZoom;\n const dx = cx * (ratio - 1);\n const dy = cy * (ratio - 1);\n // Advance the simulated scroll position so the next event in this\n // same burst anchors against where this event would have left it.\n simScrollLeft += dx;\n simScrollTop += dy;\n pendingZoom = nextZoom;\n const prevDelta = pendingScrollDelta;\n pendingScrollDelta = {\n dx: (prevDelta?.dx ?? 0) + dx,\n dy: (prevDelta?.dy ?? 0) + dy,\n };\n } else {\n pendingZoom = nextZoom;\n }\n scheduleFlush();\n };\n\n const activePointers = new Map<number, { x: number; y: number }>();\n let initialDistance = 0;\n let initialZoom = 0;\n\n const handlePointerDown = (e: PointerEvent) => {\n if (e.pointerType !== \"touch\") return;\n activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (activePointers.size === 2) {\n const [p1, p2] = Array.from(activePointers.values());\n initialDistance = Math.hypot(p2.x - p1.x, p2.y - p1.y);\n initialZoom = zoomRef.current;\n }\n };\n\n const handlePointerMove = (e: PointerEvent) => {\n if (e.pointerType !== \"touch\") return;\n if (!activePointers.has(e.pointerId)) return;\n activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (activePointers.size === 2 && initialDistance > 0) {\n const [p1, p2] = Array.from(activePointers.values());\n const distance = Math.hypot(p2.x - p1.x, p2.y - p1.y);\n const nextZoom = clamp(initialZoom * (distance / initialDistance));\n if (nextZoom !== (pendingZoom ?? zoomRef.current)) {\n // Touch pinch has no cursor-anchoring math, so last-wins is simply\n // the newest zoom value — no scroll delta to accumulate.\n pendingZoom = nextZoom;\n scheduleFlush();\n }\n e.preventDefault();\n }\n };\n\n const handlePointerEnd = (e: PointerEvent) => {\n if (e.pointerType !== \"touch\") return;\n activePointers.delete(e.pointerId);\n if (activePointers.size < 2) initialDistance = 0;\n };\n\n container.addEventListener(\"wheel\", handleWheel, { passive: false });\n container.addEventListener(\"pointerdown\", handlePointerDown);\n container.addEventListener(\"pointermove\", handlePointerMove, {\n passive: false,\n });\n container.addEventListener(\"pointerup\", handlePointerEnd);\n container.addEventListener(\"pointercancel\", handlePointerEnd);\n\n return () => {\n container.removeEventListener(\"wheel\", handleWheel);\n container.removeEventListener(\"pointerdown\", handlePointerDown);\n container.removeEventListener(\"pointermove\", handlePointerMove);\n container.removeEventListener(\"pointerup\", handlePointerEnd);\n container.removeEventListener(\"pointercancel\", handlePointerEnd);\n if (rafId !== null) cancelAnimationFrame(rafId);\n pendingZoom = null;\n pendingScrollDelta = null;\n };\n }, [containerRef, enabled, min, max, zoomToCursor]);\n}\n"]}
|
|
@@ -49,11 +49,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
49
49
|
error: string;
|
|
50
50
|
states?: undefined;
|
|
51
51
|
} | {
|
|
52
|
-
error?: undefined;
|
|
53
52
|
states: {
|
|
54
53
|
clientId: number;
|
|
55
54
|
state: string;
|
|
56
55
|
}[];
|
|
56
|
+
error?: undefined;
|
|
57
57
|
}>>;
|
|
58
58
|
/**
|
|
59
59
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -64,10 +64,10 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
64
64
|
error: string;
|
|
65
65
|
users?: undefined;
|
|
66
66
|
} | {
|
|
67
|
-
error?: undefined;
|
|
68
67
|
users: {
|
|
69
68
|
clientId: number;
|
|
70
69
|
lastSeen: number;
|
|
71
70
|
}[];
|
|
71
|
+
error?: undefined;
|
|
72
72
|
}>>;
|
|
73
73
|
//# sourceMappingURL=awareness.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"awareness.d.ts","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAS3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,KAAK,CAAC,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,GACb,IAAI,CAUN;AAKD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAO1E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI,CAOnE;AAUD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa
|
|
1
|
+
{"version":3,"file":"awareness.d.ts","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAS3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,KAAK,CAAC,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,GACb,IAAI,CAUN;AAKD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAO1E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI,CAOnE;AAUD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;kBAuCa,MAAM;eAAS,MAAM;;;GAa1D,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;kBAWM,MAAM;kBAAY,MAAM;;;GAMvD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../src/collab/presence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,kEAAkE;AAClE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEtD,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,qBAAqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,IAAI,EAAE,UAAU,CAAC;IACjB,gFAAgF;IAChF,QAAQ,EAAE,eAAe,CAAC;IAC1B,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,uDAAuD;IACvD,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB;;;;OAIG;IACH,WAAW,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;CACjD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,EACvC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,iBAAiB,
|
|
1
|
+
{"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../src/collab/presence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,kEAAkE;AAClE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEtD,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,qBAAqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,IAAI,EAAE,UAAU,CAAC;IACjB,gFAAgF;IAChF,QAAQ,EAAE,eAAe,CAAC;IAC1B,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,uDAAuD;IACvD,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB;;;;OAIG;IACH,WAAW,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;CACjD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,EACvC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,iBAAiB,CAmInB;AAMD,MAAM,WAAW,eAAe;IAC9B,uCAAuC;IACvC,CAAC,EAAE,MAAM,CAAC;IACV,wCAAwC;IACxC,CAAC,EAAE,MAAM,CAAC;CACX;AAED,4DAA4D;AAC5D,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,OAAO,GACjB,eAAe,CAKjB;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAC5B,KAAK,EAAE,eAAe,EACtB,SAAS,EAAE,OAAO,GACjB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAK1B"}
|
package/dist/collab/presence.js
CHANGED
|
@@ -26,6 +26,36 @@ export function usePresence(awareness, localClientId) {
|
|
|
26
26
|
setOthers([]);
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
|
+
// Keep the last derived snapshot so genuinely no-op change events (e.g. a
|
|
30
|
+
// local-only awareness field flip that doesn't affect any remote entry)
|
|
31
|
+
// can bail out without triggering a subscriber re-render.
|
|
32
|
+
let lastOthers = [];
|
|
33
|
+
function shallowEqualOthers(a, b) {
|
|
34
|
+
if (a === b)
|
|
35
|
+
return true;
|
|
36
|
+
if (a.length !== b.length)
|
|
37
|
+
return false;
|
|
38
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
39
|
+
const left = a[i];
|
|
40
|
+
const right = b[i];
|
|
41
|
+
if (left === right)
|
|
42
|
+
continue;
|
|
43
|
+
if (left.clientId !== right.clientId ||
|
|
44
|
+
left.isAgent !== right.isAgent ||
|
|
45
|
+
left.user.name !== right.user.name ||
|
|
46
|
+
left.user.email !== right.user.email ||
|
|
47
|
+
left.user.color !== right.user.color) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// Compare presence payloads with a stable JSON.stringify — presence
|
|
51
|
+
// fields (cursor/selection/viewport) are small JSON-safe records, so
|
|
52
|
+
// this is cheap and avoids a re-render when nothing actually changed.
|
|
53
|
+
if (JSON.stringify(left.presence) !== JSON.stringify(right.presence)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
29
59
|
function derive() {
|
|
30
60
|
const result = [];
|
|
31
61
|
awareness.getStates().forEach((state, clientId) => {
|
|
@@ -61,11 +91,32 @@ export function usePresence(awareness, localClientId) {
|
|
|
61
91
|
});
|
|
62
92
|
return result;
|
|
63
93
|
}
|
|
64
|
-
function onAwarenessChange() {
|
|
65
|
-
|
|
94
|
+
function onAwarenessChange(changes) {
|
|
95
|
+
// The awareness "change" event fires for local-only state edits too
|
|
96
|
+
// (e.g. this hook's own setPresence() calls, or the doc's
|
|
97
|
+
// activeFileId/visible fields). When every changed client id is the
|
|
98
|
+
// local client, the derived `others` array (which excludes the local
|
|
99
|
+
// client) cannot have changed, so skip the re-render entirely.
|
|
100
|
+
if (changes) {
|
|
101
|
+
const changedIds = [
|
|
102
|
+
...changes.added,
|
|
103
|
+
...changes.updated,
|
|
104
|
+
...changes.removed,
|
|
105
|
+
];
|
|
106
|
+
if (changedIds.length > 0 &&
|
|
107
|
+
changedIds.every((id) => id === localClientId)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const next = derive();
|
|
112
|
+
if (shallowEqualOthers(lastOthers, next))
|
|
113
|
+
return;
|
|
114
|
+
lastOthers = next;
|
|
115
|
+
setOthers(next);
|
|
66
116
|
}
|
|
67
117
|
// Derive immediately.
|
|
68
|
-
|
|
118
|
+
lastOthers = derive();
|
|
119
|
+
setOthers(lastOthers);
|
|
69
120
|
awareness.on("change", onAwarenessChange);
|
|
70
121
|
return () => {
|
|
71
122
|
awareness.off("change", onAwarenessChange);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presence.js","sourceRoot":"","sources":["../../src/collab/presence.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Presence kit — Liveblocks/collaboration-grade presence primitives.\n *\n * usePresence(awareness) returns:\n * - others: reactive array of remote participants (human + agent)\n * - setPresence(partial): merge fields into local awareness state\n *\n * The hook re-renders on every awareness change event and always includes\n * the agent participant (AGENT_CLIENT_ID) as isAgent: true.\n */\n\nimport { useState, useEffect, useCallback, useRef } from \"react\";\nimport type { Awareness } from \"y-protocols/awareness\";\n\nimport { AGENT_CLIENT_ID } from \"./agent-identity.js\";\nimport type { CollabUser } from \"./client.js\";\n\n/** Arbitrary JSON presence payload published by a participant. */\nexport type PresencePayload = Record<string, unknown>;\n\n/** A remote participant's full presence snapshot. */\nexport interface OtherPresence {\n /** Yjs client ID. */\n clientId: number;\n /** User identity (from awareness `user` field). */\n user: CollabUser;\n /** Arbitrary presence fields set via setPresence() / agentUpdateSelection(). */\n presence: PresencePayload;\n /** True when this participant is the AI agent. */\n isAgent: boolean;\n}\n\nexport interface UsePresenceResult {\n /** All remote participants (excludes local client). */\n others: OtherPresence[];\n /**\n * Merge fields into the local awareness state. These are broadcast to\n * peers by the fast-awareness path in useCollaborativeDoc.\n * Call this to publish cursor position, viewport, or selection.\n */\n setPresence: (partial: PresencePayload) => void;\n}\n\n/**\n * Derive OtherPresence entries from an Awareness instance.\n *\n * @param awareness Awareness instance from useCollaborativeDoc.\n * @param localClientId The local Yjs client ID (to exclude self).\n */\nexport function usePresence(\n awareness: Awareness | null | undefined,\n localClientId: number | null | undefined,\n): UsePresenceResult {\n const [others, setOthers] = useState<OtherPresence[]>([]);\n\n // Keep the latest awareness ref so setPresence closure doesn't go stale.\n const awarenessRef = useRef(awareness);\n awarenessRef.current = awareness;\n\n useEffect(() => {\n if (!awareness) {\n setOthers([]);\n return;\n }\n\n function derive(): OtherPresence[] {\n const result: OtherPresence[] = [];\n awareness!.getStates().forEach((state, clientId) => {\n if (clientId === localClientId) return; // skip self\n const s = state as Record<string, unknown>;\n const isAgent = clientId === AGENT_CLIENT_ID;\n\n // User identity — fall back to agent defaults or anonymous.\n let user: CollabUser;\n if (isAgent) {\n user = {\n name: (s.user as CollabUser)?.name ?? \"AI Assistant\",\n email: (s.user as CollabUser)?.email ?? \"agent@system\",\n color: (s.user as CollabUser)?.color ?? \"#00B5FF\",\n };\n } else {\n const u = s.user as CollabUser | undefined;\n user = {\n name: u?.name ?? \"Unknown\",\n email: u?.email ?? `client-${clientId}`,\n color: u?.color ?? \"#94a3b8\",\n };\n }\n\n // Everything that isn't `user` or `visible` is presence payload.\n const presence: PresencePayload = {};\n for (const [k, v] of Object.entries(s)) {\n if (k !== \"user\" && k !== \"visible\") {\n presence[k] = v;\n }\n }\n\n result.push({ clientId, user, presence, isAgent });\n });\n return result;\n }\n\n function onAwarenessChange() {\n setOthers(derive());\n }\n\n // Derive immediately.\n setOthers(derive());\n awareness.on(\"change\", onAwarenessChange);\n return () => {\n awareness.off(\"change\", onAwarenessChange);\n };\n }, [awareness, localClientId]);\n\n const setPresence = useCallback((partial: PresencePayload) => {\n const aw = awarenessRef.current;\n if (!aw) return;\n for (const [k, v] of Object.entries(partial)) {\n aw.setLocalStateField(k, v);\n }\n }, []);\n\n return { others, setPresence };\n}\n\n// ---------------------------------------------------------------------------\n// Normalized cursor coordinate helpers\n// ---------------------------------------------------------------------------\n\nexport interface NormalizedPoint {\n /** 0–1 fraction of container width. */\n x: number;\n /** 0–1 fraction of container height. */\n y: number;\n}\n\n/** Convert a pointer event offset to a normalized point. */\nexport function toNormalized(\n clientX: number,\n clientY: number,\n container: DOMRect,\n): NormalizedPoint {\n return {\n x: Math.max(0, Math.min(1, (clientX - container.left) / container.width)),\n y: Math.max(0, Math.min(1, (clientY - container.top) / container.height)),\n };\n}\n\n/** Convert a normalized point back to absolute offset within a container. */\nexport function fromNormalized(\n point: NormalizedPoint,\n container: DOMRect,\n): { x: number; y: number } {\n return {\n x: point.x * container.width,\n y: point.y * container.height,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"presence.js","sourceRoot":"","sources":["../../src/collab/presence.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Presence kit — Liveblocks/collaboration-grade presence primitives.\n *\n * usePresence(awareness) returns:\n * - others: reactive array of remote participants (human + agent)\n * - setPresence(partial): merge fields into local awareness state\n *\n * The hook re-renders on every awareness change event and always includes\n * the agent participant (AGENT_CLIENT_ID) as isAgent: true.\n */\n\nimport { useState, useEffect, useCallback, useRef } from \"react\";\nimport type { Awareness } from \"y-protocols/awareness\";\n\nimport { AGENT_CLIENT_ID } from \"./agent-identity.js\";\nimport type { CollabUser } from \"./client.js\";\n\n/** Arbitrary JSON presence payload published by a participant. */\nexport type PresencePayload = Record<string, unknown>;\n\n/** A remote participant's full presence snapshot. */\nexport interface OtherPresence {\n /** Yjs client ID. */\n clientId: number;\n /** User identity (from awareness `user` field). */\n user: CollabUser;\n /** Arbitrary presence fields set via setPresence() / agentUpdateSelection(). */\n presence: PresencePayload;\n /** True when this participant is the AI agent. */\n isAgent: boolean;\n}\n\nexport interface UsePresenceResult {\n /** All remote participants (excludes local client). */\n others: OtherPresence[];\n /**\n * Merge fields into the local awareness state. These are broadcast to\n * peers by the fast-awareness path in useCollaborativeDoc.\n * Call this to publish cursor position, viewport, or selection.\n */\n setPresence: (partial: PresencePayload) => void;\n}\n\n/**\n * Derive OtherPresence entries from an Awareness instance.\n *\n * @param awareness Awareness instance from useCollaborativeDoc.\n * @param localClientId The local Yjs client ID (to exclude self).\n */\nexport function usePresence(\n awareness: Awareness | null | undefined,\n localClientId: number | null | undefined,\n): UsePresenceResult {\n const [others, setOthers] = useState<OtherPresence[]>([]);\n\n // Keep the latest awareness ref so setPresence closure doesn't go stale.\n const awarenessRef = useRef(awareness);\n awarenessRef.current = awareness;\n\n useEffect(() => {\n if (!awareness) {\n setOthers([]);\n return;\n }\n\n // Keep the last derived snapshot so genuinely no-op change events (e.g. a\n // local-only awareness field flip that doesn't affect any remote entry)\n // can bail out without triggering a subscriber re-render.\n let lastOthers: OtherPresence[] = [];\n\n function shallowEqualOthers(\n a: readonly OtherPresence[],\n b: readonly OtherPresence[],\n ): boolean {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i += 1) {\n const left = a[i]!;\n const right = b[i]!;\n if (left === right) continue;\n if (\n left.clientId !== right.clientId ||\n left.isAgent !== right.isAgent ||\n left.user.name !== right.user.name ||\n left.user.email !== right.user.email ||\n left.user.color !== right.user.color\n ) {\n return false;\n }\n // Compare presence payloads with a stable JSON.stringify — presence\n // fields (cursor/selection/viewport) are small JSON-safe records, so\n // this is cheap and avoids a re-render when nothing actually changed.\n if (JSON.stringify(left.presence) !== JSON.stringify(right.presence)) {\n return false;\n }\n }\n return true;\n }\n\n function derive(): OtherPresence[] {\n const result: OtherPresence[] = [];\n awareness!.getStates().forEach((state, clientId) => {\n if (clientId === localClientId) return; // skip self\n const s = state as Record<string, unknown>;\n const isAgent = clientId === AGENT_CLIENT_ID;\n\n // User identity — fall back to agent defaults or anonymous.\n let user: CollabUser;\n if (isAgent) {\n user = {\n name: (s.user as CollabUser)?.name ?? \"AI Assistant\",\n email: (s.user as CollabUser)?.email ?? \"agent@system\",\n color: (s.user as CollabUser)?.color ?? \"#00B5FF\",\n };\n } else {\n const u = s.user as CollabUser | undefined;\n user = {\n name: u?.name ?? \"Unknown\",\n email: u?.email ?? `client-${clientId}`,\n color: u?.color ?? \"#94a3b8\",\n };\n }\n\n // Everything that isn't `user` or `visible` is presence payload.\n const presence: PresencePayload = {};\n for (const [k, v] of Object.entries(s)) {\n if (k !== \"user\" && k !== \"visible\") {\n presence[k] = v;\n }\n }\n\n result.push({ clientId, user, presence, isAgent });\n });\n return result;\n }\n\n function onAwarenessChange(changes?: {\n added: number[];\n updated: number[];\n removed: number[];\n }) {\n // The awareness \"change\" event fires for local-only state edits too\n // (e.g. this hook's own setPresence() calls, or the doc's\n // activeFileId/visible fields). When every changed client id is the\n // local client, the derived `others` array (which excludes the local\n // client) cannot have changed, so skip the re-render entirely.\n if (changes) {\n const changedIds = [\n ...changes.added,\n ...changes.updated,\n ...changes.removed,\n ];\n if (\n changedIds.length > 0 &&\n changedIds.every((id) => id === localClientId)\n ) {\n return;\n }\n }\n const next = derive();\n if (shallowEqualOthers(lastOthers, next)) return;\n lastOthers = next;\n setOthers(next);\n }\n\n // Derive immediately.\n lastOthers = derive();\n setOthers(lastOthers);\n awareness.on(\"change\", onAwarenessChange);\n return () => {\n awareness.off(\"change\", onAwarenessChange);\n };\n }, [awareness, localClientId]);\n\n const setPresence = useCallback((partial: PresencePayload) => {\n const aw = awarenessRef.current;\n if (!aw) return;\n for (const [k, v] of Object.entries(partial)) {\n aw.setLocalStateField(k, v);\n }\n }, []);\n\n return { others, setPresence };\n}\n\n// ---------------------------------------------------------------------------\n// Normalized cursor coordinate helpers\n// ---------------------------------------------------------------------------\n\nexport interface NormalizedPoint {\n /** 0–1 fraction of container width. */\n x: number;\n /** 0–1 fraction of container height. */\n y: number;\n}\n\n/** Convert a pointer event offset to a normalized point. */\nexport function toNormalized(\n clientX: number,\n clientY: number,\n container: DOMRect,\n): NormalizedPoint {\n return {\n x: Math.max(0, Math.min(1, (clientX - container.left) / container.width)),\n y: Math.max(0, Math.min(1, (clientY - container.top) / container.height)),\n };\n}\n\n/** Convert a normalized point back to absolute offset within a container. */\nexport function fromNormalized(\n point: NormalizedPoint,\n container: DOMRect,\n): { x: number; y: number } {\n return {\n x: point.x * container.width,\n y: point.y * container.height,\n };\n}\n"]}
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
error: string;
|
|
30
29
|
ok?: undefined;
|
|
30
|
+
error: string;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -41,9 +41,9 @@ export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import
|
|
|
41
41
|
* Body: { text: string, fieldName?: string, requestSource?: string }
|
|
42
42
|
*/
|
|
43
43
|
export declare const postCollabText: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
44
|
+
text?: undefined;
|
|
44
45
|
ok?: undefined;
|
|
45
46
|
error: string;
|
|
46
|
-
text?: undefined;
|
|
47
47
|
} | {
|
|
48
48
|
error?: undefined;
|
|
49
49
|
ok: boolean;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
error: string;
|
|
17
16
|
ok?: undefined;
|
|
17
|
+
error: string;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
ok: boolean;
|