@1agh/maude 0.29.0 → 0.31.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/README.md +6 -6
- package/apps/studio/acp/bridge.ts +285 -0
- package/apps/studio/acp/env.ts +48 -0
- package/apps/studio/acp/index.ts +132 -0
- package/apps/studio/acp/probe.ts +112 -0
- package/apps/studio/acp/transcript.ts +149 -0
- package/apps/studio/ai-banner.tsx +0 -1
- package/apps/studio/annotations-align.ts +149 -0
- package/apps/studio/annotations-bindings.ts +197 -0
- package/apps/studio/annotations-context-toolbar.tsx +436 -186
- package/apps/studio/annotations-groups.ts +270 -0
- package/apps/studio/annotations-layer.tsx +1714 -1661
- package/apps/studio/annotations-model.ts +2077 -0
- package/apps/studio/annotations-snap.ts +125 -0
- package/apps/studio/api.ts +433 -182
- package/apps/studio/bin/_png-playwright.mjs +1 -1
- package/apps/studio/bin/annotate.mjs +732 -0
- package/apps/studio/bin/annotate.sh +17 -0
- package/apps/studio/bin/chat-open.sh +44 -0
- package/apps/studio/bin/read-annotations.mjs +152 -17
- package/apps/studio/build.ts +1 -1
- package/apps/studio/canvas-arrowheads.ts +78 -9
- package/apps/studio/canvas-cursors.ts +2 -0
- package/apps/studio/canvas-edit.ts +257 -7
- package/apps/studio/canvas-icons.tsx +105 -0
- package/apps/studio/canvas-lib.tsx +112 -19
- package/apps/studio/canvas-list-watch.ts +177 -0
- package/apps/studio/canvas-shell.tsx +326 -9
- package/apps/studio/client/app.jsx +3579 -250
- package/apps/studio/client/canvas-url.js +5 -0
- package/apps/studio/client/github.js +99 -0
- package/apps/studio/client/index.html +1 -1
- package/apps/studio/client/panels/ChatPanel.jsx +770 -0
- package/apps/studio/client/panels/CollabModelInfographic.jsx +71 -0
- package/apps/studio/client/panels/CreateProject.jsx +334 -0
- package/apps/studio/client/panels/DiffView.jsx +590 -0
- package/apps/studio/client/panels/GitPanel.jsx +767 -0
- package/apps/studio/client/panels/IdentityBar.jsx +294 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +563 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +349 -0
- package/apps/studio/client/panels/acp-runtime.js +286 -0
- package/apps/studio/client/panels/chat-markdown.jsx +138 -0
- package/apps/studio/client/panels/git-grouping.js +86 -0
- package/apps/studio/client/styles/0-reset.css +4 -0
- package/apps/studio/client/styles/3-shell-maude.css +1326 -14
- package/apps/studio/client/styles/4-components.css +39 -0
- package/apps/studio/client/styles/5-maude-overrides.css +41 -2
- package/apps/studio/client/styles/6-acp-chat.css +771 -0
- package/apps/studio/client/styles/_index.css +2 -0
- package/apps/studio/client/tour/collab-tour.js +61 -0
- package/apps/studio/client/tour/overlay.jsx +116 -18
- package/apps/studio/client/tour/usage-tour.js +23 -10
- package/apps/studio/client/whats-new.jsx +25 -10
- package/apps/studio/collab/registry.ts +13 -0
- package/apps/studio/collab/room.ts +36 -0
- package/apps/studio/commands/annotation-strokes-command.ts +1 -1
- package/apps/studio/commands/move-artboards-command.ts +1 -1
- package/apps/studio/comments-overlay.tsx +7 -5
- package/apps/studio/context-menu.tsx +4 -3
- package/apps/studio/contextual-toolbar.tsx +14 -0
- package/apps/studio/cursors-overlay.tsx +37 -21
- package/apps/studio/dist/client.bundle.js +31795 -1829
- package/apps/studio/dist/comment-mount.js +97 -9
- package/apps/studio/dist/styles.css +7938 -1599
- package/apps/studio/dom-selection.ts +115 -0
- package/apps/studio/export-dialog.tsx +3 -3
- package/apps/studio/git/endpoints.ts +338 -0
- package/apps/studio/git/service.ts +1334 -0
- package/apps/studio/git/watch.ts +97 -0
- package/apps/studio/github/endpoints.ts +358 -0
- package/apps/studio/github/service.ts +231 -0
- package/apps/studio/github/token.ts +53 -0
- package/apps/studio/history.ts +20 -3
- package/apps/studio/hmr-broadcast.ts +9 -2
- package/apps/studio/http.ts +508 -6
- package/apps/studio/input-router.tsx +7 -0
- package/apps/studio/inspect.ts +37 -8
- package/apps/studio/participants-chrome.tsx +70 -16
- package/apps/studio/paths.ts +12 -0
- package/apps/studio/scaffold-design.ts +57 -0
- package/apps/studio/server.ts +70 -4
- package/apps/studio/sync/agent.ts +311 -57
- package/apps/studio/sync/codec.ts +69 -0
- package/apps/studio/sync/cold-start.ts +198 -0
- package/apps/studio/sync/connection-state.ts +58 -2
- package/apps/studio/sync/hub-link.ts +137 -0
- package/apps/studio/sync/index.ts +563 -221
- package/apps/studio/sync/journal.ts +190 -0
- package/apps/studio/sync/migrate-seed.ts +202 -44
- package/apps/studio/sync/projection.ts +22 -3
- package/apps/studio/sync/status.ts +15 -4
- package/apps/studio/test/acp-bridge.test.ts +127 -0
- package/apps/studio/test/acp-env.test.ts +65 -0
- package/apps/studio/test/acp-origin-gate.test.ts +78 -0
- package/apps/studio/test/acp-transcript.test.ts +112 -0
- package/apps/studio/test/activity.test.ts +1 -7
- package/apps/studio/test/annotate-write.test.ts +184 -0
- package/apps/studio/test/annotations-align.test.ts +88 -0
- package/apps/studio/test/annotations-bindings.test.ts +124 -0
- package/apps/studio/test/annotations-groups.test.ts +231 -0
- package/apps/studio/test/annotations-snap.test.ts +79 -0
- package/apps/studio/test/canvas-create-api.test.ts +72 -0
- package/apps/studio/test/canvas-edit.test.ts +181 -1
- package/apps/studio/test/canvas-list-watch.test.ts +322 -0
- package/apps/studio/test/canvas-meta-api.test.ts +161 -27
- package/apps/studio/test/canvas-origin-gate.test.ts +43 -0
- package/apps/studio/test/chat-markdown.test.tsx +58 -0
- package/apps/studio/test/collab-bridge.test.ts +0 -2
- package/apps/studio/test/collab-room.test.ts +2 -7
- package/apps/studio/test/collab-session-survival.test.tsx +176 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +5 -0
- package/apps/studio/test/csrf-write-guard.test.ts +78 -0
- package/apps/studio/test/editing-presence.test.ts +103 -0
- package/apps/studio/test/figjam-v3-model.test.ts +342 -0
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +45 -0
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +106 -0
- package/apps/studio/test/git-grouping.test.ts +106 -0
- package/apps/studio/test/git-watch.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +465 -0
- package/apps/studio/test/hub-link.test.ts +69 -0
- package/apps/studio/test/participants-chrome.test.ts +36 -1
- package/apps/studio/test/shared-doc-migrate.test.ts +224 -14
- package/apps/studio/test/sync-agent.test.ts +272 -2
- package/apps/studio/test/sync-codec.test.ts +65 -0
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +187 -0
- package/apps/studio/test/sync-cold-start.test.ts +304 -0
- package/apps/studio/test/sync-connection-state.test.ts +68 -0
- package/apps/studio/test/sync-hardening.test.ts +0 -8
- package/apps/studio/test/sync-hubs-config.test.ts +0 -1
- package/apps/studio/test/sync-incident-replay.test.ts +211 -0
- package/apps/studio/test/sync-journal.test.ts +176 -0
- package/apps/studio/test/sync-runtime.test.ts +360 -5
- package/apps/studio/test/sync-status.test.ts +66 -0
- package/apps/studio/test/tour-overlay.test.tsx +18 -0
- package/apps/studio/test/use-annotation-resize.test.ts +159 -0
- package/apps/studio/test/use-artboard-drag.test.ts +0 -1
- package/apps/studio/test/use-collab.test.ts +0 -0
- package/apps/studio/test/use-tool-mode.test.tsx +5 -2
- package/apps/studio/tool-palette.tsx +30 -11
- package/apps/studio/undo-hud.tsx +0 -1
- package/apps/studio/use-agent-presence.tsx +4 -2
- package/apps/studio/use-annotation-resize.tsx +484 -54
- package/apps/studio/use-annotation-selection.tsx +16 -4
- package/apps/studio/use-canvas-activity.tsx +2 -2
- package/apps/studio/use-chrome-visibility.tsx +66 -0
- package/apps/studio/use-collab.tsx +434 -202
- package/apps/studio/use-selection-set.tsx +9 -0
- package/apps/studio/use-tool-mode.tsx +5 -1
- package/apps/studio/whats-new.json +208 -10
- package/apps/studio/whats-new.schema.json +18 -2
- package/apps/studio/ws.ts +44 -1
- package/cli/commands/design-link.test.mjs +84 -0
- package/cli/commands/design.mjs +3 -1
- package/cli/commands/design.test.mjs +5 -1
- package/cli/lib/design-link.mjs +51 -1
- package/cli/lib/gitignore-block.mjs +16 -3
- package/cli/lib/gitignore-block.test.mjs +13 -1
- package/package.json +11 -9
- package/plugins/design/dependencies.json +17 -0
- package/plugins/design/templates/_shell.html +58 -12
|
@@ -68,6 +68,8 @@ function computeId(componentName: string, idx: number): string {
|
|
|
68
68
|
|
|
69
69
|
interface OpeningHit {
|
|
70
70
|
opening: AnyNode;
|
|
71
|
+
/** The full JSXElement node — `editText` needs `.children` (JSXText), not just the opening tag. */
|
|
72
|
+
element: AnyNode;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
/**
|
|
@@ -108,7 +110,7 @@ function findOpening(program: AnyNode, targetId: string): OpeningHit | null {
|
|
|
108
110
|
frame.jsxIndex += 1;
|
|
109
111
|
const id = computeId(frame.componentName, idx);
|
|
110
112
|
if (id === targetId) {
|
|
111
|
-
hit = { opening: node.openingElement };
|
|
113
|
+
hit = { opening: node.openingElement, element: node };
|
|
112
114
|
}
|
|
113
115
|
if (!hit) {
|
|
114
116
|
if (node.openingElement) visit(node.openingElement.attributes);
|
|
@@ -200,6 +202,111 @@ export async function editAttribute(
|
|
|
200
202
|
});
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Remove an attribute (or one inline-style property) from the element with the
|
|
207
|
+
* given `data-cd-id` — the "reset to original" path (Phase 12.3). `attr` follows
|
|
208
|
+
* the same shape as `editAttribute`: `style.<camelOrKebab>` removes one inline
|
|
209
|
+
* style key (dropping the whole `style={{}}` when it was the last key); any other
|
|
210
|
+
* name removes that plain JSX attribute. A missing key/attribute is a no-op
|
|
211
|
+
* (delta 0), never an error. Same atomic write + per-file lock as `editAttribute`.
|
|
212
|
+
*/
|
|
213
|
+
export async function removeAttribute(
|
|
214
|
+
canvasAbsPath: string,
|
|
215
|
+
id: string,
|
|
216
|
+
attr: string
|
|
217
|
+
): Promise<EditResult> {
|
|
218
|
+
return withLock(canvasAbsPath, async () => {
|
|
219
|
+
const file = Bun.file(canvasAbsPath);
|
|
220
|
+
if (!(await file.exists())) {
|
|
221
|
+
throw new CanvasEditError(`Canvas not found: ${canvasAbsPath}`, {
|
|
222
|
+
canvas: canvasAbsPath,
|
|
223
|
+
id,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const source = await file.text();
|
|
227
|
+
const next = applyRemove(canvasAbsPath, source, id, attr);
|
|
228
|
+
if (next.source === source) return { source, delta: 0 };
|
|
229
|
+
const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
|
|
230
|
+
await Bun.write(tmp, next.source);
|
|
231
|
+
const { rename } = await import('node:fs/promises');
|
|
232
|
+
await rename(tmp, canvasAbsPath);
|
|
233
|
+
return next;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Pure variant of `removeAttribute` — exposed for tests. */
|
|
238
|
+
export function applyRemove(
|
|
239
|
+
canvasAbsPath: string,
|
|
240
|
+
source: string,
|
|
241
|
+
id: string,
|
|
242
|
+
attr: string
|
|
243
|
+
): EditResult {
|
|
244
|
+
const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
|
|
245
|
+
if (parsed.errors && parsed.errors.length > 0) {
|
|
246
|
+
const first = parsed.errors[0];
|
|
247
|
+
throw new CanvasEditError(
|
|
248
|
+
`oxc-parser failed on ${canvasAbsPath}: ${first?.message ?? 'unknown'}`,
|
|
249
|
+
{
|
|
250
|
+
canvas: canvasAbsPath,
|
|
251
|
+
id,
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
const hit = findOpening(parsed.program, id);
|
|
256
|
+
if (!hit) {
|
|
257
|
+
throw new CanvasEditError(`data-cd-id "${id}" not found in ${canvasAbsPath}`, {
|
|
258
|
+
canvas: canvasAbsPath,
|
|
259
|
+
id,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const s = new MagicString(source);
|
|
263
|
+
if (attr.startsWith('style.')) {
|
|
264
|
+
removeStyleProp(s, hit.opening, attr.slice('style.'.length), source);
|
|
265
|
+
} else if (attr === 'data-cd-id') {
|
|
266
|
+
throw new CanvasEditError('data-cd-id is owned by the pipeline; cannot be removed', {
|
|
267
|
+
canvas: canvasAbsPath,
|
|
268
|
+
id,
|
|
269
|
+
});
|
|
270
|
+
} else {
|
|
271
|
+
removeStringAttr(s, hit.opening, attr, source);
|
|
272
|
+
}
|
|
273
|
+
const out = s.toString();
|
|
274
|
+
return { source: out, delta: out.length - source.length };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Apply an inline TEXT-content edit to the JSX element with the given
|
|
279
|
+
* `data-cd-id`. Leaf-text only: the element's children must be exactly one
|
|
280
|
+
* `JSXText` node (whitespace-only siblings are ignored). Mixed/expression
|
|
281
|
+
* children (`<b>x</b>`, `{count}`) throw `CanvasEditError` — the caller should
|
|
282
|
+
* surface a "use /design:edit" refusal rather than guess. The text is
|
|
283
|
+
* JSX-escaped before it touches source. Same atomic write + per-file lock as
|
|
284
|
+
* `editAttribute`. See DDR-103.
|
|
285
|
+
*/
|
|
286
|
+
export async function editText(
|
|
287
|
+
canvasAbsPath: string,
|
|
288
|
+
id: string,
|
|
289
|
+
text: string
|
|
290
|
+
): Promise<EditResult> {
|
|
291
|
+
return withLock(canvasAbsPath, async () => {
|
|
292
|
+
const file = Bun.file(canvasAbsPath);
|
|
293
|
+
if (!(await file.exists())) {
|
|
294
|
+
throw new CanvasEditError(`Canvas not found: ${canvasAbsPath}`, {
|
|
295
|
+
canvas: canvasAbsPath,
|
|
296
|
+
id,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const source = await file.text();
|
|
300
|
+
const next = applyTextEdit(canvasAbsPath, source, id, text);
|
|
301
|
+
if (next.source === source) return { source, delta: 0 };
|
|
302
|
+
const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
|
|
303
|
+
await Bun.write(tmp, next.source);
|
|
304
|
+
const { rename } = await import('node:fs/promises');
|
|
305
|
+
await rename(tmp, canvasAbsPath);
|
|
306
|
+
return next;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
203
310
|
/**
|
|
204
311
|
* Pure variant — exposed for tests + in-memory pipelines. Caller owns
|
|
205
312
|
* persistence. Throws CanvasEditError if the ID isn't found or the edit shape
|
|
@@ -245,6 +352,68 @@ export function applyEdit(
|
|
|
245
352
|
return { source: out, delta: out.length - source.length };
|
|
246
353
|
}
|
|
247
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Pure variant of `editText` — parse, locate the JSXText child, overwrite its
|
|
357
|
+
* source span (preserving the original leading/trailing whitespace so JSX
|
|
358
|
+
* indentation survives), escaping the new text. Throws `CanvasEditError` for a
|
|
359
|
+
* missing id, no text content, or mixed/expression children.
|
|
360
|
+
*/
|
|
361
|
+
export function applyTextEdit(
|
|
362
|
+
canvasAbsPath: string,
|
|
363
|
+
source: string,
|
|
364
|
+
id: string,
|
|
365
|
+
text: string
|
|
366
|
+
): EditResult {
|
|
367
|
+
const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
|
|
368
|
+
if (parsed.errors && parsed.errors.length > 0) {
|
|
369
|
+
const first = parsed.errors[0];
|
|
370
|
+
throw new CanvasEditError(
|
|
371
|
+
`oxc-parser failed on ${canvasAbsPath}: ${first?.message ?? 'unknown'}`,
|
|
372
|
+
{ canvas: canvasAbsPath, id }
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const hit = findOpening(parsed.program, id);
|
|
377
|
+
if (!hit) {
|
|
378
|
+
throw new CanvasEditError(`data-cd-id "${id}" not found in ${canvasAbsPath}`, {
|
|
379
|
+
canvas: canvasAbsPath,
|
|
380
|
+
id,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const children: AnyNode[] = Array.isArray(hit.element?.children) ? hit.element.children : [];
|
|
385
|
+
// Ignore whitespace-only JSXText siblings (`<button>\n Save\n</button>` parses
|
|
386
|
+
// as one JSXText; `<a>\n <b/>\n</a>` parses as ws + element + ws — the ws is
|
|
387
|
+
// noise). What's left is the "real" content.
|
|
388
|
+
const meaningful = children.filter(
|
|
389
|
+
(c) => !(c?.type === 'JSXText' && typeof c.value === 'string' && c.value.trim() === '')
|
|
390
|
+
);
|
|
391
|
+
if (meaningful.length === 0) {
|
|
392
|
+
throw new CanvasEditError(`element "${id}" has no editable text content`, {
|
|
393
|
+
canvas: canvasAbsPath,
|
|
394
|
+
id,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const only = meaningful[0];
|
|
398
|
+
if (meaningful.length > 1 || only?.type !== 'JSXText') {
|
|
399
|
+
throw new CanvasEditError(
|
|
400
|
+
`element "${id}" has mixed or expression content — edit it via /design:edit`,
|
|
401
|
+
{ canvas: canvasAbsPath, id }
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const start = only.start as number;
|
|
406
|
+
const end = only.end as number;
|
|
407
|
+
const raw = source.slice(start, end);
|
|
408
|
+
// Preserve the original indentation/newline framing; swap only the visible text.
|
|
409
|
+
const lead = /^\s*/.exec(raw)?.[0] ?? '';
|
|
410
|
+
const trail = /\s*$/.exec(raw)?.[0] ?? '';
|
|
411
|
+
const s = new MagicString(source);
|
|
412
|
+
s.overwrite(start, end, `${lead}${escapeJsxText(text)}${trail}`);
|
|
413
|
+
const out = s.toString();
|
|
414
|
+
return { source: out, delta: out.length - source.length };
|
|
415
|
+
}
|
|
416
|
+
|
|
248
417
|
// ---------------------------------------------------------------------------
|
|
249
418
|
// Edit shapes.
|
|
250
419
|
|
|
@@ -263,14 +432,20 @@ function editStringAttr(s: MagicString, opening: AnyNode, name: string, value: s
|
|
|
263
432
|
return;
|
|
264
433
|
}
|
|
265
434
|
if (v.type === 'Literal' || v.type === 'StringLiteral') {
|
|
266
|
-
// Replace
|
|
267
|
-
|
|
435
|
+
// Replace the whole `"value"` (quotes included) so we control the escaping.
|
|
436
|
+
// The value lands in a JSX *attribute*, where `"` must become `"` and
|
|
437
|
+
// `<`/`>` their entities — NOT JS backslash escaping. `JSON.stringify` would
|
|
438
|
+
// emit `\"`, which is invalid in a JSX attribute and corrupts the source on
|
|
439
|
+
// any value containing a double quote. Use the same `escapeAttr` as the two
|
|
440
|
+
// insert branches so all four paths agree. See DDR-103 / DDR-105.
|
|
441
|
+
s.overwrite(v.start, v.end, `"${escapeAttr(value)}"`);
|
|
268
442
|
return;
|
|
269
443
|
}
|
|
270
444
|
if (v.type === 'JSXExpressionContainer') {
|
|
271
445
|
// Replace the whole `{...}` with a plain quoted literal — keeps the
|
|
272
|
-
// resulting JSX readable
|
|
273
|
-
|
|
446
|
+
// resulting JSX readable. Same JSX-attribute escaping as above (NOT
|
|
447
|
+
// `JSON.stringify`, which would JS-escape a `"` and corrupt the source).
|
|
448
|
+
s.overwrite(v.start, v.end, `"${escapeAttr(value)}"`);
|
|
274
449
|
return;
|
|
275
450
|
}
|
|
276
451
|
// Unknown shape — refuse rather than corrupt.
|
|
@@ -289,6 +464,24 @@ function escapeAttr(value: string): string {
|
|
|
289
464
|
return value.replace(/"/g, '"').replace(/[<>]/g, (c) => (c === '<' ? '<' : '>'));
|
|
290
465
|
}
|
|
291
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Escape JSX-significant characters so user-typed text lands in source `.tsx`
|
|
469
|
+
* as inert TEXT, never as markup or an expression. The text is written between
|
|
470
|
+
* an element's tags (`<button>HERE</button>`), where `<`/`>` would start a tag
|
|
471
|
+
* and `{`/`}` would open a JSX expression — so all four (plus a bare `&`, which
|
|
472
|
+
* begins an entity) are encoded. This is the load-bearing guard that keeps the
|
|
473
|
+
* inline text editor (Phase 12) from being a source-injection vector. See
|
|
474
|
+
* DDR-103.
|
|
475
|
+
*/
|
|
476
|
+
function escapeJsxText(value: string): string {
|
|
477
|
+
return value
|
|
478
|
+
.replace(/&/g, '&')
|
|
479
|
+
.replace(/</g, '<')
|
|
480
|
+
.replace(/>/g, '>')
|
|
481
|
+
.replace(/\{/g, '{')
|
|
482
|
+
.replace(/\}/g, '}');
|
|
483
|
+
}
|
|
484
|
+
|
|
292
485
|
function editStyleProp(
|
|
293
486
|
s: MagicString,
|
|
294
487
|
opening: AnyNode,
|
|
@@ -308,14 +501,14 @@ function editStyleProp(
|
|
|
308
501
|
return;
|
|
309
502
|
}
|
|
310
503
|
const v = attr.value;
|
|
311
|
-
if (
|
|
504
|
+
if (v?.type !== 'JSXExpressionContainer') {
|
|
312
505
|
throw new CanvasEditError(
|
|
313
506
|
`style attribute on ${id} is not a {{...}} expression — refusing to edit`,
|
|
314
507
|
{ canvas: canvasAbsPath, id }
|
|
315
508
|
);
|
|
316
509
|
}
|
|
317
510
|
const obj = v.expression;
|
|
318
|
-
if (
|
|
511
|
+
if (obj?.type !== 'ObjectExpression') {
|
|
319
512
|
throw new CanvasEditError(
|
|
320
513
|
`style={...} on ${id} is not an inline ObjectExpression — refusing to edit`,
|
|
321
514
|
{ canvas: canvasAbsPath, id }
|
|
@@ -349,6 +542,63 @@ function editStyleProp(
|
|
|
349
542
|
void tail;
|
|
350
543
|
}
|
|
351
544
|
|
|
545
|
+
/**
|
|
546
|
+
* Remove a single inline-style property (the "reset to original" path — DDR-104
|
|
547
|
+
* Phase 12.3). No-op when the style attribute or the key is absent. When the key
|
|
548
|
+
* was the object's ONLY property, the whole `style={{…}}` attribute is removed so
|
|
549
|
+
* we don't leave an empty `style={{}}` behind.
|
|
550
|
+
*/
|
|
551
|
+
function removeStyleProp(s: MagicString, opening: AnyNode, prop: string, source: string): boolean {
|
|
552
|
+
const attr = findAttribute(opening, 'style');
|
|
553
|
+
if (!attr) return false;
|
|
554
|
+
const v = attr.value;
|
|
555
|
+
if (v?.type !== 'JSXExpressionContainer') return false;
|
|
556
|
+
const obj = v.expression;
|
|
557
|
+
if (obj?.type !== 'ObjectExpression') return false;
|
|
558
|
+
const props = (obj.properties as AnyNode[]).filter(
|
|
559
|
+
(p) => p?.type === 'Property' || p?.type === 'ObjectProperty'
|
|
560
|
+
);
|
|
561
|
+
const propCamel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
562
|
+
const idx = props.findIndex((p) => {
|
|
563
|
+
const k = p.key;
|
|
564
|
+
const kname =
|
|
565
|
+
k?.type === 'Identifier' ? k.name : k?.type === 'Literal' ? String(k.value) : null;
|
|
566
|
+
return kname === prop || kname === propCamel;
|
|
567
|
+
});
|
|
568
|
+
if (idx === -1) return false;
|
|
569
|
+
|
|
570
|
+
// Only property → drop the whole `style={{…}}` attribute (plus the leading space).
|
|
571
|
+
if (props.length === 1) {
|
|
572
|
+
const start = source[attr.start - 1] === ' ' ? attr.start - 1 : attr.start;
|
|
573
|
+
s.remove(start, attr.end);
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
// Otherwise remove the one property + one adjacent comma/whitespace run so the
|
|
577
|
+
// remaining object stays well-formed: consume forward to the next prop's start
|
|
578
|
+
// (eats the trailing `, `), or — for the last prop — backward from the previous
|
|
579
|
+
// prop's end (eats the leading `, `).
|
|
580
|
+
const target = props[idx];
|
|
581
|
+
if (idx < props.length - 1) {
|
|
582
|
+
s.remove(target.start as number, props[idx + 1].start as number);
|
|
583
|
+
} else {
|
|
584
|
+
s.remove(props[idx - 1].end as number, target.end as number);
|
|
585
|
+
}
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Remove a plain JSX attribute (the custom-attribute "reset" path). No-op when
|
|
591
|
+
* absent. Refuses `data-cd-id` / `style` (pipeline-owned / wrong endpoint).
|
|
592
|
+
*/
|
|
593
|
+
function removeStringAttr(s: MagicString, opening: AnyNode, name: string, source: string): boolean {
|
|
594
|
+
if (name === 'data-cd-id' || name === 'style') return false;
|
|
595
|
+
const attr = findAttribute(opening, name);
|
|
596
|
+
if (!attr) return false;
|
|
597
|
+
const start = source[attr.start - 1] === ' ' ? attr.start - 1 : attr.start;
|
|
598
|
+
s.remove(start, attr.end);
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
|
|
352
602
|
/**
|
|
353
603
|
* Render a JS object key — bare identifier when the prop is camelCase + valid
|
|
354
604
|
* JS id, quoted otherwise. Mirrors how authors write JSX styles.
|
|
@@ -473,6 +473,8 @@ export const TOOL_ICONS: Record<string, (p: IconProps) => JSX.Element> = {
|
|
|
473
473
|
rect: IconRect,
|
|
474
474
|
ellipse: IconEllipse,
|
|
475
475
|
sticky: IconSticky,
|
|
476
|
+
// FigJam v3 — labelled organizing container.
|
|
477
|
+
section: IconSection,
|
|
476
478
|
arrow: IconArrow,
|
|
477
479
|
text: IconText,
|
|
478
480
|
eraser: IconEraser,
|
|
@@ -487,3 +489,106 @@ export const SHAPE_KIND_ICONS: Record<string, (p: IconProps) => JSX.Element> = {
|
|
|
487
489
|
triangle: IconTriangle,
|
|
488
490
|
'triangle-down': IconTriangleDown,
|
|
489
491
|
};
|
|
492
|
+
|
|
493
|
+
// ── FigJam v3 — multi-select manipulation icons ──────────────────────────────
|
|
494
|
+
// Object align (an edge line + two boxes), distribute (rails + a box), and
|
|
495
|
+
// group/ungroup. Same 24×24 stroke language as the rest of the set.
|
|
496
|
+
|
|
497
|
+
export function IconObjAlignLeft(props: IconProps) {
|
|
498
|
+
return (
|
|
499
|
+
<Svg {...props}>
|
|
500
|
+
<path d="M4 3v18" />
|
|
501
|
+
<rect x="7" y="6" width="11" height="4" rx="1" />
|
|
502
|
+
<rect x="7" y="14" width="6" height="4" rx="1" />
|
|
503
|
+
</Svg>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
export function IconObjAlignHCenter(props: IconProps) {
|
|
507
|
+
return (
|
|
508
|
+
<Svg {...props}>
|
|
509
|
+
<path d="M12 3v2M12 19v2" />
|
|
510
|
+
<rect x="6" y="6" width="12" height="4" rx="1" />
|
|
511
|
+
<rect x="8" y="14" width="8" height="4" rx="1" />
|
|
512
|
+
</Svg>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
export function IconObjAlignRight(props: IconProps) {
|
|
516
|
+
return (
|
|
517
|
+
<Svg {...props}>
|
|
518
|
+
<path d="M20 3v18" />
|
|
519
|
+
<rect x="6" y="6" width="11" height="4" rx="1" />
|
|
520
|
+
<rect x="11" y="14" width="6" height="4" rx="1" />
|
|
521
|
+
</Svg>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
export function IconObjAlignTop(props: IconProps) {
|
|
525
|
+
return (
|
|
526
|
+
<Svg {...props}>
|
|
527
|
+
<path d="M3 4h18" />
|
|
528
|
+
<rect x="6" y="7" width="4" height="11" rx="1" />
|
|
529
|
+
<rect x="14" y="7" width="4" height="6" rx="1" />
|
|
530
|
+
</Svg>
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
export function IconObjAlignVCenter(props: IconProps) {
|
|
534
|
+
return (
|
|
535
|
+
<Svg {...props}>
|
|
536
|
+
<path d="M3 12h2M19 12h2" />
|
|
537
|
+
<rect x="6" y="6" width="4" height="12" rx="1" />
|
|
538
|
+
<rect x="14" y="8" width="4" height="8" rx="1" />
|
|
539
|
+
</Svg>
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
export function IconObjAlignBottom(props: IconProps) {
|
|
543
|
+
return (
|
|
544
|
+
<Svg {...props}>
|
|
545
|
+
<path d="M3 20h18" />
|
|
546
|
+
<rect x="6" y="6" width="4" height="11" rx="1" />
|
|
547
|
+
<rect x="14" y="11" width="4" height="6" rx="1" />
|
|
548
|
+
</Svg>
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
export function IconDistributeH(props: IconProps) {
|
|
552
|
+
return (
|
|
553
|
+
<Svg {...props}>
|
|
554
|
+
<path d="M4 3v18M20 3v18" />
|
|
555
|
+
<rect x="9" y="8" width="6" height="8" rx="1" />
|
|
556
|
+
</Svg>
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
export function IconDistributeV(props: IconProps) {
|
|
560
|
+
return (
|
|
561
|
+
<Svg {...props}>
|
|
562
|
+
<path d="M3 4h18M3 20h18" />
|
|
563
|
+
<rect x="8" y="9" width="8" height="6" rx="1" />
|
|
564
|
+
</Svg>
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
export function IconGroup(props: IconProps) {
|
|
568
|
+
return (
|
|
569
|
+
<Svg {...props}>
|
|
570
|
+
<path d="M3 7V4a1 1 0 011-1h3M17 3h3a1 1 0 011 1v3M21 17v3a1 1 0 01-1 1h-3M7 21H4a1 1 0 01-1-1v-3" />
|
|
571
|
+
<rect x="7" y="7" width="6" height="6" rx="1" />
|
|
572
|
+
<rect x="11" y="11" width="6" height="6" rx="1" />
|
|
573
|
+
</Svg>
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
export function IconUngroup(props: IconProps) {
|
|
577
|
+
return (
|
|
578
|
+
<Svg {...props}>
|
|
579
|
+
<rect x="4" y="4" width="7" height="7" rx="1" />
|
|
580
|
+
<rect x="13" y="13" width="7" height="7" rx="1" />
|
|
581
|
+
<path d="M15 6l3 3M6 15l3 3" />
|
|
582
|
+
</Svg>
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** FigJam v3 — section tool: a region with its name chip docked top-left. */
|
|
587
|
+
export function IconSection(props: IconProps) {
|
|
588
|
+
return (
|
|
589
|
+
<Svg {...props}>
|
|
590
|
+
<rect x="4" y="8" width="16" height="12" rx="2" />
|
|
591
|
+
<path d="M4 4h7" />
|
|
592
|
+
</Svg>
|
|
593
|
+
);
|
|
594
|
+
}
|
|
@@ -102,6 +102,7 @@ import {
|
|
|
102
102
|
matchesArtboard,
|
|
103
103
|
useCanvasActivity,
|
|
104
104
|
} from './use-canvas-activity.tsx';
|
|
105
|
+
import { useChromeVisibility } from './use-chrome-visibility.tsx';
|
|
105
106
|
import { CollabProvider, canvasSlugFromPath } from './use-collab.tsx';
|
|
106
107
|
import { useSelectionSetOptional } from './use-selection-set.tsx';
|
|
107
108
|
import { MaybeToolProvider, useToolModeOptional } from './use-tool-mode.tsx';
|
|
@@ -118,6 +119,17 @@ const WHEEL_ZOOM_K = 0.0015; // larger = more sensitive wheel
|
|
|
118
119
|
const SETTLE_MS = 500;
|
|
119
120
|
const PUBLISH_MS = 50;
|
|
120
121
|
|
|
122
|
+
// WebKit (Safari + the Tauri WKWebView native shell, DDR-106) vs Blink (Chrome).
|
|
123
|
+
// The two engines render the CSS `zoom` property incompatibly (see writeTransform),
|
|
124
|
+
// so the scale dimension takes a different path on each. Detected via Apple's
|
|
125
|
+
// `navigator.vendor` and the WebKit-only `GestureEvent` global — both absent in
|
|
126
|
+
// Chrome/Edge/Firefox, present in Safari and WKWebView. Evaluated once at module
|
|
127
|
+
// load; the canvas always runs in a browser/webview context.
|
|
128
|
+
const IS_WEBKIT =
|
|
129
|
+
typeof navigator !== 'undefined' &&
|
|
130
|
+
(/apple/i.test(navigator.vendor || '') ||
|
|
131
|
+
(typeof window !== 'undefined' && 'GestureEvent' in window));
|
|
132
|
+
|
|
121
133
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
122
134
|
// Engine CSS (Phase 4) — injected once per iframe inside DesignCanvas's mount.
|
|
123
135
|
// The visual chrome of `.dc-artboard` (borders, label strip, SKU type) still
|
|
@@ -130,6 +142,10 @@ const ENGINE_CSS = `
|
|
|
130
142
|
inset: 0;
|
|
131
143
|
overflow: hidden;
|
|
132
144
|
outline: none;
|
|
145
|
+
/* WebKit text inflation must not re-grow artboard text that the world scales
|
|
146
|
+
down on zoom-out. Inherited, so this covers every descendant. No-op on Blink. */
|
|
147
|
+
-webkit-text-size-adjust: 100%;
|
|
148
|
+
text-size-adjust: 100%;
|
|
133
149
|
/* DDR-046 — snap-layer magenta is distinct from --accent so the snap chrome
|
|
134
150
|
never visually melts into the selection halo during a drag-snap gesture.
|
|
135
151
|
OKLCH default approximates FigJam magenta in the project's color space. */
|
|
@@ -653,27 +669,47 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
653
669
|
jumpTargetsRef.current = jumpTargets;
|
|
654
670
|
|
|
655
671
|
// worldRef is stable across renders — read inside callbacks lazily, no dep.
|
|
656
|
-
// Use CSS `zoom` (not `transform: scale`) for the scale dimension. `zoom`
|
|
657
|
-
// re-flows layout at the new size so the browser re-rasterizes text at the
|
|
658
|
-
// target resolution — text stays crisp at any zoom level. `transform: scale`
|
|
659
|
-
// upscales a cached layer, which produces the pixelation users see at
|
|
660
|
-
// zoom > ~1.5. CSS `zoom` is supported in Chrome / Safari / Edge (always)
|
|
661
|
-
// and Firefox 126+; for a dev-server design tool that's full coverage.
|
|
662
672
|
//
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
673
|
+
// The scale dimension takes an engine-specific path because the CSS `zoom`
|
|
674
|
+
// property is NOT interoperable between Blink and WebKit:
|
|
675
|
+
//
|
|
676
|
+
// • Blink (Chrome): CSS `zoom`. `zoom` re-flows layout at the new size so the
|
|
677
|
+
// browser re-rasterizes text at the target resolution — text stays crisp at
|
|
678
|
+
// any zoom level (whereas `transform: scale` upscales a cached layer →
|
|
679
|
+
// pixelation at zoom > ~1.5). Blink composites `zoom` changes on a fast path,
|
|
680
|
+
// so pinch stays smooth. Unchanged from the original implementation.
|
|
681
|
+
// ! Subtle: under `zoom: N`, a co-located `transform: translate(Xpx, Ypx)`
|
|
682
|
+
// ! translates by N×X / N×Y screen px (translate is in pre-zoom coords, then
|
|
683
|
+
// ! the layer is zoomed). `vpRef` holds the translate in *screen* px, so we
|
|
684
|
+
// ! divide by zoom at write time. pan/zoom math stays screen-px throughout.
|
|
685
|
+
//
|
|
686
|
+
// • WebKit (Safari + the Tauri WKWebView shell): `transform: translate() scale()`.
|
|
687
|
+
// On WebKit, `zoom` co-located with a `transform` does NOT cascade scale into
|
|
688
|
+
// descendant text (fonts stay fixed size — the reported bug) and forces a
|
|
689
|
+
// main-thread relayout on every pinch tick (janky trackpad zoom). A composited
|
|
690
|
+
// `transform: scale` scales all descendants uniformly incl. text and never
|
|
691
|
+
// relayouts. transform-origin is pinned top-left so the affine matches the
|
|
692
|
+
// `screen = translate + scale·world` model the rest of the math assumes — so
|
|
693
|
+
// here the translate is plain screen px (no /z). Crispness at high zoom is
|
|
694
|
+
// recovered by releasing the compositor layer on settle (`crisp` → will-change
|
|
695
|
+
// auto), which prompts WebKit to re-rasterize text at the target scale.
|
|
670
696
|
// biome-ignore lint/correctness/useExhaustiveDependencies: refs only — stable identity by design.
|
|
671
|
-
const writeTransform = useCallback((v: ViewportState) => {
|
|
697
|
+
const writeTransform = useCallback((v: ViewportState, opts?: { crisp?: boolean }) => {
|
|
672
698
|
const el = worldRef.current;
|
|
673
699
|
if (!el) return;
|
|
674
700
|
const z = v.zoom || 1;
|
|
675
|
-
|
|
676
|
-
|
|
701
|
+
if (IS_WEBKIT) {
|
|
702
|
+
el.style.zoom = '';
|
|
703
|
+
el.style.transformOrigin = '0 0';
|
|
704
|
+
el.style.transform = `translate(${v.x}px, ${v.y}px) scale(${z})`;
|
|
705
|
+
// Mid-gesture keep the layer promoted (smooth); when settled release it so
|
|
706
|
+
// WebKit re-paints text crisp at the new scale instead of upscaling the
|
|
707
|
+
// cached bitmap.
|
|
708
|
+
el.style.willChange = opts?.crisp ? 'auto' : 'transform';
|
|
709
|
+
} else {
|
|
710
|
+
el.style.transform = `translate(${v.x / z}px, ${v.y / z}px)`;
|
|
711
|
+
el.style.zoom = String(z);
|
|
712
|
+
}
|
|
677
713
|
el.style.visibility = 'visible';
|
|
678
714
|
}, []);
|
|
679
715
|
|
|
@@ -710,8 +746,12 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
710
746
|
isInteractingStateRef.current = false;
|
|
711
747
|
setIsInteracting(false);
|
|
712
748
|
interactEndTimerRef.current = null;
|
|
749
|
+
// Motion has stopped — re-write the world transform in crisp mode so the
|
|
750
|
+
// WebKit path drops its compositor layer and re-rasterizes text sharply at
|
|
751
|
+
// the settled scale. No-op on the Blink path (CSS `zoom` is already crisp).
|
|
752
|
+
writeTransform(vpRef.current, { crisp: true });
|
|
713
753
|
}, 220);
|
|
714
|
-
}, []);
|
|
754
|
+
}, [writeTransform]);
|
|
715
755
|
|
|
716
756
|
const applyViewport = useCallback(
|
|
717
757
|
(next: ViewportState) => {
|
|
@@ -866,7 +906,7 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
866
906
|
const initial = getInitial();
|
|
867
907
|
if (initial) {
|
|
868
908
|
vpRef.current = { ...initial };
|
|
869
|
-
writeTransform(vpRef.current);
|
|
909
|
+
writeTransform(vpRef.current, { crisp: true });
|
|
870
910
|
setViewportPublished({ ...vpRef.current });
|
|
871
911
|
}
|
|
872
912
|
// If host has no size yet, refit when ResizeObserver delivers one.
|
|
@@ -879,7 +919,7 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
879
919
|
hadSize = true;
|
|
880
920
|
const refit = computeFitRef.current();
|
|
881
921
|
vpRef.current = { ...refit };
|
|
882
|
-
writeTransform(vpRef.current);
|
|
922
|
+
writeTransform(vpRef.current, { crisp: true });
|
|
883
923
|
setViewportPublished({ ...vpRef.current });
|
|
884
924
|
}
|
|
885
925
|
});
|
|
@@ -917,6 +957,11 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
917
957
|
lastY: number;
|
|
918
958
|
} = { active: false, pointerId: -1, lastX: 0, lastY: 0 };
|
|
919
959
|
|
|
960
|
+
// Safari/WKWebView trackpad pinch — see gesture* handlers below. While a
|
|
961
|
+
// native gesture is in flight, the ctrlKey-wheel branch defers to it so a
|
|
962
|
+
// pinch doesn't double-apply (WebKit may emit both event streams).
|
|
963
|
+
const gesture = { active: false, lastScale: 1 };
|
|
964
|
+
|
|
920
965
|
const onWheel = (e: WheelEvent) => {
|
|
921
966
|
e.preventDefault();
|
|
922
967
|
const rect = host.getBoundingClientRect();
|
|
@@ -926,6 +971,7 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
926
971
|
// without a physical Ctrl press — so the same branch covers both
|
|
927
972
|
// Ctrl+wheel (mouse) and pinch-zoom (trackpad).
|
|
928
973
|
if (e.ctrlKey || e.metaKey) {
|
|
974
|
+
if (gesture.active) return; // WebKit GestureEvent owns this pinch
|
|
929
975
|
// T32 — clamp deltaY into [-50, 50] before the exp() to bring
|
|
930
976
|
// trackpad-pinch (small per-frame delta) and mouse-wheel (one
|
|
931
977
|
// notch = ±100) onto the same perceived-speed curve. Mouse-wheel
|
|
@@ -951,6 +997,38 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
951
997
|
panBy(-e.deltaX, -e.deltaY);
|
|
952
998
|
};
|
|
953
999
|
|
|
1000
|
+
// Safari/WKWebView-only trackpad pinch. WebKit exposes the native pinch via
|
|
1001
|
+
// `gesture*` events (absent in Blink — these listeners simply never fire
|
|
1002
|
+
// there). Two reasons to handle them: (1) preventDefault suppresses Safari's
|
|
1003
|
+
// built-in page-zoom that would otherwise fight the canvas; (2) `e.scale`
|
|
1004
|
+
// (cumulative since gesturestart) gives a smoother, 1:1 pinch than reverse-
|
|
1005
|
+
// engineering it from synthesized ctrlKey-wheel deltas. The onWheel ctrlKey
|
|
1006
|
+
// branch defers while `gesture.active` so a pinch is never applied twice.
|
|
1007
|
+
type SafariGestureEvent = Event & { scale: number; clientX: number; clientY: number };
|
|
1008
|
+
const onGestureStart = (ev: Event) => {
|
|
1009
|
+
ev.preventDefault();
|
|
1010
|
+
const e = ev as SafariGestureEvent;
|
|
1011
|
+
gesture.active = true;
|
|
1012
|
+
gesture.lastScale = e.scale || 1;
|
|
1013
|
+
};
|
|
1014
|
+
const onGestureChange = (ev: Event) => {
|
|
1015
|
+
ev.preventDefault();
|
|
1016
|
+
if (!gesture.active) return;
|
|
1017
|
+
const e = ev as SafariGestureEvent;
|
|
1018
|
+
const rect = host.getBoundingClientRect();
|
|
1019
|
+
const cx = e.clientX - rect.left;
|
|
1020
|
+
const cy = e.clientY - rect.top;
|
|
1021
|
+
const s = e.scale || 1;
|
|
1022
|
+
const factor = gesture.lastScale > 0 ? s / gesture.lastScale : 1;
|
|
1023
|
+
gesture.lastScale = s;
|
|
1024
|
+
zoomAt(factor, cx, cy);
|
|
1025
|
+
};
|
|
1026
|
+
const onGestureEnd = (ev: Event) => {
|
|
1027
|
+
ev.preventDefault();
|
|
1028
|
+
gesture.active = false;
|
|
1029
|
+
gesture.lastScale = 1;
|
|
1030
|
+
};
|
|
1031
|
+
|
|
954
1032
|
const onPointerDown = (e: PointerEvent) => {
|
|
955
1033
|
const isMiddle = e.button === 1;
|
|
956
1034
|
const isLeftWithSpace = e.button === 0 && spaceHeld.current;
|
|
@@ -1072,6 +1150,11 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
1072
1150
|
host.addEventListener('pointermove', onPointerMove);
|
|
1073
1151
|
host.addEventListener('pointerup', endPan);
|
|
1074
1152
|
host.addEventListener('pointercancel', endPan);
|
|
1153
|
+
// WebKit-only; no-ops elsewhere. passive:false so preventDefault can cancel
|
|
1154
|
+
// Safari's native page-zoom.
|
|
1155
|
+
host.addEventListener('gesturestart', onGestureStart, { passive: false });
|
|
1156
|
+
host.addEventListener('gesturechange', onGestureChange, { passive: false });
|
|
1157
|
+
host.addEventListener('gestureend', onGestureEnd, { passive: false });
|
|
1075
1158
|
|
|
1076
1159
|
return () => {
|
|
1077
1160
|
doc.removeEventListener('wheel', captureWheel, { capture: true } as EventListenerOptions);
|
|
@@ -1082,6 +1165,9 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
1082
1165
|
host.removeEventListener('pointermove', onPointerMove);
|
|
1083
1166
|
host.removeEventListener('pointerup', endPan);
|
|
1084
1167
|
host.removeEventListener('pointercancel', endPan);
|
|
1168
|
+
host.removeEventListener('gesturestart', onGestureStart);
|
|
1169
|
+
host.removeEventListener('gesturechange', onGestureChange);
|
|
1170
|
+
host.removeEventListener('gestureend', onGestureEnd);
|
|
1085
1171
|
};
|
|
1086
1172
|
}, [hostRef]);
|
|
1087
1173
|
|
|
@@ -2035,6 +2121,7 @@ export function DCMiniMap() {
|
|
|
2035
2121
|
ensureOverlayStyles();
|
|
2036
2122
|
const world = useWorldContext();
|
|
2037
2123
|
const controller = useViewportControllerContext();
|
|
2124
|
+
const chrome = useChromeVisibility();
|
|
2038
2125
|
const bodyRef = useRef<HTMLDivElement | null>(null);
|
|
2039
2126
|
// 132 - 22 (header) = 110 body height; width matches the chrome.
|
|
2040
2127
|
const MAP_W = 196;
|
|
@@ -2045,6 +2132,9 @@ export function DCMiniMap() {
|
|
|
2045
2132
|
});
|
|
2046
2133
|
|
|
2047
2134
|
if (!world || !controller) return null;
|
|
2135
|
+
// Menubar "View ▸ Minimap" toggle + Presentation Mode (which hides ALL
|
|
2136
|
+
// chrome). `chrome` is null in a bare DS specimen — then default-visible.
|
|
2137
|
+
if (chrome && (!chrome.minimap || chrome.present)) return null;
|
|
2048
2138
|
|
|
2049
2139
|
const geometry = computeMiniMapGeometry(world.artboards, MAP_W, MAP_BODY_H);
|
|
2050
2140
|
const host = world.hostRef.current;
|
|
@@ -2165,7 +2255,10 @@ DCMiniMap.displayName = 'DCMiniMap';
|
|
|
2165
2255
|
export function DCZoomToolbar() {
|
|
2166
2256
|
ensureOverlayStyles();
|
|
2167
2257
|
const controller = useViewportControllerContext();
|
|
2258
|
+
const chrome = useChromeVisibility();
|
|
2168
2259
|
if (!controller) return null;
|
|
2260
|
+
// Menubar "View ▸ Zoom controls" toggle + Presentation Mode.
|
|
2261
|
+
if (chrome && (!chrome.zoom || chrome.present)) return null;
|
|
2169
2262
|
const pct = Math.round(controller.viewport.zoom * 100);
|
|
2170
2263
|
return (
|
|
2171
2264
|
<div className="dc-zoom-tb" role="toolbar" aria-label="Zoom">
|