@agent-native/core 0.125.0 → 0.126.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/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/server/email-template.ts +13 -0
- package/corpus/core/src/server/email.ts +40 -4
- package/corpus/core/src/sharing/actions/share-resource.ts +55 -5
- package/corpus/core/src/sharing/registry.ts +41 -1
- package/corpus/templates/assets/actions/edit-image.ts +31 -18
- package/corpus/templates/assets/actions/refine-image.ts +43 -29
- package/corpus/templates/assets/actions/restyle-image.ts +30 -19
- package/corpus/templates/assets/actions/variant-slots.ts +26 -0
- package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +85 -17
- package/corpus/templates/clips/changelog/2026-07-27-share-notification-emails-now-include-a-clickable-recording-.md +6 -0
- package/corpus/templates/clips/server/db/index.ts +28 -6
- package/corpus/templates/clips/server/lib/share-email-hero.ts +105 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +100 -12
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +17 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +169 -22
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/email-template.d.ts +6 -0
- package/dist/server/email-template.d.ts.map +1 -1
- package/dist/server/email-template.js +6 -0
- package/dist/server/email-template.js.map +1 -1
- package/dist/server/email.d.ts +7 -0
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +31 -6
- package/dist/server/email.js.map +1 -1
- package/dist/sharing/actions/share-resource.d.ts.map +1 -1
- package/dist/sharing/actions/share-resource.js +49 -5
- package/dist/sharing/actions/share-resource.js.map +1 -1
- package/dist/sharing/registry.d.ts +40 -1
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js.map +1 -1
- package/package.json +1 -1
- package/src/server/email-template.ts +13 -0
- package/src/server/email.ts +40 -4
- package/src/sharing/actions/share-resource.ts +55 -5
- package/src/sharing/registry.ts +41 -1
|
@@ -89,6 +89,9 @@ export function GenerationResults({ threadId }: { threadId: string | null }) {
|
|
|
89
89
|
const queryClient = useQueryClient();
|
|
90
90
|
const [clearAllOpen, setClearAllOpen] = useState(false);
|
|
91
91
|
const [previewSlotId, setPreviewSlotId] = useState<string | null>(null);
|
|
92
|
+
const trayScrollRef = useRef<HTMLDivElement | null>(null);
|
|
93
|
+
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
94
|
+
const [canScrollRight, setCanScrollRight] = useState(false);
|
|
92
95
|
const stateKey = variantStateKey(threadId);
|
|
93
96
|
const stateQueryKey = useMemo(() => ["app-state", stateKey], [stateKey]);
|
|
94
97
|
const { data: variants } = useQuery({
|
|
@@ -129,6 +132,21 @@ export function GenerationResults({ threadId }: { threadId: string | null }) {
|
|
|
129
132
|
),
|
|
130
133
|
[variants?.slots],
|
|
131
134
|
);
|
|
135
|
+
// Variant numbers reflect generation order (oldest = 1), not display order:
|
|
136
|
+
// refined candidates render first but keep the next number in sequence
|
|
137
|
+
// instead of stealing "Variant 1" from the newest-first display sort above.
|
|
138
|
+
const variantNumberBySlotId = useMemo(() => {
|
|
139
|
+
const map = new Map<string, number>();
|
|
140
|
+
(variants?.slots ?? [])
|
|
141
|
+
.slice()
|
|
142
|
+
.sort(
|
|
143
|
+
(left, right) =>
|
|
144
|
+
slotTime(left) - slotTime(right) ||
|
|
145
|
+
left.slotId.localeCompare(right.slotId),
|
|
146
|
+
)
|
|
147
|
+
.forEach((slot, index) => map.set(slot.slotId, index + 1));
|
|
148
|
+
return map;
|
|
149
|
+
}, [variants?.slots]);
|
|
132
150
|
const belongsToThread = Boolean(
|
|
133
151
|
variants &&
|
|
134
152
|
(threadId ? variants.threadId === threadId : !variants.threadId),
|
|
@@ -165,6 +183,25 @@ export function GenerationResults({ threadId }: { threadId: string | null }) {
|
|
|
165
183
|
[previewSlotId, slots],
|
|
166
184
|
);
|
|
167
185
|
|
|
186
|
+
const updateScrollEdges = () => {
|
|
187
|
+
const el = trayScrollRef.current;
|
|
188
|
+
if (!el) return;
|
|
189
|
+
setCanScrollLeft(el.scrollLeft > 1);
|
|
190
|
+
setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
// Re-measure after the slot list changes size (new/removed candidates)
|
|
195
|
+
// since that can flip whether either arrow should be enabled.
|
|
196
|
+
updateScrollEdges();
|
|
197
|
+
}, [slots.length]);
|
|
198
|
+
|
|
199
|
+
const scrollTrayBy = (direction: 1 | -1) => {
|
|
200
|
+
const el = trayScrollRef.current;
|
|
201
|
+
if (!el) return;
|
|
202
|
+
el.scrollBy({ left: direction * el.clientWidth * 0.9, behavior: "smooth" });
|
|
203
|
+
};
|
|
204
|
+
|
|
168
205
|
if (!belongsToThread || !variants) return null;
|
|
169
206
|
if (slots.length === 0) return null;
|
|
170
207
|
|
|
@@ -296,6 +333,7 @@ export function GenerationResults({ threadId }: { threadId: string | null }) {
|
|
|
296
333
|
<GenerationPreviewDialog
|
|
297
334
|
slot={previewSlot}
|
|
298
335
|
slots={slots}
|
|
336
|
+
variantNumberBySlotId={variantNumberBySlotId}
|
|
299
337
|
prompt={variants.prompt}
|
|
300
338
|
libraryTitle={libraryTitle}
|
|
301
339
|
isSaving={saveGenerated.isPending}
|
|
@@ -351,22 +389,50 @@ export function GenerationResults({ threadId }: { threadId: string | null }) {
|
|
|
351
389
|
</Button>
|
|
352
390
|
</div>
|
|
353
391
|
|
|
354
|
-
<div className="
|
|
355
|
-
<div
|
|
356
|
-
{
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
392
|
+
<div className="relative">
|
|
393
|
+
<div
|
|
394
|
+
ref={trayScrollRef}
|
|
395
|
+
onScroll={updateScrollEdges}
|
|
396
|
+
className="assets-library-tray flex gap-3 overflow-x-auto scroll-smooth p-3"
|
|
397
|
+
>
|
|
398
|
+
{slots.map((slot) => {
|
|
399
|
+
const variantNumber =
|
|
400
|
+
variantNumberBySlotId.get(slot.slotId) ?? 1;
|
|
401
|
+
return (
|
|
402
|
+
<GenerationDraftCard
|
|
403
|
+
key={slot.slotId}
|
|
404
|
+
slot={slot}
|
|
405
|
+
variantNumber={variantNumber}
|
|
406
|
+
isSaving={saveGenerated.isPending}
|
|
407
|
+
isDismissing={dismissSlot.isPending}
|
|
408
|
+
onPreview={() => setPreviewSlotId(slot.slotId)}
|
|
409
|
+
onSave={() => saveSlot(slot)}
|
|
410
|
+
onRefine={() => refineSlot(slot, variantNumber)}
|
|
411
|
+
onDismiss={() => dismissSlotById(slot)}
|
|
412
|
+
/>
|
|
413
|
+
);
|
|
414
|
+
})}
|
|
369
415
|
</div>
|
|
416
|
+
{canScrollLeft ? (
|
|
417
|
+
<button
|
|
418
|
+
type="button"
|
|
419
|
+
aria-label={t("library.previousImage")}
|
|
420
|
+
onClick={() => scrollTrayBy(-1)}
|
|
421
|
+
className="absolute left-1 top-1/2 z-10 inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full border border-border/80 bg-background/90 text-foreground shadow-sm transition hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
422
|
+
>
|
|
423
|
+
<IconChevronLeft className="h-4 w-4" />
|
|
424
|
+
</button>
|
|
425
|
+
) : null}
|
|
426
|
+
{canScrollRight ? (
|
|
427
|
+
<button
|
|
428
|
+
type="button"
|
|
429
|
+
aria-label={t("library.nextImage")}
|
|
430
|
+
onClick={() => scrollTrayBy(1)}
|
|
431
|
+
className="absolute right-1 top-1/2 z-10 inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full border border-border/80 bg-background/90 text-foreground shadow-sm transition hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
432
|
+
>
|
|
433
|
+
<IconChevronRight className="h-4 w-4" />
|
|
434
|
+
</button>
|
|
435
|
+
) : null}
|
|
370
436
|
</div>
|
|
371
437
|
</div>
|
|
372
438
|
</section>
|
|
@@ -399,7 +465,7 @@ function GenerationDraftCard({
|
|
|
399
465
|
number: variantNumber,
|
|
400
466
|
});
|
|
401
467
|
return (
|
|
402
|
-
<div className="overflow-hidden rounded-lg border border-border/80 bg-background transition hover:border-foreground/25 hover:bg-muted/10">
|
|
468
|
+
<div className="w-36 shrink-0 overflow-hidden rounded-lg border border-border/80 bg-background transition hover:border-foreground/25 hover:bg-muted/10 sm:w-44">
|
|
403
469
|
<div className="group relative">
|
|
404
470
|
<button
|
|
405
471
|
type="button"
|
|
@@ -510,6 +576,7 @@ function GenerationDraftCard({
|
|
|
510
576
|
function GenerationPreviewDialog({
|
|
511
577
|
slot,
|
|
512
578
|
slots,
|
|
579
|
+
variantNumberBySlotId,
|
|
513
580
|
prompt,
|
|
514
581
|
libraryTitle,
|
|
515
582
|
isSaving,
|
|
@@ -522,6 +589,7 @@ function GenerationPreviewDialog({
|
|
|
522
589
|
}: {
|
|
523
590
|
slot: VariantSlot | null;
|
|
524
591
|
slots: VariantSlot[];
|
|
592
|
+
variantNumberBySlotId: Map<string, number>;
|
|
525
593
|
prompt: string;
|
|
526
594
|
libraryTitle: string | null;
|
|
527
595
|
isSaving: boolean;
|
|
@@ -553,7 +621,7 @@ function GenerationPreviewDialog({
|
|
|
553
621
|
const sources = slot ? assetPreviewSources(slot, "preview") : [];
|
|
554
622
|
const src = sources[0];
|
|
555
623
|
const variantNumber = slot
|
|
556
|
-
?
|
|
624
|
+
? (variantNumberBySlotId.get(slot.slotId) ?? 0)
|
|
557
625
|
: 0;
|
|
558
626
|
const variantLabel = t("library.variantWithNumber", {
|
|
559
627
|
number: variantNumber,
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { createGetDb, getDbExec } from "@agent-native/core/db";
|
|
2
|
-
import {
|
|
2
|
+
import { organizations } from "@agent-native/core/org";
|
|
3
3
|
import { registerShareableResource } from "@agent-native/core/sharing";
|
|
4
4
|
import { eq } from "drizzle-orm";
|
|
5
5
|
|
|
6
|
+
import {
|
|
7
|
+
absoluteUrl,
|
|
8
|
+
recordingShareHeroHtml,
|
|
9
|
+
} from "../lib/share-email-hero.js";
|
|
6
10
|
import * as schema from "./schema.js";
|
|
7
11
|
|
|
8
12
|
export const getDb = createGetDb(schema);
|
|
@@ -22,9 +26,20 @@ async function orgBrandLogoUrl(
|
|
|
22
26
|
.from(schema.organizationSettings)
|
|
23
27
|
.where(eq(schema.organizationSettings.organizationId, organizationId))
|
|
24
28
|
.limit(1);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
return absoluteUrl(row?.brandLogoUrl);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Show the sharing org's name beside the logo instead of the app name. */
|
|
33
|
+
async function orgBrandName(
|
|
34
|
+
organizationId: string | undefined,
|
|
35
|
+
): Promise<string | undefined> {
|
|
36
|
+
if (!organizationId) return undefined;
|
|
37
|
+
const [row] = await getDb()
|
|
38
|
+
.select({ name: organizations.name })
|
|
39
|
+
.from(organizations)
|
|
40
|
+
.where(eq(organizations.id, organizationId))
|
|
41
|
+
.limit(1);
|
|
42
|
+
return row?.name?.trim() || undefined;
|
|
28
43
|
}
|
|
29
44
|
|
|
30
45
|
registerShareableResource({
|
|
@@ -34,8 +49,15 @@ registerShareableResource({
|
|
|
34
49
|
displayName: "Recording",
|
|
35
50
|
titleColumn: "title",
|
|
36
51
|
getResourcePath: (recording) => `/r/${recording.id}`,
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
getLogoUrl: (recording) => orgBrandLogoUrl(recording.organizationId),
|
|
53
|
+
getBrandName: (recording) => orgBrandName(recording.organizationId),
|
|
54
|
+
// Replies reach the person who shared the clip; the sending address stays
|
|
55
|
+
// the verified one so SPF/DKIM still pass.
|
|
56
|
+
getSender: (_recording, ctx) => ({
|
|
57
|
+
fromName: `${ctx.sender.name} via Clips`,
|
|
58
|
+
replyTo: ctx.sender.email,
|
|
59
|
+
}),
|
|
60
|
+
getHeroHtml: (recording, ctx) => recordingShareHeroHtml(recording, ctx),
|
|
39
61
|
getDb,
|
|
40
62
|
ownerAccessIgnoresOrg: true,
|
|
41
63
|
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAppProductionUrl,
|
|
3
|
+
withConfiguredAppBasePath,
|
|
4
|
+
} from "@agent-native/core/server";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Origin plus the configured mount path. Deployments served under
|
|
8
|
+
* APP_BASE_PATH (e.g. `/clips`) would otherwise link email assets at the
|
|
9
|
+
* gateway root, where nothing is served.
|
|
10
|
+
*/
|
|
11
|
+
function appBaseUrl(): string {
|
|
12
|
+
return withConfiguredAppBasePath(getAppProductionUrl());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Make a stored URL absolute for use in emails (relative paths won't load). */
|
|
16
|
+
export function absoluteUrl(
|
|
17
|
+
url: string | null | undefined,
|
|
18
|
+
): string | undefined {
|
|
19
|
+
const trimmed = url?.trim();
|
|
20
|
+
if (!trimmed) return undefined;
|
|
21
|
+
return trimmed.startsWith("/") ? `${appBaseUrl()}${trimmed}` : trimmed;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function escapeAttr(s: string): string {
|
|
25
|
+
return s
|
|
26
|
+
.replace(/&/g, "&")
|
|
27
|
+
.replace(/</g, "<")
|
|
28
|
+
.replace(/>/g, ">")
|
|
29
|
+
.replace(/"/g, """)
|
|
30
|
+
.replace(/'/g, "'");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Percent-encode characters that could terminate a CSS `url('…')`. A style
|
|
35
|
+
* attribute is HTML-decoded before it is parsed as CSS, so HTML escaping alone
|
|
36
|
+
* does not protect the CSS context — an escaped quote decodes back to a real
|
|
37
|
+
* one and lets the value break out into further declarations.
|
|
38
|
+
*/
|
|
39
|
+
function escapeCssUrl(url: string): string {
|
|
40
|
+
return url.replace(
|
|
41
|
+
/['"()\\\s]/g,
|
|
42
|
+
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isHttpUrl(value: string): boolean {
|
|
47
|
+
try {
|
|
48
|
+
const { protocol } = new URL(value);
|
|
49
|
+
return protocol === "https:" || protocol === "http:";
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ShareHeroRecording = {
|
|
56
|
+
thumbnailUrl?: string | null;
|
|
57
|
+
animatedThumbnailUrl?: string | null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build the share-email preview for a recording: a 16:9 thumbnail with a
|
|
62
|
+
* centered play badge, linking to the clip. Uses the background-image + VML
|
|
63
|
+
* technique so the badge stays centered across clients, including Outlook. The
|
|
64
|
+
* play badge is served from Clips' own public assets.
|
|
65
|
+
*/
|
|
66
|
+
export function recordingShareHeroHtml(
|
|
67
|
+
recording: ShareHeroRecording,
|
|
68
|
+
ctx: { href: string; alt?: string },
|
|
69
|
+
): string | undefined {
|
|
70
|
+
// GIF-only recordings leave `thumbnailUrl` empty, so fall back to the
|
|
71
|
+
// animated thumbnail rather than dropping the preview entirely.
|
|
72
|
+
const thumb = absoluteUrl(
|
|
73
|
+
recording.thumbnailUrl || recording.animatedThumbnailUrl,
|
|
74
|
+
);
|
|
75
|
+
if (!thumb || !isHttpUrl(thumb)) return undefined;
|
|
76
|
+
const url = escapeAttr(thumb);
|
|
77
|
+
const cssUrl = escapeAttr(escapeCssUrl(thumb));
|
|
78
|
+
const href = escapeAttr(ctx.href);
|
|
79
|
+
const title = escapeAttr(ctx.alt ?? "Play video");
|
|
80
|
+
const badge = escapeAttr(`${appBaseUrl()}/play-badge.png`);
|
|
81
|
+
const W = 488;
|
|
82
|
+
const H = 275;
|
|
83
|
+
return `
|
|
84
|
+
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="margin:24px 0 0 0;">
|
|
85
|
+
<tr>
|
|
86
|
+
<td>
|
|
87
|
+
<a href="${href}" style="display:block; text-decoration:none;" title="${title}">
|
|
88
|
+
<!--[if mso]>
|
|
89
|
+
<v:image xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:${W}px;height:${H}px;" src="${url}" />
|
|
90
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="false" stroke="false" style="position:absolute;width:${W}px;height:${H}px;">
|
|
91
|
+
<v:textbox inset="0,0,0,0"><center>
|
|
92
|
+
<![endif]-->
|
|
93
|
+
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="${W}" height="${H}" background="${url}" style="width:100%; max-width:${W}px; height:${H}px; background-image:url('${cssUrl}'); background-size:cover; background-position:center; background-color:#141417; border-radius:12px;">
|
|
94
|
+
<tr>
|
|
95
|
+
<td align="center" valign="middle" height="${H}" style="height:${H}px;">
|
|
96
|
+
<img src="${badge}" alt="Play video" width="64" height="64" style="width:64px; height:64px; border:0; display:inline-block;" />
|
|
97
|
+
</td>
|
|
98
|
+
</tr>
|
|
99
|
+
</table>
|
|
100
|
+
<!--[if mso]></center></v:textbox></v:rect><![endif]-->
|
|
101
|
+
</a>
|
|
102
|
+
</td>
|
|
103
|
+
</tr>
|
|
104
|
+
</table>`;
|
|
105
|
+
}
|
|
@@ -23,6 +23,13 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
})();
|
|
26
|
+
var selectedLayerDragPriorityEnabled = (function() {
|
|
27
|
+
try {
|
|
28
|
+
return !!__SELECTED_LAYER_DRAG_PRIORITY__;
|
|
29
|
+
} catch (_e) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
26
33
|
var scaleToolEnabled = false;
|
|
27
34
|
function dndLog(phase, data) {
|
|
28
35
|
if (!window.__DND_DEBUG) return;
|
|
@@ -1596,6 +1603,7 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
1596
1603
|
clearComponentTag();
|
|
1597
1604
|
}
|
|
1598
1605
|
var selectedEl = null;
|
|
1606
|
+
var selectionChromeHidden = false;
|
|
1599
1607
|
var hoveredEl = null;
|
|
1600
1608
|
var highlightOverlayStyle = "default";
|
|
1601
1609
|
var activeNodeHtmlPreview = null;
|
|
@@ -2773,7 +2781,11 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
2773
2781
|
hideSelectionOverlay();
|
|
2774
2782
|
}
|
|
2775
2783
|
} else if (selectedEl) {
|
|
2776
|
-
|
|
2784
|
+
if (selectionChromeHidden) {
|
|
2785
|
+
hideSelectionOverlay();
|
|
2786
|
+
} else {
|
|
2787
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
2788
|
+
}
|
|
2777
2789
|
} else {
|
|
2778
2790
|
hideParentAutoLayoutOverlay();
|
|
2779
2791
|
}
|
|
@@ -3507,29 +3519,27 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
3507
3519
|
document.addEventListener(events.move, onMove, true);
|
|
3508
3520
|
document.addEventListener(events.up, onUp, true);
|
|
3509
3521
|
}
|
|
3510
|
-
function
|
|
3511
|
-
stopNativeInteraction(e);
|
|
3512
|
-
blurActiveTextEditor();
|
|
3522
|
+
function collectLayerHitCandidates(clientX, clientY) {
|
|
3513
3523
|
var shieldPointerEvents = shieldOverlay.style.pointerEvents;
|
|
3514
3524
|
var selectionPointerEvents = selectionOverlay.style.pointerEvents;
|
|
3515
3525
|
var highlightPointerEvents = highlightOverlay.style.pointerEvents;
|
|
3516
3526
|
shieldOverlay.style.pointerEvents = "none";
|
|
3517
3527
|
selectionOverlay.style.pointerEvents = "none";
|
|
3518
3528
|
highlightOverlay.style.pointerEvents = "none";
|
|
3519
|
-
var pointTargets = document.elementsFromPoint ? document.elementsFromPoint(
|
|
3529
|
+
var pointTargets = document.elementsFromPoint ? document.elementsFromPoint(clientX, clientY) : [document.elementFromPoint(clientX, clientY)];
|
|
3520
3530
|
shieldOverlay.style.pointerEvents = shieldPointerEvents;
|
|
3521
3531
|
selectionOverlay.style.pointerEvents = selectionPointerEvents;
|
|
3522
3532
|
highlightOverlay.style.pointerEvents = highlightPointerEvents;
|
|
3523
|
-
var
|
|
3533
|
+
var elements = [];
|
|
3524
3534
|
var layerCandidates = [];
|
|
3525
3535
|
pointTargets.forEach(function(pointTarget) {
|
|
3526
3536
|
if (!pointTarget || pointTarget.nodeType !== 1) return;
|
|
3527
3537
|
if (isOverlayElement(pointTarget)) return;
|
|
3528
3538
|
var candidate = selectionTargetForHit(pointTarget);
|
|
3529
|
-
if (!candidate || isDocumentRootElement(candidate) || isOverlayElement(candidate) || isLayerInteractionBlocked(candidate) || isTemplateCloneElement(candidate) ||
|
|
3539
|
+
if (!candidate || isDocumentRootElement(candidate) || isOverlayElement(candidate) || isLayerInteractionBlocked(candidate) || isTemplateCloneElement(candidate) || elements.indexOf(candidate) !== -1) {
|
|
3530
3540
|
return;
|
|
3531
3541
|
}
|
|
3532
|
-
|
|
3542
|
+
elements.push(candidate);
|
|
3533
3543
|
var candidateInfo = getElementInfo(candidate);
|
|
3534
3544
|
var explicitLabel = candidate.getAttribute && candidate.getAttribute("data-agent-native-layer-name") || "";
|
|
3535
3545
|
var textLabel = (candidate.textContent || "").trim().replace(/\\s+/g, " ");
|
|
@@ -3541,6 +3551,30 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
3541
3551
|
info: candidateInfo
|
|
3542
3552
|
});
|
|
3543
3553
|
});
|
|
3554
|
+
return { elements, layerCandidates };
|
|
3555
|
+
}
|
|
3556
|
+
function stackCycleTarget(clientX, clientY, currentEl) {
|
|
3557
|
+
if (!currentEl) return null;
|
|
3558
|
+
var stack = collectLayerHitCandidates(clientX, clientY);
|
|
3559
|
+
var currentIdx = stack.elements.indexOf(currentEl);
|
|
3560
|
+
if (currentIdx === -1) return null;
|
|
3561
|
+
var keys = stack.layerCandidates.map(function(candidate) {
|
|
3562
|
+
return candidate.key;
|
|
3563
|
+
});
|
|
3564
|
+
var nextKey = nextStackCandidate(
|
|
3565
|
+
keys,
|
|
3566
|
+
stack.layerCandidates[currentIdx].key
|
|
3567
|
+
);
|
|
3568
|
+
if (nextKey === null) return null;
|
|
3569
|
+
var nextEl = stack.elements[keys.indexOf(nextKey)];
|
|
3570
|
+
return nextEl && !isLayerInteractionBlocked(nextEl) ? nextEl : null;
|
|
3571
|
+
}
|
|
3572
|
+
function openContextMenuAtEvent(e) {
|
|
3573
|
+
stopNativeInteraction(e);
|
|
3574
|
+
blurActiveTextEditor();
|
|
3575
|
+
var collected = collectLayerHitCandidates(e.clientX, e.clientY);
|
|
3576
|
+
var candidateElements = collected.elements;
|
|
3577
|
+
var layerCandidates = collected.layerCandidates;
|
|
3544
3578
|
var target = candidateElements[0] || null;
|
|
3545
3579
|
var info = null;
|
|
3546
3580
|
if (target) {
|
|
@@ -6238,7 +6272,8 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
6238
6272
|
height: dragElStartHeight
|
|
6239
6273
|
},
|
|
6240
6274
|
snapCandidateRects,
|
|
6241
|
-
|
|
6275
|
+
// Convert the screen-space base to content px (1/zoom).
|
|
6276
|
+
SNAP_THRESHOLD_PX * chromeLineScale()
|
|
6242
6277
|
) : { dx: 0, dy: 0, guideV: null, guideH: null };
|
|
6243
6278
|
nextLeft += snapResult.dx;
|
|
6244
6279
|
nextTop += snapResult.dy;
|
|
@@ -6726,6 +6761,29 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
6726
6761
|
}
|
|
6727
6762
|
pendingShieldDrag = null;
|
|
6728
6763
|
}
|
|
6764
|
+
function dragTargetForPointerDown(args) {
|
|
6765
|
+
var selectedEl2 = args.selectedEl;
|
|
6766
|
+
var hitEl = args.hitEl;
|
|
6767
|
+
var hitRaw = args.hitRaw || hitEl;
|
|
6768
|
+
var selectedAlive = !!args.selectedAlive;
|
|
6769
|
+
if (selectedEl2 && selectedAlive && selectedEl2.contains && selectedEl2.contains(hitRaw)) {
|
|
6770
|
+
return selectedEl2;
|
|
6771
|
+
}
|
|
6772
|
+
if (args.preferSelected && selectedEl2 && selectedAlive) {
|
|
6773
|
+
var r = args.selectedRect;
|
|
6774
|
+
var p = args.point;
|
|
6775
|
+
if (r && p && r.width > 0 && r.height > 0 && p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom) {
|
|
6776
|
+
return selectedEl2;
|
|
6777
|
+
}
|
|
6778
|
+
}
|
|
6779
|
+
return hitEl;
|
|
6780
|
+
}
|
|
6781
|
+
function nextStackCandidate(candidateKeys, currentKey) {
|
|
6782
|
+
if (!candidateKeys || candidateKeys.length === 0) return null;
|
|
6783
|
+
var idx = candidateKeys.indexOf(currentKey);
|
|
6784
|
+
if (idx === -1) return null;
|
|
6785
|
+
return candidateKeys[(idx + 1) % candidateKeys.length];
|
|
6786
|
+
}
|
|
6729
6787
|
function beginPotentialShieldDrag(e) {
|
|
6730
6788
|
stopNativeInteraction(e);
|
|
6731
6789
|
if (e.button !== 0) return;
|
|
@@ -6737,7 +6795,17 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
6737
6795
|
beginMarqueeSelection(e);
|
|
6738
6796
|
return;
|
|
6739
6797
|
}
|
|
6740
|
-
var
|
|
6798
|
+
var selectedAlive = !!selectedEl && document.documentElement.contains(selectedEl);
|
|
6799
|
+
var selectedRect = selectedAlive && selectedEl.getBoundingClientRect ? selectedEl.getBoundingClientRect() : null;
|
|
6800
|
+
var dragTarget = dragTargetForPointerDown({
|
|
6801
|
+
selectedEl,
|
|
6802
|
+
selectedAlive,
|
|
6803
|
+
selectedRect,
|
|
6804
|
+
hitEl: hitTarget,
|
|
6805
|
+
hitRaw: hit,
|
|
6806
|
+
point: { x: e.clientX, y: e.clientY },
|
|
6807
|
+
preferSelected: selectedLayerDragPriorityEnabled
|
|
6808
|
+
});
|
|
6741
6809
|
var clickTarget = hitTarget;
|
|
6742
6810
|
if (!dragTarget || dragTarget === document.body || dragTarget === document.documentElement || isLayerInteractionBlocked(dragTarget)) {
|
|
6743
6811
|
return;
|
|
@@ -6793,7 +6861,12 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
6793
6861
|
postCrossScreenDrag("cancel");
|
|
6794
6862
|
}
|
|
6795
6863
|
if (ev) stopNativeInteraction(ev);
|
|
6796
|
-
|
|
6864
|
+
var cycledEl = !readOnly && (e.metaKey || e.ctrlKey) && !e.shiftKey ? stackCycleTarget(e.clientX, e.clientY, selectedEl) : null;
|
|
6865
|
+
if (cycledEl) {
|
|
6866
|
+
selectTarget(cycledEl);
|
|
6867
|
+
} else {
|
|
6868
|
+
selectTarget(clickTarget || dragTarget, ev);
|
|
6869
|
+
}
|
|
6797
6870
|
suppressNextShieldClickBriefly();
|
|
6798
6871
|
}
|
|
6799
6872
|
clearPendingShieldDrag();
|
|
@@ -7876,6 +7949,17 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
7876
7949
|
);
|
|
7877
7950
|
return;
|
|
7878
7951
|
}
|
|
7952
|
+
if (e.data.type === "set-selection-chrome-hidden") {
|
|
7953
|
+
selectionChromeHidden = !!e.data.hidden;
|
|
7954
|
+
if (selectionChromeHidden) {
|
|
7955
|
+
hideSelectionOverlay();
|
|
7956
|
+
} else if (selectedEl) {
|
|
7957
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
7958
|
+
updateParentAutoLayoutOverlay(selectedEl);
|
|
7959
|
+
refreshOverlays();
|
|
7960
|
+
}
|
|
7961
|
+
return;
|
|
7962
|
+
}
|
|
7879
7963
|
if (e.data.type === "select-element") {
|
|
7880
7964
|
var candidates = [];
|
|
7881
7965
|
if (Array.isArray(e.data.selectorCandidates)) {
|
|
@@ -7908,7 +7992,11 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
7908
7992
|
hoveredSpacingHandleKey = "";
|
|
7909
7993
|
}
|
|
7910
7994
|
selectedEl = target;
|
|
7911
|
-
|
|
7995
|
+
if (selectionChromeHidden) {
|
|
7996
|
+
hideSelectionOverlay();
|
|
7997
|
+
} else {
|
|
7998
|
+
positionOverlay(selectionOverlay, target);
|
|
7999
|
+
}
|
|
7912
8000
|
if (hoveredEl === selectedEl) highlightOverlay.style.display = "none";
|
|
7913
8001
|
if (selectionChangedByHost) {
|
|
7914
8002
|
postElementSelect(target);
|
|
@@ -316,6 +316,15 @@ ${editorChromeBridgeScript}
|
|
|
316
316
|
*/
|
|
317
317
|
const LIVE_REFLOW_ENABLED = true;
|
|
318
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Rollout gate: when on, a pointerdown inside the current selection's box keeps
|
|
321
|
+
* the selected element as the drag target even when an overlapping
|
|
322
|
+
* non-descendant sibling wins the hit test. Baked into the bridge as
|
|
323
|
+
* `__SELECTED_LAYER_DRAG_PRIORITY__`; flip to `false` for descendant-only
|
|
324
|
+
* behavior.
|
|
325
|
+
*/
|
|
326
|
+
const SELECTED_LAYER_DRAG_PRIORITY_ENABLED = true;
|
|
327
|
+
|
|
319
328
|
interface DesignCanvasProps {
|
|
320
329
|
content: string;
|
|
321
330
|
contentKey?: string;
|
|
@@ -950,6 +959,10 @@ function buildEditorChromeBridgeScript(args: {
|
|
|
950
959
|
"__LIVE_REFLOW_ENABLED__",
|
|
951
960
|
LIVE_REFLOW_ENABLED ? "true" : "false",
|
|
952
961
|
)
|
|
962
|
+
.replace(
|
|
963
|
+
"__SELECTED_LAYER_DRAG_PRIORITY__",
|
|
964
|
+
SELECTED_LAYER_DRAG_PRIORITY_ENABLED ? "true" : "false",
|
|
965
|
+
)
|
|
953
966
|
);
|
|
954
967
|
}
|
|
955
968
|
|
|
@@ -2131,6 +2144,10 @@ export function DesignCanvas({
|
|
|
2131
2144
|
.replace(
|
|
2132
2145
|
"__LIVE_REFLOW_ENABLED__",
|
|
2133
2146
|
LIVE_REFLOW_ENABLED ? "true" : "false",
|
|
2147
|
+
)
|
|
2148
|
+
.replace(
|
|
2149
|
+
"__SELECTED_LAYER_DRAG_PRIORITY__",
|
|
2150
|
+
SELECTED_LAYER_DRAG_PRIORITY_ENABLED ? "true" : "false",
|
|
2134
2151
|
);
|
|
2135
2152
|
// ALWAYS injected (like the other always-on bridges above) so
|
|
2136
2153
|
// MultiScreenCanvas's cross-screen drag hit-testing
|