@agent-native/core 0.148.0 → 0.149.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/corpus/README.md +1 -1
- package/corpus/templates/analytics/changelog/2026-08-10-wide-tables-in-analytics-chat-now-scroll-horizontally-within.md +6 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +13 -53
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -70
- package/corpus/templates/calendar/app/lib/event-layout.ts +122 -0
- package/corpus/templates/calendar/app/routes/_app.availability.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-08-10-opening-availability-settings-from-the-agent-lands-on-the-av.md +6 -0
- package/corpus/templates/calendar/vitest.config.ts +6 -1
- package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +3 -1
- package/corpus/templates/clips/app/components/player/video-player.tsx +44 -4
- package/corpus/templates/clips/changelog/2026-08-10-fixed-transcription-failures-for-recordings-without-captured.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-10-mobile-clips-can-now-enter-fullscreen-on-iphone-safari.md +6 -0
- package/corpus/templates/content/actions/_property-utils.ts +11 -1
- package/corpus/templates/content/actions/view-screen.ts +62 -12
- package/corpus/templates/content/server/plugins/agent-chat.ts +2 -0
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +22 -0
- package/corpus/templates/design/AGENTS.md +3 -0
- package/corpus/templates/design/actions/create-design-from-template.ts +19 -0
- package/corpus/templates/design/actions/get-design-snapshot.ts +18 -0
- package/corpus/templates/design/actions/get-design-template.ts +178 -0
- package/corpus/templates/design/actions/list-designs.ts +3 -1
- package/corpus/templates/design/actions/view-screen.ts +31 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +225 -214
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
- package/corpus/templates/design/app/i18n-data.ts +78 -0
- package/corpus/templates/design/app/lib/design-authors.ts +64 -0
- package/corpus/templates/design/app/pages/Index.tsx +114 -4
- package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +1 -0
- package/corpus/templates/design/changelog/2026-08-07-alt-dragging-an-element-to-copy-it-now-keeps-the-copy-visibl.md +6 -0
- package/corpus/templates/design/changelog/2026-08-07-design-cards-now-show-who-created-each-design-with-a-filter-.md +6 -0
- package/corpus/templates/design/changelog/2026-08-07-drawing-a-shape-on-the-overview-canvas-now-keeps-the-shape-s.md +6 -0
- package/corpus/templates/design/changelog/2026-08-07-hold-option-alt-while-dragging-a-frame-on-the-overview-canva.md +6 -0
- package/corpus/templates/design/server/lib/design-template-data.ts +143 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/forms/AGENTS.md +2 -2
- package/corpus/templates/forms/actions/create-form.ts +1 -1
- package/corpus/templates/forms/actions/update-form.ts +5 -5
- package/corpus/templates/forms/changelog/2026-08-10-forms-now-keep-agent-applied-field-changes-visible-after-an-.md +6 -0
- package/corpus/templates/forms/changelog/2026-08-10-published-form-links-always-use-the-working-public-form-url.md +6 -0
- package/corpus/templates/forms/server/plugins/agent-chat.ts +2 -2
- package/corpus/templates/slides/AGENTS.md +4 -3
- package/corpus/templates/slides/actions/import-google-slides-reference.ts +56 -10
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +12 -74
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +5 -1
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +15 -4
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +116 -55
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -0
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +143 -18
- package/corpus/templates/slides/app/i18n/en-US.ts +6 -0
- package/corpus/templates/slides/app/lib/generation-state.ts +7 -1
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +32 -14
- package/corpus/templates/slides/app/pages/Index.tsx +110 -128
- package/corpus/templates/slides/changelog/2026-08-10-imported-reference-decks-now-show-clear-success-and-stay-sel.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-pasted-google-slides-links-import-after-reconnecting-google-.md +6 -0
- package/corpus/templates/slides/server/handlers/google-docs-auth.ts +4 -0
- package/corpus/templates/slides/server/lib/google-docs-oauth.ts +13 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/dist/agent/engine/registry.d.ts +8 -2
- package/dist/agent/engine/registry.js +63 -29
- package/dist/agent/production-agent.js +7 -0
- package/dist/agent/run-manager.d.ts +5 -4
- package/dist/agent/run-manager.js +6 -5
- package/dist/cli/claude-code-participant.d.ts +2 -1
- package/dist/cli/claude-code-participant.js +19 -0
- package/dist/cli/code-agent-executor.js +223 -0
- package/dist/cli/create-workspace.js +3 -3
- package/dist/cli/create.d.ts +3 -2
- package/dist/cli/create.js +20 -24
- package/dist/client/AgentPanel.js +8 -4
- package/dist/client/AssistantChat.js +34 -1
- package/dist/client/agent-chat-adapter.d.ts +1 -0
- package/dist/client/agent-chat-adapter.js +10 -2
- package/dist/client/blocks/library/diagram.js +1 -1
- package/dist/client/blocks/library/sanitize-html.js +66 -2
- package/dist/client/blocks/library/wireframe-kit.js +69 -0
- package/dist/client/chat/markdown-renderer.d.ts +1 -0
- package/dist/client/chat/markdown-renderer.js +4 -0
- package/dist/client/chat/message-components.d.ts +1 -0
- package/dist/client/chat/message-components.js +9 -1
- package/dist/client/chat/tool-call-display.d.ts +8 -0
- package/dist/client/chat/tool-call-display.js +30 -21
- package/dist/client/chat-first/app-pane.js +1 -1
- package/dist/client/chat-first/apps-rail.d.ts +1 -1
- package/dist/client/chat-first/apps-rail.js +12 -12
- package/dist/client/chat-first/chat-history.d.ts +1 -1
- package/dist/client/chat-first/chat-history.js +2 -0
- package/dist/client/chat-first/copy.js +1 -0
- package/dist/client/chat-first/primary-nav.js +1 -1
- package/dist/client/chat-first/types.d.ts +1 -0
- package/dist/client/resources/McpConnectionSuggestion.js +2 -2
- package/dist/client/settings/SettingsTabsPage.d.ts +3 -1
- package/dist/client/settings/SettingsTabsPage.js +2 -2
- package/dist/client/sse-event-processor.d.ts +8 -0
- package/dist/client/sse-event-processor.js +13 -6
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +5 -7
- package/dist/deploy/build.js +19 -12
- package/dist/localization/default-messages.js +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/provider-api.d.ts +2 -2
- package/dist/scripts/call-agent.js +3 -3
- package/dist/scripts/describe-workspace-apps.js +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-capabilities.js +0 -0
- package/dist/server/agent-chat/context-tools.js +1 -1
- package/dist/server/agent-chat/prompt-resources.js +1 -1
- package/dist/server/agent-chat-plugin.js +1 -1
- package/dist/server/auth.js +190 -42
- package/dist/server/better-auth-migrations.js +53 -0
- package/dist/server/http-response-telemetry.js +7 -1
- package/dist/server/onboarding-html.js +119 -33
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/password-policy.d.ts +2 -1
- package/dist/shared/password-policy.js +2 -1
- package/dist/styles/agent-conversation.css +20 -3
- package/dist/styles/agent-native.css +14 -2
- package/dist/styles/blocks.css +132 -0
- package/dist/templates/default/app/i18n/ar-SA.ts +1 -1
- package/dist/templates/default/app/i18n/de-DE.ts +1 -1
- package/dist/templates/default/app/i18n/en-US.ts +1 -1
- package/dist/templates/default/app/i18n/es-ES.ts +1 -1
- package/dist/templates/default/app/i18n/fr-FR.ts +1 -1
- package/dist/templates/default/app/i18n/hi-IN.ts +1 -1
- package/dist/templates/default/app/i18n/ja-JP.ts +1 -1
- package/dist/templates/default/app/i18n/ko-KR.ts +1 -1
- package/dist/templates/default/app/i18n/pt-BR.ts +1 -1
- package/dist/templates/default/app/i18n/zh-CN.ts +1 -1
- package/dist/templates/default/app/i18n/zh-TW.ts +1 -1
- package/dist/templates/default/pnpm-workspace.yaml +1 -0
- package/dist/templates/workspace-root/pnpm-workspace.yaml +2 -0
- package/docs/content/actions.mdx +4 -1
- package/docs/content/agent-surfaces.mdx +8 -6
- package/docs/content/automations.mdx +1 -1
- package/docs/content/cloneable-saas.mdx +6 -6
- package/docs/content/database.mdx +6 -1
- package/docs/content/deployment.mdx +10 -8
- package/docs/content/docs-components.mdx +22 -14
- package/docs/content/drop-in-agent.mdx +1 -1
- package/docs/content/environment-variables.mdx +4 -3
- package/docs/content/getting-started-actions.mdx +5 -4
- package/docs/content/getting-started-pages.mdx +7 -6
- package/docs/content/getting-started.mdx +4 -3
- package/docs/content/notifications.mdx +6 -6
- package/docs/content/progress.mdx +5 -5
- package/docs/content/routing.mdx +3 -1
- package/docs/content/server-database.mdx +13 -4
- package/docs/content/server-overview.mdx +9 -9
- package/docs/content/server-plugins.mdx +10 -4
- package/docs/content/server-routes.mdx +7 -2
- package/docs/content/tracking.mdx +4 -4
- package/docs/content/what-is-agent-native.mdx +4 -4
- package/package.json +2 -2
- package/src/templates/default/app/i18n/ar-SA.ts +1 -1
- package/src/templates/default/app/i18n/de-DE.ts +1 -1
- package/src/templates/default/app/i18n/en-US.ts +1 -1
- package/src/templates/default/app/i18n/es-ES.ts +1 -1
- package/src/templates/default/app/i18n/fr-FR.ts +1 -1
- package/src/templates/default/app/i18n/hi-IN.ts +1 -1
- package/src/templates/default/app/i18n/ja-JP.ts +1 -1
- package/src/templates/default/app/i18n/ko-KR.ts +1 -1
- package/src/templates/default/app/i18n/pt-BR.ts +1 -1
- package/src/templates/default/app/i18n/zh-CN.ts +1 -1
- package/src/templates/default/app/i18n/zh-TW.ts +1 -1
- package/src/templates/default/pnpm-workspace.yaml +1 -0
- package/src/templates/workspace-root/pnpm-workspace.yaml +2 -0
|
@@ -5074,10 +5074,210 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5074
5074
|
[updateSelectedDraftIds, updateSelectedIds],
|
|
5075
5075
|
);
|
|
5076
5076
|
|
|
5077
|
+
const beginDuplicateGesture = useCallback(
|
|
5078
|
+
(id: string, e: React.MouseEvent) => {
|
|
5079
|
+
const entries = getCurrentFrameEntries();
|
|
5080
|
+
const selection = selectedIdsRef.current;
|
|
5081
|
+
const targets = (selection.includes(id) ? selection : [id])
|
|
5082
|
+
.filter((targetId) => !lockedScreenIdSet.has(targetId))
|
|
5083
|
+
.flatMap((targetId) => {
|
|
5084
|
+
const target = screensRef.current.find((s) => s.id === targetId);
|
|
5085
|
+
const geometry = entries.find(
|
|
5086
|
+
(entry) => entry.id === targetId,
|
|
5087
|
+
)?.geometry;
|
|
5088
|
+
return target && geometry ? [{ screen: target, geometry }] : [];
|
|
5089
|
+
});
|
|
5090
|
+
const source = targets.find((target) => target.screen.id === id);
|
|
5091
|
+
if (!source) return;
|
|
5092
|
+
e.preventDefault();
|
|
5093
|
+
e.stopPropagation();
|
|
5094
|
+
duplicateCleanup.current?.();
|
|
5095
|
+
|
|
5096
|
+
const display = screenDisplayName(
|
|
5097
|
+
source.screen,
|
|
5098
|
+
getResolvedMetadata(source.screen),
|
|
5099
|
+
);
|
|
5100
|
+
const surfaceRect = surfaceRef.current?.getBoundingClientRect();
|
|
5101
|
+
const origin = { x: e.clientX, y: e.clientY };
|
|
5102
|
+
const originCanvas = canvasPointFromClient(e.clientX, e.clientY);
|
|
5103
|
+
const previewPoint = {
|
|
5104
|
+
x: surfaceRect ? e.clientX - surfaceRect.left + 16 : e.clientX,
|
|
5105
|
+
y: surfaceRect ? e.clientY - surfaceRect.top + 16 : e.clientY,
|
|
5106
|
+
};
|
|
5107
|
+
|
|
5108
|
+
const previewWidth = source.geometry.width;
|
|
5109
|
+
const previewHeight = source.geometry.height;
|
|
5110
|
+
|
|
5111
|
+
setDuplicatePreview({
|
|
5112
|
+
display,
|
|
5113
|
+
count: targets.length,
|
|
5114
|
+
x: previewPoint.x,
|
|
5115
|
+
y: previewPoint.y,
|
|
5116
|
+
width: previewWidth,
|
|
5117
|
+
height: previewHeight,
|
|
5118
|
+
canDuplicate: !!onDuplicate,
|
|
5119
|
+
moved: false,
|
|
5120
|
+
});
|
|
5121
|
+
// Mount the interaction shield and mute preview-iframe pointer events for
|
|
5122
|
+
// the duration of the gesture, same as every other drag — otherwise the
|
|
5123
|
+
// pointer freezes crossing a live embedded iframe and a release over a
|
|
5124
|
+
// screen never reaches handleMouseUp.
|
|
5125
|
+
setIsDragging(true);
|
|
5126
|
+
// Figma parity: show a copy-affordance cursor while the alt-drag
|
|
5127
|
+
// duplicate gesture is armed. Tracked live in handleMouseMove below
|
|
5128
|
+
// (same live e.altKey tracking that already drives canDuplicate), so
|
|
5129
|
+
// releasing alt mid-drag falls back to the default arrow instead of
|
|
5130
|
+
// staying on the copy cursor for a gesture that will no longer
|
|
5131
|
+
// duplicate on mouseup.
|
|
5132
|
+
setDragCursor(e.altKey ? "copy" : null);
|
|
5133
|
+
|
|
5134
|
+
// PERF: track the last committed canDuplicate/moved/cursor values in
|
|
5135
|
+
// plain closure locals (not state) so the mousemove tick below can tell
|
|
5136
|
+
// whether anything conditional actually changed without reading back
|
|
5137
|
+
// through React state (which the imperative writes below intentionally
|
|
5138
|
+
// stop keeping fresh every tick — see duplicatePreviewElRef).
|
|
5139
|
+
let lastCanDuplicate = !!onDuplicate;
|
|
5140
|
+
let lastMoved = false;
|
|
5141
|
+
let lastCursor: string | null = e.altKey ? "copy" : null;
|
|
5142
|
+
|
|
5143
|
+
const handleMouseMove = (ev: MouseEvent) => {
|
|
5144
|
+
const dx = ev.clientX - origin.x;
|
|
5145
|
+
const dy = ev.clientY - origin.y;
|
|
5146
|
+
const moved = Math.hypot(dx, dy) >= DUPLICATE_DRAG_THRESHOLD;
|
|
5147
|
+
const rect = surfaceRef.current?.getBoundingClientRect();
|
|
5148
|
+
const x = rect ? ev.clientX - rect.left + 16 : ev.clientX;
|
|
5149
|
+
const y = rect ? ev.clientY - rect.top + 16 : ev.clientY;
|
|
5150
|
+
// Live alt state, not just capability: if the user releases alt
|
|
5151
|
+
// mid-drag the preview should visibly fall back to its "not armed"
|
|
5152
|
+
// dashed/preview styling, matching that mouseup will then cancel
|
|
5153
|
+
// the duplicate instead of creating one (see handleMouseUp below).
|
|
5154
|
+
const canDuplicate = !!onDuplicate && ev.altKey;
|
|
5155
|
+
|
|
5156
|
+
// PERF9: the ghost's position is a direct DOM write every tick
|
|
5157
|
+
// instead of a setDuplicatePreview (full re-render) — same
|
|
5158
|
+
// "imperative now, commit state only when something conditional
|
|
5159
|
+
// changes" discipline as the frame/draft drag paths above. Falls
|
|
5160
|
+
// back to setDuplicatePreview itself if the node hasn't mounted yet
|
|
5161
|
+
// (e.g. the very first tick right after mousedown, before React has
|
|
5162
|
+
// committed the initial preview).
|
|
5163
|
+
const el = duplicatePreviewElRef.current;
|
|
5164
|
+
if (el) {
|
|
5165
|
+
el.style.left = `${x}px`;
|
|
5166
|
+
el.style.top = `${y}px`;
|
|
5167
|
+
}
|
|
5168
|
+
|
|
5169
|
+
if (!el || canDuplicate !== lastCanDuplicate || moved !== lastMoved) {
|
|
5170
|
+
lastCanDuplicate = canDuplicate;
|
|
5171
|
+
lastMoved = moved;
|
|
5172
|
+
setDuplicatePreview({
|
|
5173
|
+
display,
|
|
5174
|
+
count: targets.length,
|
|
5175
|
+
x,
|
|
5176
|
+
y,
|
|
5177
|
+
width: previewWidth,
|
|
5178
|
+
height: previewHeight,
|
|
5179
|
+
canDuplicate,
|
|
5180
|
+
moved,
|
|
5181
|
+
});
|
|
5182
|
+
}
|
|
5183
|
+
|
|
5184
|
+
const cursor = ev.altKey ? "copy" : null;
|
|
5185
|
+
if (cursor !== lastCursor) {
|
|
5186
|
+
lastCursor = cursor;
|
|
5187
|
+
setDragCursor(cursor);
|
|
5188
|
+
}
|
|
5189
|
+
};
|
|
5190
|
+
|
|
5191
|
+
const cleanupDuplicateGesture = () => {
|
|
5192
|
+
setDuplicatePreview(null);
|
|
5193
|
+
duplicateCleanup.current = null;
|
|
5194
|
+
// finishDrag clears isDragging, unmounts the shield, and — critically —
|
|
5195
|
+
// runs dragCleanup.current() to detach the window listeners installed
|
|
5196
|
+
// by installDragListeners and restore preview-iframe pointer events.
|
|
5197
|
+
// dragState.current was never set for this gesture, so finishDrag's
|
|
5198
|
+
// other resets (marquee/creation-preview/etc.) are no-ops here.
|
|
5199
|
+
finishDrag();
|
|
5200
|
+
};
|
|
5201
|
+
|
|
5202
|
+
const handleMouseUp = (ev: MouseEvent) => {
|
|
5203
|
+
const moved =
|
|
5204
|
+
Math.hypot(ev.clientX - origin.x, ev.clientY - origin.y) >=
|
|
5205
|
+
DUPLICATE_DRAG_THRESHOLD;
|
|
5206
|
+
// Alt is read live, not from mousedown: the gesture only ever showed a
|
|
5207
|
+
// ghost, so releasing alt must cancel outright, never fall back to a
|
|
5208
|
+
// move the original frame never started.
|
|
5209
|
+
const shouldDuplicate = moved && ev.altKey;
|
|
5210
|
+
|
|
5211
|
+
if (onDuplicate && shouldDuplicate) {
|
|
5212
|
+
const dropCanvasPosition = canvasPointFromClient(
|
|
5213
|
+
ev.clientX,
|
|
5214
|
+
ev.clientY,
|
|
5215
|
+
);
|
|
5216
|
+
const delta = {
|
|
5217
|
+
x: dropCanvasPosition.x - originCanvas.x,
|
|
5218
|
+
y: dropCanvasPosition.y - originCanvas.y,
|
|
5219
|
+
};
|
|
5220
|
+
// The user placed these clones deliberately — suppress the
|
|
5221
|
+
// lineup-recenter camera move when the created screens land in
|
|
5222
|
+
// `screens` (Figma keeps the camera still on alt-drag duplicate).
|
|
5223
|
+
lineupRecenterSuppressRef.current = {
|
|
5224
|
+
atMs: Date.now(),
|
|
5225
|
+
fromCount: screensRef.current.length,
|
|
5226
|
+
addedCount: targets.length,
|
|
5227
|
+
};
|
|
5228
|
+
targets.forEach((target) => {
|
|
5229
|
+
const canvasPosition = {
|
|
5230
|
+
x: target.geometry.x + delta.x,
|
|
5231
|
+
y: target.geometry.y + delta.y,
|
|
5232
|
+
};
|
|
5233
|
+
onDuplicate(target.screen.id, {
|
|
5234
|
+
mode: "alt-drag",
|
|
5235
|
+
screen: target.screen,
|
|
5236
|
+
canvasPosition,
|
|
5237
|
+
canvasOffset: {
|
|
5238
|
+
x: dropCanvasPosition.x - canvasPosition.x,
|
|
5239
|
+
y: dropCanvasPosition.y - canvasPosition.y,
|
|
5240
|
+
},
|
|
5241
|
+
dropCanvasPosition,
|
|
5242
|
+
});
|
|
5243
|
+
});
|
|
5244
|
+
} else if (!moved) {
|
|
5245
|
+
onPick(id);
|
|
5246
|
+
}
|
|
5247
|
+
|
|
5248
|
+
cleanupDuplicateGesture();
|
|
5249
|
+
};
|
|
5250
|
+
|
|
5251
|
+
duplicateCleanup.current = cleanupDuplicateGesture;
|
|
5252
|
+
installDragListeners(
|
|
5253
|
+
handleMouseMove,
|
|
5254
|
+
handleMouseUp,
|
|
5255
|
+
cleanupDuplicateGesture,
|
|
5256
|
+
);
|
|
5257
|
+
},
|
|
5258
|
+
[
|
|
5259
|
+
canvasPointFromClient,
|
|
5260
|
+
finishDrag,
|
|
5261
|
+
getCurrentFrameEntries,
|
|
5262
|
+
getResolvedMetadata,
|
|
5263
|
+
installDragListeners,
|
|
5264
|
+
lockedScreenIdSet,
|
|
5265
|
+
onDuplicate,
|
|
5266
|
+
onPick,
|
|
5267
|
+
],
|
|
5268
|
+
);
|
|
5269
|
+
|
|
5077
5270
|
const beginFrameDrag = useCallback(
|
|
5078
5271
|
(id: string, e: React.MouseEvent) => {
|
|
5079
5272
|
if (readOnly) return;
|
|
5080
|
-
if (e.button !== 0 ||
|
|
5273
|
+
if (e.button !== 0 || lockedScreenIdSet.has(id)) return;
|
|
5274
|
+
// Frame body, label row, and the selection box over a selected frame all
|
|
5275
|
+
// start drags here, so alt's move-vs-copy meaning is decided exactly once.
|
|
5276
|
+
if (e.altKey) {
|
|
5277
|
+
beginDuplicateGesture(id, e);
|
|
5278
|
+
return;
|
|
5279
|
+
}
|
|
5280
|
+
if (e.shiftKey) return;
|
|
5081
5281
|
e.preventDefault();
|
|
5082
5282
|
e.stopPropagation();
|
|
5083
5283
|
// Frame mousedowns stop propagation, so they never reach handleMouseDown.
|
|
@@ -5429,6 +5629,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5429
5629
|
},
|
|
5430
5630
|
[
|
|
5431
5631
|
activeId,
|
|
5632
|
+
beginDuplicateGesture,
|
|
5432
5633
|
findPrimitiveDropTarget,
|
|
5433
5634
|
finishDrag,
|
|
5434
5635
|
getCanvasPoint,
|
|
@@ -6284,191 +6485,6 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6284
6485
|
],
|
|
6285
6486
|
);
|
|
6286
6487
|
|
|
6287
|
-
const beginDuplicateGesture = useCallback(
|
|
6288
|
-
(screen: ScreenFile, display: string, e: React.MouseEvent<HTMLElement>) => {
|
|
6289
|
-
if (readOnly) return;
|
|
6290
|
-
if (e.button !== 0 || !e.altKey || lockedScreenIdSet.has(screen.id)) {
|
|
6291
|
-
return;
|
|
6292
|
-
}
|
|
6293
|
-
e.preventDefault();
|
|
6294
|
-
e.stopPropagation();
|
|
6295
|
-
duplicateCleanup.current?.();
|
|
6296
|
-
|
|
6297
|
-
const surfaceRect = surfaceRef.current?.getBoundingClientRect();
|
|
6298
|
-
const origin = { x: e.clientX, y: e.clientY };
|
|
6299
|
-
const originCanvas = canvasPointFromClient(e.clientX, e.clientY);
|
|
6300
|
-
const sourceFrame = getCurrentFrameEntries().find(
|
|
6301
|
-
(entry) => entry.id === screen.id,
|
|
6302
|
-
);
|
|
6303
|
-
const pointerOffset = sourceFrame
|
|
6304
|
-
? {
|
|
6305
|
-
x: originCanvas.x - sourceFrame.geometry.x,
|
|
6306
|
-
y: originCanvas.y - sourceFrame.geometry.y,
|
|
6307
|
-
}
|
|
6308
|
-
: { x: 0, y: 0 };
|
|
6309
|
-
const previewPoint = {
|
|
6310
|
-
x: surfaceRect ? e.clientX - surfaceRect.left + 16 : e.clientX,
|
|
6311
|
-
y: surfaceRect ? e.clientY - surfaceRect.top + 16 : e.clientY,
|
|
6312
|
-
};
|
|
6313
|
-
|
|
6314
|
-
const previewWidth = sourceFrame?.geometry.width ?? SCREEN_WIDTH;
|
|
6315
|
-
const previewHeight = sourceFrame?.geometry.height ?? SCREEN_HEIGHT;
|
|
6316
|
-
|
|
6317
|
-
setDuplicatePreview({
|
|
6318
|
-
display,
|
|
6319
|
-
x: previewPoint.x,
|
|
6320
|
-
y: previewPoint.y,
|
|
6321
|
-
width: previewWidth,
|
|
6322
|
-
height: previewHeight,
|
|
6323
|
-
canDuplicate: !!onDuplicate,
|
|
6324
|
-
moved: false,
|
|
6325
|
-
});
|
|
6326
|
-
// Mount the interaction shield and mute preview-iframe pointer events for
|
|
6327
|
-
// the duration of the gesture, same as every other drag — otherwise the
|
|
6328
|
-
// pointer freezes crossing a live embedded iframe and a release over a
|
|
6329
|
-
// screen never reaches handleMouseUp.
|
|
6330
|
-
setIsDragging(true);
|
|
6331
|
-
// Figma parity: show a copy-affordance cursor while the alt-drag
|
|
6332
|
-
// duplicate gesture is armed. Tracked live in handleMouseMove below
|
|
6333
|
-
// (same live e.altKey tracking that already drives canDuplicate), so
|
|
6334
|
-
// releasing alt mid-drag falls back to the default arrow instead of
|
|
6335
|
-
// staying on the copy cursor for a gesture that will no longer
|
|
6336
|
-
// duplicate on mouseup.
|
|
6337
|
-
setDragCursor(e.altKey ? "copy" : null);
|
|
6338
|
-
|
|
6339
|
-
// PERF: track the last committed canDuplicate/moved/cursor values in
|
|
6340
|
-
// plain closure locals (not state) so the mousemove tick below can tell
|
|
6341
|
-
// whether anything conditional actually changed without reading back
|
|
6342
|
-
// through React state (which the imperative writes below intentionally
|
|
6343
|
-
// stop keeping fresh every tick — see duplicatePreviewElRef).
|
|
6344
|
-
let lastCanDuplicate = !!onDuplicate;
|
|
6345
|
-
let lastMoved = false;
|
|
6346
|
-
let lastCursor: string | null = e.altKey ? "copy" : null;
|
|
6347
|
-
|
|
6348
|
-
const handleMouseMove = (ev: MouseEvent) => {
|
|
6349
|
-
const dx = ev.clientX - origin.x;
|
|
6350
|
-
const dy = ev.clientY - origin.y;
|
|
6351
|
-
const moved = Math.hypot(dx, dy) >= DUPLICATE_DRAG_THRESHOLD;
|
|
6352
|
-
const rect = surfaceRef.current?.getBoundingClientRect();
|
|
6353
|
-
const x = rect ? ev.clientX - rect.left + 16 : ev.clientX;
|
|
6354
|
-
const y = rect ? ev.clientY - rect.top + 16 : ev.clientY;
|
|
6355
|
-
// Live alt state, not just capability: if the user releases alt
|
|
6356
|
-
// mid-drag the preview should visibly fall back to its "not armed"
|
|
6357
|
-
// dashed/preview styling, matching that mouseup will then cancel
|
|
6358
|
-
// the duplicate instead of creating one (see handleMouseUp below).
|
|
6359
|
-
const canDuplicate = !!onDuplicate && ev.altKey;
|
|
6360
|
-
|
|
6361
|
-
// PERF9: the ghost's position is a direct DOM write every tick
|
|
6362
|
-
// instead of a setDuplicatePreview (full re-render) — same
|
|
6363
|
-
// "imperative now, commit state only when something conditional
|
|
6364
|
-
// changes" discipline as the frame/draft drag paths above. Falls
|
|
6365
|
-
// back to setDuplicatePreview itself if the node hasn't mounted yet
|
|
6366
|
-
// (e.g. the very first tick right after mousedown, before React has
|
|
6367
|
-
// committed the initial preview).
|
|
6368
|
-
const el = duplicatePreviewElRef.current;
|
|
6369
|
-
if (el) {
|
|
6370
|
-
el.style.left = `${x}px`;
|
|
6371
|
-
el.style.top = `${y}px`;
|
|
6372
|
-
}
|
|
6373
|
-
|
|
6374
|
-
if (!el || canDuplicate !== lastCanDuplicate || moved !== lastMoved) {
|
|
6375
|
-
lastCanDuplicate = canDuplicate;
|
|
6376
|
-
lastMoved = moved;
|
|
6377
|
-
setDuplicatePreview({
|
|
6378
|
-
display,
|
|
6379
|
-
x,
|
|
6380
|
-
y,
|
|
6381
|
-
width: previewWidth,
|
|
6382
|
-
height: previewHeight,
|
|
6383
|
-
canDuplicate,
|
|
6384
|
-
moved,
|
|
6385
|
-
});
|
|
6386
|
-
}
|
|
6387
|
-
|
|
6388
|
-
const cursor = ev.altKey ? "copy" : null;
|
|
6389
|
-
if (cursor !== lastCursor) {
|
|
6390
|
-
lastCursor = cursor;
|
|
6391
|
-
setDragCursor(cursor);
|
|
6392
|
-
}
|
|
6393
|
-
};
|
|
6394
|
-
|
|
6395
|
-
const cleanupDuplicateGesture = () => {
|
|
6396
|
-
setDuplicatePreview(null);
|
|
6397
|
-
duplicateCleanup.current = null;
|
|
6398
|
-
// finishDrag clears isDragging, unmounts the shield, and — critically —
|
|
6399
|
-
// runs dragCleanup.current() to detach the window listeners installed
|
|
6400
|
-
// by installDragListeners and restore preview-iframe pointer events.
|
|
6401
|
-
// dragState.current was never set for this gesture, so finishDrag's
|
|
6402
|
-
// other resets (marquee/creation-preview/etc.) are no-ops here.
|
|
6403
|
-
finishDrag();
|
|
6404
|
-
};
|
|
6405
|
-
|
|
6406
|
-
const handleMouseUp = (ev: MouseEvent) => {
|
|
6407
|
-
const moved =
|
|
6408
|
-
Math.hypot(ev.clientX - origin.x, ev.clientY - origin.y) >=
|
|
6409
|
-
DUPLICATE_DRAG_THRESHOLD;
|
|
6410
|
-
const mode = moved ? "alt-drag" : "alt-click";
|
|
6411
|
-
// Figma semantics: a plain alt-click (no drag) never duplicates —
|
|
6412
|
-
// only an actual alt-drag does. And alt is evaluated live: releasing
|
|
6413
|
-
// it before mouseup cancels the pending duplicate rather than
|
|
6414
|
-
// creating one anyway (this gesture never moves the original frame
|
|
6415
|
-
// during the drag — it only shows a floating ghost preview — so
|
|
6416
|
-
// "cancel" here means no-op, not handing off to a live move).
|
|
6417
|
-
const shouldDuplicate = moved && ev.altKey;
|
|
6418
|
-
|
|
6419
|
-
if (onDuplicate && shouldDuplicate) {
|
|
6420
|
-
const dropCanvasPosition = canvasPointFromClient(
|
|
6421
|
-
ev.clientX,
|
|
6422
|
-
ev.clientY,
|
|
6423
|
-
);
|
|
6424
|
-
// shouldDuplicate implies moved, so the drop position is always
|
|
6425
|
-
// relative to the pointer's offset into the source frame (the
|
|
6426
|
-
// "snap next to source" placement only applied to the old
|
|
6427
|
-
// zero-move alt-click case, which no longer duplicates at all).
|
|
6428
|
-
const canvasPosition = {
|
|
6429
|
-
x: dropCanvasPosition.x - pointerOffset.x,
|
|
6430
|
-
y: dropCanvasPosition.y - pointerOffset.y,
|
|
6431
|
-
};
|
|
6432
|
-
// The user placed this clone deliberately — suppress the
|
|
6433
|
-
// lineup-recenter camera move when the created screen lands in
|
|
6434
|
-
// `screens` (Figma keeps the camera still on alt-drag duplicate).
|
|
6435
|
-
lineupRecenterSuppressRef.current = {
|
|
6436
|
-
atMs: Date.now(),
|
|
6437
|
-
fromCount: screensRef.current.length,
|
|
6438
|
-
addedCount: 1,
|
|
6439
|
-
};
|
|
6440
|
-
onDuplicate(screen.id, {
|
|
6441
|
-
mode,
|
|
6442
|
-
screen,
|
|
6443
|
-
canvasPosition,
|
|
6444
|
-
canvasOffset: pointerOffset,
|
|
6445
|
-
dropCanvasPosition,
|
|
6446
|
-
});
|
|
6447
|
-
} else if (!moved) {
|
|
6448
|
-
onPick(screen.id);
|
|
6449
|
-
}
|
|
6450
|
-
|
|
6451
|
-
cleanupDuplicateGesture();
|
|
6452
|
-
};
|
|
6453
|
-
|
|
6454
|
-
duplicateCleanup.current = cleanupDuplicateGesture;
|
|
6455
|
-
installDragListeners(
|
|
6456
|
-
handleMouseMove,
|
|
6457
|
-
handleMouseUp,
|
|
6458
|
-
cleanupDuplicateGesture,
|
|
6459
|
-
);
|
|
6460
|
-
},
|
|
6461
|
-
[
|
|
6462
|
-
canvasPointFromClient,
|
|
6463
|
-
finishDrag,
|
|
6464
|
-
getCurrentFrameEntries,
|
|
6465
|
-
installDragListeners,
|
|
6466
|
-
lockedScreenIdSet,
|
|
6467
|
-
onDuplicate,
|
|
6468
|
-
onPick,
|
|
6469
|
-
],
|
|
6470
|
-
);
|
|
6471
|
-
|
|
6472
6488
|
const handleMouseDown = useCallback(
|
|
6473
6489
|
(e: React.MouseEvent) => {
|
|
6474
6490
|
claimKeyboardFocus();
|
|
@@ -7386,15 +7402,15 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
7386
7402
|
) {
|
|
7387
7403
|
return;
|
|
7388
7404
|
}
|
|
7389
|
-
// Always suppress the browser default (bookmark dialog) — but leave
|
|
7390
|
-
// propagation intact until we know a frame is duplicable here, so the
|
|
7391
|
-
// global hotkey hook can still duplicate non-frame layer selections.
|
|
7392
|
-
event.preventDefault();
|
|
7393
7405
|
// Only act on frame IDs — filter out canvas primitives (sub-elements).
|
|
7394
7406
|
const frameIds = selectedIdsRef.current.filter(
|
|
7395
7407
|
(id) => frameGeometryRef.current[id],
|
|
7396
7408
|
);
|
|
7409
|
+
// Claim the event only once a frame will really be duplicated — the
|
|
7410
|
+
// global hotkey hook skips anything already defaultPrevented, and it
|
|
7411
|
+
// suppresses the bookmark dialog itself when it takes over.
|
|
7397
7412
|
if (frameIds.length === 0) return;
|
|
7413
|
+
event.preventDefault();
|
|
7398
7414
|
event.stopPropagation();
|
|
7399
7415
|
event.stopImmediatePropagation();
|
|
7400
7416
|
// Duplicate every selected frame, not just the first — each duplicate
|
|
@@ -8152,7 +8168,6 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
8152
8168
|
onStartFrameDrag={beginFrameDrag}
|
|
8153
8169
|
onStartResize={beginResize}
|
|
8154
8170
|
onStartRotate={beginRotate}
|
|
8155
|
-
onStartDuplicateGesture={beginDuplicateGesture}
|
|
8156
8171
|
// Pass the id-first callbacks straight through (PF18): Screen
|
|
8157
8172
|
// itself binds screen.id when it calls these, so every screen
|
|
8158
8173
|
// instance gets the exact same stable function reference here
|
|
@@ -8574,7 +8589,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
8574
8589
|
>
|
|
8575
8590
|
<div className="flex h-full w-full items-start justify-between rounded-lg bg-muted/20 p-2">
|
|
8576
8591
|
<span className="max-w-[190px] truncate !text-[11px] font-medium text-foreground">
|
|
8577
|
-
{duplicatePreview.
|
|
8592
|
+
{duplicatePreview.count > 1
|
|
8593
|
+
? `${duplicatePreview.display} +${duplicatePreview.count - 1}`
|
|
8594
|
+
: duplicatePreview.display}
|
|
8578
8595
|
</span>
|
|
8579
8596
|
<span className="flex items-center gap-1 rounded-md border border-border bg-background px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground shadow-sm">
|
|
8580
8597
|
<IconCopy className="h-3 w-3" />
|
|
@@ -9411,6 +9428,14 @@ function GradientEditOverlay({
|
|
|
9411
9428
|
);
|
|
9412
9429
|
}
|
|
9413
9430
|
|
|
9431
|
+
/** The name shown in a frame's label row and in the alt-drag copy ghost. */
|
|
9432
|
+
function screenDisplayName(
|
|
9433
|
+
screen: ScreenFile,
|
|
9434
|
+
metadata: ResolvedScreenMetadata,
|
|
9435
|
+
): string {
|
|
9436
|
+
return metadata.title ?? prettyScreenName(screen.filename);
|
|
9437
|
+
}
|
|
9438
|
+
|
|
9414
9439
|
/** Standard Tailwind breakpoint widths, mobile-first (base / md: / lg: / xl:). */
|
|
9415
9440
|
const STANDARD_BREAKPOINT_WIDTHS = [390, 768, 1280] as const;
|
|
9416
9441
|
|
|
@@ -9483,11 +9508,6 @@ interface ScreenProps {
|
|
|
9483
9508
|
e: React.MouseEvent,
|
|
9484
9509
|
) => void;
|
|
9485
9510
|
onStartRotate: (id: string, e: React.MouseEvent) => void;
|
|
9486
|
-
onStartDuplicateGesture: (
|
|
9487
|
-
screen: ScreenFile,
|
|
9488
|
-
display: string,
|
|
9489
|
-
e: React.MouseEvent<HTMLElement>,
|
|
9490
|
-
) => void;
|
|
9491
9511
|
// Id-first (screenId, widthPx) shape, same as MultiScreenCanvas's own
|
|
9492
9512
|
// onActiveBreakpointChange prop (PF18): Screen binds screen.id itself when
|
|
9493
9513
|
// calling these, so the parent can pass the same stable function reference
|
|
@@ -9541,7 +9561,6 @@ const Screen = memo(function Screen({
|
|
|
9541
9561
|
onStartFrameDrag,
|
|
9542
9562
|
onStartResize,
|
|
9543
9563
|
onStartRotate,
|
|
9544
|
-
onStartDuplicateGesture,
|
|
9545
9564
|
screenContent,
|
|
9546
9565
|
renderBreakpointContent,
|
|
9547
9566
|
cullTier,
|
|
@@ -9552,7 +9571,7 @@ const Screen = memo(function Screen({
|
|
|
9552
9571
|
onEditBreakpoint,
|
|
9553
9572
|
}: ScreenProps) {
|
|
9554
9573
|
const t = useT();
|
|
9555
|
-
const display =
|
|
9574
|
+
const display = screenDisplayName(screen, metadata);
|
|
9556
9575
|
const previewUrl = metadata.previewUrl ?? getPreviewUrl(screen.content);
|
|
9557
9576
|
const previewViewport = getScreenPreviewViewport(metadata, geometry);
|
|
9558
9577
|
const suppressNextClick = useRef(false);
|
|
@@ -9684,15 +9703,11 @@ const Screen = memo(function Screen({
|
|
|
9684
9703
|
return;
|
|
9685
9704
|
}
|
|
9686
9705
|
if (e.altKey) {
|
|
9687
|
-
//
|
|
9688
|
-
// this
|
|
9689
|
-
//
|
|
9690
|
-
// away from the newly created duplicate.
|
|
9706
|
+
// Without this the trailing click after an alt-drag copy falls
|
|
9707
|
+
// through to this row's onClick and steals selection back from the
|
|
9708
|
+
// new duplicate.
|
|
9691
9709
|
suppressNextClick.current = true;
|
|
9692
|
-
|
|
9693
|
-
return;
|
|
9694
|
-
}
|
|
9695
|
-
if (e.shiftKey) {
|
|
9710
|
+
} else if (e.shiftKey) {
|
|
9696
9711
|
e.stopPropagation();
|
|
9697
9712
|
return;
|
|
9698
9713
|
}
|
|
@@ -9838,13 +9853,10 @@ const Screen = memo(function Screen({
|
|
|
9838
9853
|
e.stopPropagation();
|
|
9839
9854
|
return;
|
|
9840
9855
|
}
|
|
9841
|
-
if (e.altKey && e.button === 0) {
|
|
9842
|
-
suppressNextClick.current = true;
|
|
9843
|
-
onStartDuplicateGesture(screen, display, e);
|
|
9844
|
-
return;
|
|
9845
|
-
}
|
|
9846
9856
|
if (e.button === 0) {
|
|
9847
|
-
if (e.
|
|
9857
|
+
if (e.altKey) {
|
|
9858
|
+
suppressNextClick.current = true;
|
|
9859
|
+
} else if (e.shiftKey) {
|
|
9848
9860
|
e.stopPropagation();
|
|
9849
9861
|
return;
|
|
9850
9862
|
}
|
|
@@ -10105,7 +10117,6 @@ function areScreenPropsEqual(prev: ScreenProps, next: ScreenProps) {
|
|
|
10105
10117
|
prev.onStartFrameDrag === next.onStartFrameDrag &&
|
|
10106
10118
|
prev.onStartResize === next.onStartResize &&
|
|
10107
10119
|
prev.onStartRotate === next.onStartRotate &&
|
|
10108
|
-
prev.onStartDuplicateGesture === next.onStartDuplicateGesture &&
|
|
10109
10120
|
// Now id-first (screenId, widthPx) callbacks passed straight through
|
|
10110
10121
|
// from MultiScreenCanvas's own props (PF18) instead of a fresh
|
|
10111
10122
|
// per-screen arrow allocated in the render loop, so these are expected
|
|
@@ -976,6 +976,9 @@ export interface ResolvedScreenMetadata {
|
|
|
976
976
|
|
|
977
977
|
export interface DuplicatePreview {
|
|
978
978
|
display: string;
|
|
979
|
+
/** How many frames the drop will copy — the whole frame selection when the
|
|
980
|
+
* alt-drag started on a selected frame, otherwise just the pressed one. */
|
|
981
|
+
count: number;
|
|
979
982
|
x: number;
|
|
980
983
|
y: number;
|
|
981
984
|
width: number;
|