@agent-native/core 0.90.5 → 0.90.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/README.md +34 -7
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +13 -0
- package/corpus/core/docs/content/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/toolkit-agent-ux.mdx +1 -1
- package/corpus/core/docs/content/toolkit-app-adapters.mdx +1 -1
- package/corpus/core/docs/content/toolkit-collaboration-ui.mdx +1 -1
- package/corpus/core/docs/content/toolkit-collaboration.mdx +1 -1
- package/corpus/core/docs/content/toolkit-command-navigation.mdx +2 -2
- package/corpus/core/docs/content/toolkit-comments-review.mdx +1 -1
- package/corpus/core/docs/content/toolkit-history.mdx +1 -1
- package/corpus/core/docs/content/toolkit-observability.mdx +1 -1
- package/corpus/core/docs/content/toolkit-org-team.mdx +1 -1
- package/corpus/core/docs/content/toolkit-resources.mdx +1 -1
- package/corpus/core/docs/content/toolkit-settings.mdx +1 -1
- package/corpus/core/docs/content/toolkit-setup-connections.mdx +1 -1
- package/corpus/core/docs/content/toolkit-sharing-ui.mdx +1 -1
- package/corpus/core/docs/content/toolkit-sharing.mdx +1 -1
- package/corpus/core/docs/content/toolkit-shell-hooks.mdx +1 -1
- package/corpus/core/docs/content/toolkit-ui.mdx +1 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/use-db-sync.ts +99 -1
- package/corpus/core/src/collab/client.ts +49 -4
- package/corpus/core/src/deploy/build.ts +103 -0
- package/corpus/core/src/server/core-routes-plugin.ts +137 -8
- package/corpus/templates/clips/changelog/2026-07-07-faster-local-whisper-dictation-start.md +6 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +311 -31
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +2 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +9 -0
- package/corpus/templates/design/actions/read-source-file.ts +28 -0
- package/corpus/templates/design/actions/update-file.ts +76 -7
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +181 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +4 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +387 -25
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1110 -88
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +368 -16
- package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +24 -0
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +27 -6
- package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +18 -0
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +5 -4
- package/corpus/templates/design/app/components/design/multi-screen/handle-hit-zones.ts +124 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +23 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +19 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-a-free-floating-element-into-a-layout-column-or-row.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-a-repeated-list-item-like-a-to-do-card-generated-fr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-an-element-onto-a-button-or-chip-s-middle-no-longer.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-elements-dropped-into-another-board-shape-now-stay-exactly-w.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-drag-reordering-elements-within-a-screen-no-longer-sil.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-an-element-from-a-screen-onto-the-canvas-now-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-elements-between-screens-into-ai-generated-co.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-moving-elements-between-screens-no-longer-bakes-intern.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-the-cross-screen-drop-indicator-now-points-at-the-corr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-the-zoom-menu-no-longer-leaves-the-canvas-unresponsive.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-text-drawn-on-the-canvas-is-now-readable-white-instead-of-in.md +6 -0
- package/corpus/templates/design/server/source-workspace.ts +27 -3
- package/corpus/templates/design/shared/board-file.ts +359 -0
- package/corpus/templates/design/shared/breakpoint-media.ts +21 -3
- package/corpus/templates/design/shared/code-layer.ts +145 -10
- package/dist/client/use-db-sync.d.ts +18 -0
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +88 -1
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +43 -4
- package/dist/collab/client.js.map +1 -1
- package/dist/deploy/build.d.ts +1 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +57 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +10 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +103 -9
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/docs/content/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
- package/docs/content/toolkit-agent-ux.mdx +1 -1
- package/docs/content/toolkit-app-adapters.mdx +1 -1
- package/docs/content/toolkit-collaboration-ui.mdx +1 -1
- package/docs/content/toolkit-collaboration.mdx +1 -1
- package/docs/content/toolkit-command-navigation.mdx +2 -2
- package/docs/content/toolkit-comments-review.mdx +1 -1
- package/docs/content/toolkit-history.mdx +1 -1
- package/docs/content/toolkit-observability.mdx +1 -1
- package/docs/content/toolkit-org-team.mdx +1 -1
- package/docs/content/toolkit-resources.mdx +1 -1
- package/docs/content/toolkit-settings.mdx +1 -1
- package/docs/content/toolkit-setup-connections.mdx +1 -1
- package/docs/content/toolkit-sharing-ui.mdx +1 -1
- package/docs/content/toolkit-sharing.mdx +1 -1
- package/docs/content/toolkit-shell-hooks.mdx +1 -1
- package/docs/content/toolkit-ui.mdx +1 -1
- package/package.json +1 -1
|
@@ -239,6 +239,10 @@ import {
|
|
|
239
239
|
getChromeLabelTransition,
|
|
240
240
|
getSelectionBoxTransition,
|
|
241
241
|
} from "./multi-screen/chrome-transitions";
|
|
242
|
+
import {
|
|
243
|
+
getCornerHandleGeometry,
|
|
244
|
+
getEdgeHandleHitGeometry,
|
|
245
|
+
} from "./multi-screen/handle-hit-zones";
|
|
242
246
|
|
|
243
247
|
export function isDirectScreenHoverTarget(
|
|
244
248
|
target: EventTarget | null,
|
|
@@ -274,6 +278,38 @@ export function isOsFileDrag(event: {
|
|
|
274
278
|
return false;
|
|
275
279
|
}
|
|
276
280
|
|
|
281
|
+
// PERF9-WHEEL: module-scoped "a wheel/pinch camera gesture is in flight"
|
|
282
|
+
// flag, set by markWheelGestureActive on the first applied flush and cleared
|
|
283
|
+
// by commitView once the gesture settles (or on canvas unmount). Lives at
|
|
284
|
+
// module scope (not React state) because its consumers are event handlers —
|
|
285
|
+
// DesignEditor's hover callbacks — that must read the CURRENT value without
|
|
286
|
+
// any render round-trip: flipping React state for this at gesture start
|
|
287
|
+
// re-rendered every Screen and measurably stalled the first wheel ticks.
|
|
288
|
+
//
|
|
289
|
+
// Single-canvas-instance assumption: this flag is process-wide, not scoped
|
|
290
|
+
// to a particular MultiScreenCanvas mount. The editor only ever renders one
|
|
291
|
+
// MultiScreenCanvas at a time, so that's fine in practice — but if a second
|
|
292
|
+
// instance were ever mounted concurrently (e.g. a future side-by-side or
|
|
293
|
+
// picture-in-picture view), both would read/write the SAME flag and could
|
|
294
|
+
// stomp each other's gesture state. The unmount cleanup effect below clears
|
|
295
|
+
// it UNCONDITIONALLY (no ownership/identity check), so an unmounting second
|
|
296
|
+
// instance would also incorrectly clear a still-active gesture that belongs
|
|
297
|
+
// to the other, still-mounted instance. Revisit this if that assumption ever
|
|
298
|
+
// changes.
|
|
299
|
+
let wheelCameraGestureActive = false;
|
|
300
|
+
|
|
301
|
+
/** True while a MultiScreenCanvas wheel/pinch camera gesture is in flight
|
|
302
|
+
* (first applied wheel flush → settled debounced commit). DesignEditor's
|
|
303
|
+
* hover handlers consult this to skip hover setState mid-gesture: muting
|
|
304
|
+
* the content layers at gesture start flips the element under the cursor,
|
|
305
|
+
* firing a hover-clear that would otherwise cost a full-tree React render
|
|
306
|
+
* exactly when the pan needs the main thread most. Hover state simply goes
|
|
307
|
+
* stale for the gesture's duration (same contract as a space-drag pan) and
|
|
308
|
+
* recomputes from the next real pointer event after settle. */
|
|
309
|
+
export function isWheelCameraGestureActive(): boolean {
|
|
310
|
+
return wheelCameraGestureActive;
|
|
311
|
+
}
|
|
312
|
+
|
|
277
313
|
export function shouldBoardSurfaceCapturePointerEvents(args: {
|
|
278
314
|
tool: MultiScreenCanvasTool | string;
|
|
279
315
|
gestureActive?: boolean;
|
|
@@ -296,6 +332,80 @@ export function shouldShowFrameFullViewButton(args: {
|
|
|
296
332
|
return args.emphasized || !!args.showFullView || !!args.childHoverActive;
|
|
297
333
|
}
|
|
298
334
|
|
|
335
|
+
/** How long an armed duplicate-commit suppression stays valid while the
|
|
336
|
+
* created file(s) round-trip through the server before appearing in
|
|
337
|
+
* `screens`. Generous vs. the usual create-file latency, but short enough
|
|
338
|
+
* that an armed flag left behind by a failed mutation can't swallow a much
|
|
339
|
+
* later, unrelated screen-count recenter. */
|
|
340
|
+
export const LINEUP_RECENTER_SUPPRESS_MAX_AGE_MS = 10_000;
|
|
341
|
+
|
|
342
|
+
/** Armed by a duplicate commit (alt-drag drop or Cmd+D) so the
|
|
343
|
+
* lineup-recenter effect can recognise — and skip — exactly the screen-count
|
|
344
|
+
* transition(s) that duplicate is about to produce. */
|
|
345
|
+
export interface LineupRecenterDuplicateArm {
|
|
346
|
+
atMs: number;
|
|
347
|
+
/** `screens.length` at the moment the duplicate was dispatched. */
|
|
348
|
+
fromCount: number;
|
|
349
|
+
/** How many duplicates were dispatched (alt-drag: 1; Cmd+D: one per
|
|
350
|
+
* selected frame — they land one server round-trip at a time). */
|
|
351
|
+
addedCount: number;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Whether the "center the lineup when the screen footprint changes" effect
|
|
356
|
+
* must SKIP its camera move for this run. A duplicate (alt-drag drop OR
|
|
357
|
+
* Cmd+D) places the clone deliberately relative to content the user is
|
|
358
|
+
* already looking at — Figma keeps the camera still on that commit, but the
|
|
359
|
+
* recenter effect (keyed on `screens.length`) would otherwise pan/zoom the
|
|
360
|
+
* whole board the moment each new screen lands, making every untouched
|
|
361
|
+
* sibling visibly shift. The duplicate commit arms `armed` with a timestamp,
|
|
362
|
+
* the pre-duplicate screen count, and how many clones were dispatched; the
|
|
363
|
+
* effect suppresses only transitions inside `(fromCount, fromCount +
|
|
364
|
+
* addedCount]`, only while fresh, and never for device-frame-preview-driven
|
|
365
|
+
* runs — so initial-load fit, explicit Zoom-to-fit (cameraCommand, a
|
|
366
|
+
* different path entirely), toolbar add-screen recenters, and device-frame
|
|
367
|
+
* changes all keep their behavior. Pure/exported for unit tests.
|
|
368
|
+
*/
|
|
369
|
+
export function shouldSuppressLineupRecenter(args: {
|
|
370
|
+
armed: LineupRecenterDuplicateArm | null;
|
|
371
|
+
nowMs: number;
|
|
372
|
+
screenCount: number;
|
|
373
|
+
deviceFrameChanged: boolean;
|
|
374
|
+
maxAgeMs?: number;
|
|
375
|
+
}): boolean {
|
|
376
|
+
if (!args.armed) return false;
|
|
377
|
+
if (args.deviceFrameChanged) return false;
|
|
378
|
+
const age = args.nowMs - args.armed.atMs;
|
|
379
|
+
if (age < 0 || age > (args.maxAgeMs ?? LINEUP_RECENTER_SUPPRESS_MAX_AGE_MS)) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
return (
|
|
383
|
+
args.screenCount > args.armed.fromCount && // i18n-ignore — pure boolean expression, guard misreads the parenthesized return in this .tsx as JSX text
|
|
384
|
+
args.screenCount <= args.armed.fromCount + args.armed.addedCount
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* True when this lineup-recenter run was caused purely by the screen count
|
|
390
|
+
* SHRINKING (delete, or undo of a duplicate/add). Nothing new needs to
|
|
391
|
+
* become reachable when screens disappear, and Figma keeps the camera still
|
|
392
|
+
* on delete/undo — so the recenter effect skips these runs entirely (undo
|
|
393
|
+
* after a duplicate previously yanked the camera a second time).
|
|
394
|
+
* Device-frame-preview changes still recenter regardless of count direction.
|
|
395
|
+
* Pure/exported for unit tests.
|
|
396
|
+
*/
|
|
397
|
+
export function isLineupShrinkOnlyChange(args: {
|
|
398
|
+
previousCount: number | null;
|
|
399
|
+
screenCount: number;
|
|
400
|
+
deviceFrameChanged: boolean;
|
|
401
|
+
}): boolean {
|
|
402
|
+
return (
|
|
403
|
+
!args.deviceFrameChanged &&
|
|
404
|
+
args.previousCount !== null &&
|
|
405
|
+
args.screenCount < args.previousCount
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
299
409
|
/**
|
|
300
410
|
* Item 8b — whether an overview breakpoint frame's "…" menu affordance
|
|
301
411
|
* (Change width / Remove) should render: gated by edit permission AND at
|
|
@@ -569,10 +679,37 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
569
679
|
const dragState = useRef<DragState | null>(null);
|
|
570
680
|
const dragCleanup = useRef<(() => void) | null>(null);
|
|
571
681
|
const duplicateCleanup = useRef<(() => void) | null>(null);
|
|
682
|
+
// Armed by a duplicate commit (alt-drag drop or Cmd+D) so the
|
|
683
|
+
// lineup-recenter effect keeps the camera still when the
|
|
684
|
+
// deliberately-placed clone(s) land in `screens` — see
|
|
685
|
+
// shouldSuppressLineupRecenter.
|
|
686
|
+
const lineupRecenterSuppressRef = useRef<LineupRecenterDuplicateArm | null>(
|
|
687
|
+
null,
|
|
688
|
+
);
|
|
689
|
+
const lineupRecenterDeviceFrameRef = useRef(previewDeviceFrame);
|
|
690
|
+
const lineupRecenterPrevCountRef = useRef<number | null>(null);
|
|
572
691
|
const handledSelectAllRequestRef = useRef(selectAllRequest);
|
|
573
692
|
const handledClearSelectionRequestRef = useRef(clearSelectionRequest);
|
|
574
693
|
const [isDragging, setIsDragging] = useState(false);
|
|
575
694
|
const [isPanning, setIsPanning] = useState(false);
|
|
695
|
+
// PERF9-WHEEL: an in-flight wheel/pinch gesture imperatively mutes pointer
|
|
696
|
+
// events on every live preview iframe (screens + board surface), restoring
|
|
697
|
+
// each element's prior inline value once the gesture settles (commitView).
|
|
698
|
+
// Without this, every wheel-pan tick moves live iframes under a stationary
|
|
699
|
+
// cursor, the browser re-hit-tests into them, and their hover/hit-test
|
|
700
|
+
// bridges post pointermove/element-hover messages (thousands per 240-tick
|
|
701
|
+
// pan) that can setState in DesignEditor — ~20ms full-tree React renders
|
|
702
|
+
// mid-gesture, which is what capped wheel-pan well under 60fps while drags
|
|
703
|
+
// ran at ~98fps on the same board. Done with direct style writes on cached
|
|
704
|
+
// nodes (NOT React state): flipping a canvasGestureActive-style flag at
|
|
705
|
+
// gesture start re-rendered every Screen + mounted interaction shields,
|
|
706
|
+
// costing a measured ~75ms first-tick stall — the exact class of jank
|
|
707
|
+
// PERF9 exists to avoid. Same "imperative now, reconcile once at settle"
|
|
708
|
+
// contract as applyViewToDom/scheduleViewCommit.
|
|
709
|
+
const wheelGestureActiveRef = useRef(false);
|
|
710
|
+
const wheelGestureMutedElementsRef = useRef<Map<HTMLElement, string> | null>(
|
|
711
|
+
null,
|
|
712
|
+
);
|
|
576
713
|
const [marquee, setMarquee] = useState<MarqueeRect | null>(null);
|
|
577
714
|
const marqueeRef = useRef<MarqueeRect | null>(marquee);
|
|
578
715
|
const [alignmentGuides, setAlignmentGuidesRaw] = useState<AlignmentGuide[]>(
|
|
@@ -1202,6 +1339,42 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1202
1339
|
|
|
1203
1340
|
// Center the lineup when the screen footprint changes so new frames stay reachable.
|
|
1204
1341
|
useEffect(() => {
|
|
1342
|
+
const deviceFrameChanged =
|
|
1343
|
+
lineupRecenterDeviceFrameRef.current !== previewDeviceFrame;
|
|
1344
|
+
lineupRecenterDeviceFrameRef.current = previewDeviceFrame;
|
|
1345
|
+
const previousCount = lineupRecenterPrevCountRef.current;
|
|
1346
|
+
lineupRecenterPrevCountRef.current = screens.length;
|
|
1347
|
+
// Screen-count decreases (delete, undo of a duplicate) never recenter —
|
|
1348
|
+
// see isLineupShrinkOnlyChange.
|
|
1349
|
+
if (
|
|
1350
|
+
isLineupShrinkOnlyChange({
|
|
1351
|
+
previousCount,
|
|
1352
|
+
screenCount: screens.length,
|
|
1353
|
+
deviceFrameChanged,
|
|
1354
|
+
})
|
|
1355
|
+
) {
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
// A duplicate (alt-drag drop or Cmd+D) placed the new screen(s)
|
|
1359
|
+
// deliberately: keep the camera still for exactly those screen-count
|
|
1360
|
+
// transitions (see shouldSuppressLineupRecenter's doc for what this must
|
|
1361
|
+
// NOT affect).
|
|
1362
|
+
const armed = lineupRecenterSuppressRef.current;
|
|
1363
|
+
if (
|
|
1364
|
+
shouldSuppressLineupRecenter({
|
|
1365
|
+
armed,
|
|
1366
|
+
nowMs: Date.now(),
|
|
1367
|
+
screenCount: screens.length,
|
|
1368
|
+
deviceFrameChanged,
|
|
1369
|
+
})
|
|
1370
|
+
) {
|
|
1371
|
+
// Disarm only once every dispatched clone has landed — a multi-frame
|
|
1372
|
+
// Cmd+D's duplicates arrive one create-file round-trip at a time.
|
|
1373
|
+
if (armed && screens.length >= armed.fromCount + armed.addedCount) {
|
|
1374
|
+
lineupRecenterSuppressRef.current = null;
|
|
1375
|
+
}
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1205
1378
|
if (!surfaceRef.current || screens.length === 0) return;
|
|
1206
1379
|
const rect = surfaceRef.current.getBoundingClientRect();
|
|
1207
1380
|
const scale = zoomRef.current / 100;
|
|
@@ -1278,6 +1451,14 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1278
1451
|
if (chromeSettleTimerRef.current !== null) {
|
|
1279
1452
|
window.clearTimeout(chromeSettleTimerRef.current);
|
|
1280
1453
|
}
|
|
1454
|
+
// PERF9-WHEEL: unmounting mid-gesture means the settled commitView
|
|
1455
|
+
// never runs — don't leave the module-scoped gesture flag stuck (the
|
|
1456
|
+
// muted elements unmount with the canvas, so no style restore needed).
|
|
1457
|
+
// Clears unconditionally (single-canvas-instance assumption — see the
|
|
1458
|
+
// module-scope doc comment on wheelCameraGestureActive above).
|
|
1459
|
+
wheelGestureActiveRef.current = false;
|
|
1460
|
+
wheelCameraGestureActive = false;
|
|
1461
|
+
wheelGestureMutedElementsRef.current = null;
|
|
1281
1462
|
};
|
|
1282
1463
|
}, []);
|
|
1283
1464
|
|
|
@@ -1515,6 +1696,27 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1515
1696
|
crossScreenLastHitResultRef.current.clear();
|
|
1516
1697
|
};
|
|
1517
1698
|
|
|
1699
|
+
// Single source of truth for a target screen's "viewport" dimensions used
|
|
1700
|
+
// to scale board<->iframe coordinates. MUST prefer the iframe's live
|
|
1701
|
+
// clientWidth/clientHeight over resolveScreenMetadata's DEFAULTED
|
|
1702
|
+
// 1280x2560 fallback (used for screens — e.g. duplicated screens — with
|
|
1703
|
+
// no screenMetadata entry in designs.data). runHitTest and
|
|
1704
|
+
// getTargetLocalPoint already did this correctly; requestCrossScreenDropGuide
|
|
1705
|
+
// previously called getResolvedMetadata directly with no iframe fallback,
|
|
1706
|
+
// so the drawn guide used different (often wrong-by-4x) geometry than the
|
|
1707
|
+
// hit-test that produced it — guides rendered squashed/mispositioned for
|
|
1708
|
+
// any screen missing metadata. Centralizing here keeps all three call
|
|
1709
|
+
// sites in sync going forward.
|
|
1710
|
+
const getTargetViewportMetadata = (
|
|
1711
|
+
targetScreen: (typeof screensRef.current)[number],
|
|
1712
|
+
targetIframe: HTMLIFrameElement | null | undefined,
|
|
1713
|
+
): { width: number; height: number } => ({
|
|
1714
|
+
width:
|
|
1715
|
+
targetIframe?.clientWidth || getResolvedMetadata(targetScreen).width,
|
|
1716
|
+
height:
|
|
1717
|
+
targetIframe?.clientHeight || getResolvedMetadata(targetScreen).height,
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1518
1720
|
const runHitTest = (
|
|
1519
1721
|
candidate: CrossScreenDragTarget,
|
|
1520
1722
|
boardPoint: Point,
|
|
@@ -1531,10 +1733,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1531
1733
|
);
|
|
1532
1734
|
const targetContentWindow = targetIframe?.contentWindow;
|
|
1533
1735
|
if (!targetContentWindow) return Promise.resolve({});
|
|
1534
|
-
const targetViewportWidth =
|
|
1535
|
-
|
|
1536
|
-
const targetViewportHeight =
|
|
1537
|
-
targetIframe.clientHeight || getResolvedMetadata(targetScreen).height;
|
|
1736
|
+
const { width: targetViewportWidth, height: targetViewportHeight } =
|
|
1737
|
+
getTargetViewportMetadata(targetScreen, targetIframe);
|
|
1538
1738
|
const scaleX = targetViewportWidth / Math.max(1, targetGeometry.width);
|
|
1539
1739
|
const scaleY = targetViewportHeight / Math.max(1, targetGeometry.height);
|
|
1540
1740
|
const localX = (boardPoint.x - targetGeometry.x) * scaleX;
|
|
@@ -1573,6 +1773,18 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1573
1773
|
typeof ev.data.anchorNodeId === "string"
|
|
1574
1774
|
? ev.data.anchorNodeId
|
|
1575
1775
|
: undefined,
|
|
1776
|
+
// Id-on-demand handshake passthrough (see the fields' doc
|
|
1777
|
+
// comments on CrossScreenHitTestResult): without these, drops
|
|
1778
|
+
// into id-less AI-generated screens can never flow-insert.
|
|
1779
|
+
pendingNodeId:
|
|
1780
|
+
typeof ev.data.pendingNodeId === "string" && ev.data.pendingNodeId
|
|
1781
|
+
? ev.data.pendingNodeId
|
|
1782
|
+
: undefined,
|
|
1783
|
+
anchorSelector:
|
|
1784
|
+
typeof ev.data.anchorSelector === "string" &&
|
|
1785
|
+
ev.data.anchorSelector
|
|
1786
|
+
? ev.data.anchorSelector
|
|
1787
|
+
: undefined,
|
|
1576
1788
|
placement: isCrossScreenDropPlacement(ev.data.placement)
|
|
1577
1789
|
? ev.data.placement
|
|
1578
1790
|
: undefined,
|
|
@@ -1620,10 +1832,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1620
1832
|
const targetIframe = surfaceRef.current?.querySelector<HTMLIFrameElement>(
|
|
1621
1833
|
`[data-screen-iframe-id="${targetIframeId}"]`,
|
|
1622
1834
|
);
|
|
1623
|
-
const targetViewportWidth =
|
|
1624
|
-
|
|
1625
|
-
const targetViewportHeight =
|
|
1626
|
-
targetIframe?.clientHeight || getResolvedMetadata(targetScreen).height;
|
|
1835
|
+
const { width: targetViewportWidth, height: targetViewportHeight } =
|
|
1836
|
+
getTargetViewportMetadata(targetScreen, targetIframe);
|
|
1627
1837
|
const scaleX =
|
|
1628
1838
|
targetViewportWidth / Math.max(1, candidate.geometry.width);
|
|
1629
1839
|
const scaleY =
|
|
@@ -1645,11 +1855,22 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1645
1855
|
const targetScreen = screensRef.current.find(
|
|
1646
1856
|
(s) => s.id === candidate.id,
|
|
1647
1857
|
);
|
|
1858
|
+
const targetIframeId = targetScreen
|
|
1859
|
+
? CSS.escape(getActiveScreenIframeId(targetScreen))
|
|
1860
|
+
: null;
|
|
1861
|
+
const targetIframe = targetIframeId
|
|
1862
|
+
? surfaceRef.current?.querySelector<HTMLIFrameElement>(
|
|
1863
|
+
`[data-screen-iframe-id="${targetIframeId}"]`,
|
|
1864
|
+
)
|
|
1865
|
+
: null;
|
|
1648
1866
|
const guide = targetScreen
|
|
1649
1867
|
? getCrossScreenDropGuideForHitTest({
|
|
1650
1868
|
hit,
|
|
1651
1869
|
targetGeometry: candidate.geometry,
|
|
1652
|
-
targetMetadata:
|
|
1870
|
+
targetMetadata: getTargetViewportMetadata(
|
|
1871
|
+
targetScreen,
|
|
1872
|
+
targetIframe,
|
|
1873
|
+
),
|
|
1653
1874
|
})
|
|
1654
1875
|
: null;
|
|
1655
1876
|
setCrossScreenDropGuide(guide);
|
|
@@ -1737,7 +1958,14 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1737
1958
|
}
|
|
1738
1959
|
|
|
1739
1960
|
void runHitTest(targetCandidate, lastBoardPoint).then(
|
|
1740
|
-
({
|
|
1961
|
+
({
|
|
1962
|
+
anchorNodeId,
|
|
1963
|
+
pendingNodeId,
|
|
1964
|
+
anchorSelector,
|
|
1965
|
+
placement,
|
|
1966
|
+
dropMode,
|
|
1967
|
+
anchorRect,
|
|
1968
|
+
}) => {
|
|
1741
1969
|
const targetAnchorPlacement = isCrossScreenDropPlacement(placement)
|
|
1742
1970
|
? placement
|
|
1743
1971
|
: undefined;
|
|
@@ -1751,6 +1979,12 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1751
1979
|
sourceScreenId,
|
|
1752
1980
|
targetScreenId: targetCandidate.id,
|
|
1753
1981
|
targetAnchorNodeId: anchorNodeId,
|
|
1982
|
+
// Id-on-demand handshake (CrossScreenHitTestResult doc): lets
|
|
1983
|
+
// the drop handler persist the minted pending id into the
|
|
1984
|
+
// stored dest document and flow-insert instead of silently
|
|
1985
|
+
// degrading to absolute placement on id-less screens.
|
|
1986
|
+
targetAnchorPendingNodeId: pendingNodeId,
|
|
1987
|
+
targetAnchorSelector: anchorSelector,
|
|
1754
1988
|
targetAnchorPlacement,
|
|
1755
1989
|
targetDropMode: dropMode,
|
|
1756
1990
|
targetAnchorRect: anchorRect,
|
|
@@ -1959,12 +2193,28 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1959
2193
|
}
|
|
1960
2194
|
|
|
1961
2195
|
// Remember the latest drag payload for use on "end".
|
|
2196
|
+
//
|
|
2197
|
+
// Pointer-offset pin fix: the bridge recomputes `pointerOffset` on
|
|
2198
|
+
// EVERY "move" post from the dragged element's CURRENT
|
|
2199
|
+
// getBoundingClientRect — which keeps changing while a flow-reorder
|
|
2200
|
+
// drag live-reorders the source element under the still-in-bounds
|
|
2201
|
+
// pointer. Letting a later move's offset win (the previous `??`
|
|
2202
|
+
// fallback here never actually engaged, since the bridge always
|
|
2203
|
+
// supplies a value) made the eventual screen->canvas drop land at
|
|
2204
|
+
// `boardPoint - (whatever slot the element occupied at the LAST
|
|
2205
|
+
// in-source tick)` instead of the true press-time grab offset,
|
|
2206
|
+
// drifting the landing position by up to the element's own size.
|
|
2207
|
+
// Figma keeps the grab point pixel-pinned under the cursor for the
|
|
2208
|
+
// whole gesture — so once "start" has captured a pointer offset,
|
|
2209
|
+
// never let a "move" message's (re-derived, drifting) offset
|
|
2210
|
+
// override it; only fall back to a move-supplied offset on the rare
|
|
2211
|
+
// path where "start" itself didn't have one yet.
|
|
1962
2212
|
crossScreenDragMsgRef.current = {
|
|
1963
2213
|
selector: selector ?? "",
|
|
1964
2214
|
sourceId,
|
|
1965
2215
|
sourcePointerOffset:
|
|
1966
|
-
sourcePointerOffset ??
|
|
1967
|
-
|
|
2216
|
+
crossScreenDragMsgRef.current?.sourcePointerOffset ??
|
|
2217
|
+
sourcePointerOffset,
|
|
1968
2218
|
sourceElementSize:
|
|
1969
2219
|
sourceElementSize ??
|
|
1970
2220
|
crossScreenDragMsgRef.current?.sourceElementSize,
|
|
@@ -5000,6 +5250,14 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5000
5250
|
x: dropCanvasPosition.x - pointerOffset.x,
|
|
5001
5251
|
y: dropCanvasPosition.y - pointerOffset.y,
|
|
5002
5252
|
};
|
|
5253
|
+
// The user placed this clone deliberately — suppress the
|
|
5254
|
+
// lineup-recenter camera move when the created screen lands in
|
|
5255
|
+
// `screens` (Figma keeps the camera still on alt-drag duplicate).
|
|
5256
|
+
lineupRecenterSuppressRef.current = {
|
|
5257
|
+
atMs: Date.now(),
|
|
5258
|
+
fromCount: screensRef.current.length,
|
|
5259
|
+
addedCount: 1,
|
|
5260
|
+
};
|
|
5003
5261
|
onDuplicate(screen.id, {
|
|
5004
5262
|
mode,
|
|
5005
5263
|
screen,
|
|
@@ -5264,6 +5522,24 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5264
5522
|
const shouldSettleChrome = pendingChromeSettleRef.current;
|
|
5265
5523
|
pendingChromeSettleRef.current = false;
|
|
5266
5524
|
if (shouldSettleChrome) startChromeSettle();
|
|
5525
|
+
// PERF9-WHEEL: the gesture has settled — restore every muted content
|
|
5526
|
+
// layer's own prior inline pointer-events value (imperative, mirroring
|
|
5527
|
+
// how markWheelGestureActive muted them; restoring the recorded value,
|
|
5528
|
+
// not "", keeps React's inline-style bookkeeping consistent since React
|
|
5529
|
+
// only rewrites style properties it believes changed).
|
|
5530
|
+
if (wheelGestureActiveRef.current) {
|
|
5531
|
+
wheelGestureActiveRef.current = false;
|
|
5532
|
+
wheelCameraGestureActive = false;
|
|
5533
|
+
const muted = wheelGestureMutedElementsRef.current;
|
|
5534
|
+
wheelGestureMutedElementsRef.current = null;
|
|
5535
|
+
if (muted) {
|
|
5536
|
+
muted.forEach((previousPointerEvents, element) => {
|
|
5537
|
+
if (element.isConnected) {
|
|
5538
|
+
element.style.pointerEvents = previousPointerEvents;
|
|
5539
|
+
}
|
|
5540
|
+
});
|
|
5541
|
+
}
|
|
5542
|
+
}
|
|
5267
5543
|
setCanvasZoom(zoomRef.current);
|
|
5268
5544
|
setPan(panRef.current);
|
|
5269
5545
|
onZoomChange?.(zoomRef.current);
|
|
@@ -5322,6 +5598,36 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5322
5598
|
scheduleViewCommit();
|
|
5323
5599
|
}, [cameraCommand, applyViewToDom, scheduleViewCommit]);
|
|
5324
5600
|
|
|
5601
|
+
// PERF9-WHEEL: the first flush that actually moves the camera mutes
|
|
5602
|
+
// pointer events on every live preview iframe with direct style writes on
|
|
5603
|
+
// a snapshot NodeList (ref-guarded — runs ONCE per gesture, zero React
|
|
5604
|
+
// commits). commitView restores the recorded per-element values once the
|
|
5605
|
+
// gesture has been idle for the debounce beat. A mid-gesture React render
|
|
5606
|
+
// from unrelated state could rewrite an iframe's style and re-enable it
|
|
5607
|
+
// early; that only reverts to pre-fix behavior for the gesture's tail, so
|
|
5608
|
+
// it's an acceptable trade for keeping gesture start/end render-free.
|
|
5609
|
+
const markWheelGestureActive = useCallback(() => {
|
|
5610
|
+
if (wheelGestureActiveRef.current) return;
|
|
5611
|
+
wheelGestureActiveRef.current = true;
|
|
5612
|
+
wheelCameraGestureActive = true;
|
|
5613
|
+
const surface = surfaceRef.current;
|
|
5614
|
+
if (!surface) return;
|
|
5615
|
+
// Mute the same layers a drag's canvasGestureActive gates via props:
|
|
5616
|
+
// every screen's content wrapper and the board-surface layer — this
|
|
5617
|
+
// covers the live iframes AND DesignCanvas's parent-side hover/hit-test
|
|
5618
|
+
// overlays beneath them.
|
|
5619
|
+
const muted = new Map<HTMLElement, string>();
|
|
5620
|
+
surface
|
|
5621
|
+
.querySelectorAll<HTMLElement>(
|
|
5622
|
+
"[data-screen-content], [data-board-surface-layer]",
|
|
5623
|
+
)
|
|
5624
|
+
.forEach((element) => {
|
|
5625
|
+
muted.set(element, element.style.pointerEvents);
|
|
5626
|
+
element.style.pointerEvents = "none";
|
|
5627
|
+
});
|
|
5628
|
+
wheelGestureMutedElementsRef.current = muted;
|
|
5629
|
+
}, []);
|
|
5630
|
+
|
|
5325
5631
|
const flushPendingWheelGesture = useCallback(() => {
|
|
5326
5632
|
wheelGestureFrameRef.current = null;
|
|
5327
5633
|
const gesture = pendingWheelGestureRef.current;
|
|
@@ -5350,6 +5656,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5350
5656
|
});
|
|
5351
5657
|
zoomRef.current = nextZoom;
|
|
5352
5658
|
panRef.current = nextPan;
|
|
5659
|
+
markWheelGestureActive();
|
|
5353
5660
|
applyViewToDom();
|
|
5354
5661
|
scheduleViewCommit({ settleChrome: true });
|
|
5355
5662
|
return;
|
|
@@ -5360,9 +5667,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5360
5667
|
y: panRef.current.y - gesture.deltaY,
|
|
5361
5668
|
};
|
|
5362
5669
|
panRef.current = nextPan;
|
|
5670
|
+
markWheelGestureActive();
|
|
5363
5671
|
applyViewToDom();
|
|
5364
5672
|
scheduleViewCommit();
|
|
5365
|
-
}, [applyViewToDom, scheduleViewCommit]);
|
|
5673
|
+
}, [applyViewToDom, markWheelGestureActive, scheduleViewCommit]);
|
|
5366
5674
|
|
|
5367
5675
|
const enqueueWheelGesture = useCallback(
|
|
5368
5676
|
(gesture: PendingWheelGesture) => {
|
|
@@ -5404,8 +5712,6 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5404
5712
|
metaKey: boolean;
|
|
5405
5713
|
shiftKey: boolean;
|
|
5406
5714
|
}) => {
|
|
5407
|
-
const rect = surfaceRef.current?.getBoundingClientRect();
|
|
5408
|
-
if (!rect) return;
|
|
5409
5715
|
const delta = getWheelDeltaFromValues(
|
|
5410
5716
|
args.deltaX,
|
|
5411
5717
|
args.deltaY,
|
|
@@ -5413,6 +5719,11 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5413
5719
|
);
|
|
5414
5720
|
|
|
5415
5721
|
if (args.ctrlKey || args.metaKey) {
|
|
5722
|
+
// PERF9-WHEEL: only the zoom branch needs the surface rect (cursor
|
|
5723
|
+
// anchoring). Pan ticks skip the per-event getBoundingClientRect —
|
|
5724
|
+
// no forced style/layout read on the hot pan path.
|
|
5725
|
+
const rect = surfaceRef.current?.getBoundingClientRect();
|
|
5726
|
+
if (!rect) return;
|
|
5416
5727
|
const zoomDeltaY = clamp(
|
|
5417
5728
|
delta.y,
|
|
5418
5729
|
-MAX_WHEEL_ZOOM_DELTA,
|
|
@@ -5761,6 +6072,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5761
6072
|
// is offset relative to its OWN source geometry (not chained off the
|
|
5762
6073
|
// previous duplicate), mirroring how a multi-select alt-drag would
|
|
5763
6074
|
// offset each frame independently.
|
|
6075
|
+
let dispatched = 0;
|
|
5764
6076
|
for (const targetId of frameIds) {
|
|
5765
6077
|
const screen = screens.find((s) => s.id === targetId);
|
|
5766
6078
|
if (!screen) continue;
|
|
@@ -5778,6 +6090,17 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5778
6090
|
canvasPosition,
|
|
5779
6091
|
dropCanvasPosition: canvasPosition,
|
|
5780
6092
|
});
|
|
6093
|
+
dispatched += 1;
|
|
6094
|
+
}
|
|
6095
|
+
if (dispatched > 0) {
|
|
6096
|
+
// Cmd+D places each clone right beside its source — like the
|
|
6097
|
+
// alt-drag drop, the camera must stay still while the clones land
|
|
6098
|
+
// (see shouldSuppressLineupRecenter).
|
|
6099
|
+
lineupRecenterSuppressRef.current = {
|
|
6100
|
+
atMs: Date.now(),
|
|
6101
|
+
fromCount: screensRef.current.length,
|
|
6102
|
+
addedCount: dispatched,
|
|
6103
|
+
};
|
|
5781
6104
|
}
|
|
5782
6105
|
};
|
|
5783
6106
|
|
|
@@ -5811,6 +6134,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5811
6134
|
}, [effectiveTool]);
|
|
5812
6135
|
const penActive = effectiveTool === "pen";
|
|
5813
6136
|
const creationToolActive = Boolean(getDraftCreationTool(effectiveTool));
|
|
6137
|
+
// PERF9-WHEEL: an in-flight wheel pan/zoom also mutes iframe pointer
|
|
6138
|
+
// events, but imperatively (see markWheelGestureActive) rather than via
|
|
6139
|
+
// this flag — flipping React state here at gesture start re-rendered every
|
|
6140
|
+
// Screen and stalled the first wheel tick.
|
|
5814
6141
|
const canvasGestureActive = isDragging || isPanning;
|
|
5815
6142
|
const boardSurfaceInteractive = shouldBoardSurfaceCapturePointerEvents({
|
|
5816
6143
|
tool: effectiveTool,
|
|
@@ -6156,6 +6483,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6156
6483
|
// iframes (which have their own stacking context above this).
|
|
6157
6484
|
<div
|
|
6158
6485
|
className="[&_.design-canvas-iframe-wrapper]:shadow-none [&_.design-canvas-iframe-wrapper]:ring-0"
|
|
6486
|
+
// PERF9-WHEEL: stable hook so markWheelGestureActive can mute
|
|
6487
|
+
// this layer's pointer events imperatively during a wheel
|
|
6488
|
+
// gesture, exactly like the [data-screen-content] wrappers.
|
|
6489
|
+
data-board-surface-layer
|
|
6159
6490
|
style={getBoardSurfaceLayerStyle({
|
|
6160
6491
|
geometry: boardGeo,
|
|
6161
6492
|
interactive: boardSurfaceInteractive,
|
|
@@ -6768,6 +7099,8 @@ function DraftPrimitiveLayer({
|
|
|
6768
7099
|
chromeScale={chromeScale}
|
|
6769
7100
|
chromeSettling={chromeSettling}
|
|
6770
7101
|
rotationDeg={draft.geometry.rotation ?? 0}
|
|
7102
|
+
frameWidth={draft.geometry.width}
|
|
7103
|
+
frameHeight={draft.geometry.height}
|
|
6771
7104
|
onStartResize={(handle, event) =>
|
|
6772
7105
|
onStartResize(draft.id, handle, event)
|
|
6773
7106
|
}
|
|
@@ -8029,6 +8362,8 @@ const Screen = memo(function Screen({
|
|
|
8029
8362
|
chromeScale={chromeScale}
|
|
8030
8363
|
chromeSettling={chromeSettling}
|
|
8031
8364
|
rotationDeg={geometry.rotation ?? 0}
|
|
8365
|
+
frameWidth={geometry.width}
|
|
8366
|
+
frameHeight={geometry.height}
|
|
8032
8367
|
onStartResize={(handle, e) => onStartResize(screen.id, handle, e)}
|
|
8033
8368
|
onStartRotate={(e) => onStartRotate(screen.id, e)}
|
|
8034
8369
|
/>
|
|
@@ -8751,6 +9086,8 @@ function PassiveSelectionBox({
|
|
|
8751
9086
|
config.cursor,
|
|
8752
9087
|
chromeScale,
|
|
8753
9088
|
chromeSettling,
|
|
9089
|
+
geometry.width,
|
|
9090
|
+
geometry.height,
|
|
8754
9091
|
)}
|
|
8755
9092
|
/>
|
|
8756
9093
|
))}
|
|
@@ -8805,6 +9142,8 @@ function SelectionBox({
|
|
|
8805
9142
|
chromeScale={chromeScale}
|
|
8806
9143
|
chromeSettling={chromeSettling}
|
|
8807
9144
|
rotationDeg={geometry.rotation ?? 0}
|
|
9145
|
+
frameWidth={geometry.width}
|
|
9146
|
+
frameHeight={geometry.height}
|
|
8808
9147
|
onStartResize={onStartResize}
|
|
8809
9148
|
onStartRotate={onStartRotate}
|
|
8810
9149
|
/>
|
|
@@ -8820,6 +9159,8 @@ function ResizeHandles({
|
|
|
8820
9159
|
chromeScale = 1,
|
|
8821
9160
|
chromeSettling = false,
|
|
8822
9161
|
rotationDeg = 0,
|
|
9162
|
+
frameWidth = Number.POSITIVE_INFINITY,
|
|
9163
|
+
frameHeight = Number.POSITIVE_INFINITY,
|
|
8823
9164
|
onStartResize,
|
|
8824
9165
|
onStartRotate,
|
|
8825
9166
|
}: {
|
|
@@ -8833,6 +9174,11 @@ function ResizeHandles({
|
|
|
8833
9174
|
* visual direction instead of a static unrotated cursor (CSS `cursor` is
|
|
8834
9175
|
* never itself rotated by a transform on the element). */
|
|
8835
9176
|
rotationDeg?: number;
|
|
9177
|
+
/** Frame dimensions in local (board) px, used to clamp how far handle hit
|
|
9178
|
+
* zones may reach into the frame body (handle-hit-zones.ts). Omit for the
|
|
9179
|
+
* unclamped historical behavior. */
|
|
9180
|
+
frameWidth?: number;
|
|
9181
|
+
frameHeight?: number;
|
|
8836
9182
|
onStartResize: (handle: ResizeHandle, e: React.MouseEvent) => void;
|
|
8837
9183
|
onStartRotate: (e: React.MouseEvent) => void;
|
|
8838
9184
|
}) {
|
|
@@ -8863,6 +9209,8 @@ function ResizeHandles({
|
|
|
8863
9209
|
getResizeCursorForHandle(config.handle, rotationDeg),
|
|
8864
9210
|
chromeScale,
|
|
8865
9211
|
chromeSettling,
|
|
9212
|
+
frameWidth,
|
|
9213
|
+
frameHeight,
|
|
8866
9214
|
)}
|
|
8867
9215
|
onMouseDown={(e) => onStartResize(config.handle, e)}
|
|
8868
9216
|
/>
|
|
@@ -8877,6 +9225,8 @@ function ResizeHandles({
|
|
|
8877
9225
|
getResizeCursorForHandle(config.handle, rotationDeg),
|
|
8878
9226
|
chromeScale,
|
|
8879
9227
|
chromeSettling,
|
|
9228
|
+
frameWidth,
|
|
9229
|
+
frameHeight,
|
|
8880
9230
|
)}
|
|
8881
9231
|
onMouseDown={(e) => onStartResize(config.handle, e)}
|
|
8882
9232
|
/>
|
|
@@ -8939,31 +9289,38 @@ const ROTATE_HANDLE_CONFIGS: Array<{
|
|
|
8939
9289
|
corner: string;
|
|
8940
9290
|
}> = [{ corner: "nw" }, { corner: "ne" }, { corner: "se" }, { corner: "sw" }];
|
|
8941
9291
|
|
|
9292
|
+
// Edge/corner handle hit zones are clamped against the frame's own dimensions
|
|
9293
|
+
// (handle-hit-zones.ts) so that at low zoom the chromeScale-compensated hit
|
|
9294
|
+
// slop can never cover the whole frame body and steal every press as a
|
|
9295
|
+
// resize — the frame's central 50% band stays grabbable for a move drag at
|
|
9296
|
+
// any zoom. On large frames these produce the historical geometry exactly.
|
|
8942
9297
|
function edgeHandleStyle(
|
|
8943
9298
|
handle: ResizeHandle,
|
|
8944
9299
|
cursor: string,
|
|
8945
9300
|
chromeScale: number,
|
|
8946
9301
|
chromeSettling: boolean,
|
|
9302
|
+
frameWidth: number,
|
|
9303
|
+
frameHeight: number,
|
|
8947
9304
|
): CSSProperties {
|
|
8948
|
-
const size = 14 * chromeScale;
|
|
8949
|
-
const offset = -size / 2;
|
|
8950
9305
|
if (handle === "n" || handle === "s") {
|
|
9306
|
+
const geometry = getEdgeHandleHitGeometry(chromeScale, frameHeight);
|
|
8951
9307
|
return {
|
|
8952
9308
|
cursor,
|
|
8953
9309
|
transition: getChromeHandleTransition(chromeSettling),
|
|
8954
9310
|
left: 0,
|
|
8955
9311
|
right: 0,
|
|
8956
|
-
height:
|
|
8957
|
-
[handle === "n" ? "top" : "bottom"]:
|
|
9312
|
+
height: geometry.thickness,
|
|
9313
|
+
[handle === "n" ? "top" : "bottom"]: geometry.outwardOffset,
|
|
8958
9314
|
};
|
|
8959
9315
|
}
|
|
9316
|
+
const geometry = getEdgeHandleHitGeometry(chromeScale, frameWidth);
|
|
8960
9317
|
return {
|
|
8961
9318
|
cursor,
|
|
8962
9319
|
transition: getChromeHandleTransition(chromeSettling),
|
|
8963
9320
|
top: 0,
|
|
8964
9321
|
bottom: 0,
|
|
8965
|
-
width:
|
|
8966
|
-
[handle === "w" ? "left" : "right"]:
|
|
9322
|
+
width: geometry.thickness,
|
|
9323
|
+
[handle === "w" ? "left" : "right"]: geometry.outwardOffset,
|
|
8967
9324
|
};
|
|
8968
9325
|
}
|
|
8969
9326
|
|
|
@@ -8972,17 +9329,22 @@ function cornerHandleStyle(
|
|
|
8972
9329
|
cursor: string,
|
|
8973
9330
|
chromeScale: number,
|
|
8974
9331
|
chromeSettling: boolean,
|
|
9332
|
+
frameWidth: number,
|
|
9333
|
+
frameHeight: number,
|
|
8975
9334
|
): CSSProperties {
|
|
8976
|
-
const size
|
|
8977
|
-
|
|
9335
|
+
const { size, offsetX, offsetY } = getCornerHandleGeometry(
|
|
9336
|
+
chromeScale,
|
|
9337
|
+
frameWidth,
|
|
9338
|
+
frameHeight,
|
|
9339
|
+
);
|
|
8978
9340
|
return {
|
|
8979
9341
|
cursor,
|
|
8980
9342
|
transition: getChromeHandleTransition(chromeSettling),
|
|
8981
9343
|
width: size,
|
|
8982
9344
|
height: size,
|
|
8983
9345
|
borderWidth: Math.max(1, 1.25 * chromeScale),
|
|
8984
|
-
...(handle.includes("n") ? { top:
|
|
8985
|
-
...(handle.includes("w") ? { left:
|
|
9346
|
+
...(handle.includes("n") ? { top: offsetY } : { bottom: offsetY }),
|
|
9347
|
+
...(handle.includes("w") ? { left: offsetX } : { right: offsetX }),
|
|
8986
9348
|
};
|
|
8987
9349
|
}
|
|
8988
9350
|
|