@agent-native/core 0.124.1 → 0.124.3
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 +2 -2
- package/corpus/core/CHANGELOG.md +15 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/assets/branding/favicon-base64.ts +5 -0
- package/corpus/core/src/cli/templates-meta.ts +1 -0
- package/corpus/core/src/server/email.ts +2 -5
- package/corpus/core/src/shared/streaming-text-smoothing.ts +15 -14
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +34 -11
- package/corpus/templates/design/.agents/skills/responsive-breakpoints/SKILL.md +8 -0
- package/corpus/templates/design/actions/generate-design.ts +259 -36
- package/corpus/templates/design/actions/generate-screens.ts +51 -14
- package/corpus/templates/design/actions/present-design-variants.ts +6 -3
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +8 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +158 -9
- package/corpus/templates/design/app/components/design/design-canvas/content-size-report.ts +124 -0
- package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +61 -9
- package/corpus/templates/design/changelog/2026-07-24-ai-designs-now-default-to-a-full-size-desktop-mobile-pair-fr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-24-design-frames-now-fill-the-viewport-height-and-grow-to-fit-t.md +6 -0
- package/corpus/templates/design/changelog/2026-07-24-generating-an-additional-screen-now-places-it-beside-the-exi.md +6 -0
- package/dist/assets/branding/favicon-base64.d.ts +2 -0
- package/dist/assets/branding/favicon-base64.d.ts.map +1 -0
- package/dist/assets/branding/favicon-base64.js +5 -0
- package/dist/assets/branding/favicon-base64.js.map +1 -0
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +1 -0
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +2 -2
- package/dist/server/email.js.map +1 -1
- package/dist/shared/streaming-text-smoothing.d.ts.map +1 -1
- package/dist/shared/streaming-text-smoothing.js +15 -8
- package/dist/shared/streaming-text-smoothing.js.map +1 -1
- package/package.json +1 -1
- package/src/assets/branding/favicon-base64.ts +5 -0
- package/src/cli/templates-meta.ts +1 -0
- package/src/server/email.ts +2 -5
- package/src/shared/streaming-text-smoothing.ts +15 -14
|
@@ -77,15 +77,66 @@ const GENERATION_VIEWPORT_SIZES: Record<
|
|
|
77
77
|
> = {
|
|
78
78
|
mobile: { width: 390, height: 844 },
|
|
79
79
|
tablet: { width: 768, height: 1024 },
|
|
80
|
-
desktop: { width: 1440, height:
|
|
80
|
+
desktop: { width: 1440, height: 900 },
|
|
81
81
|
};
|
|
82
|
+
const GENERATION_VIEWPORT_LABELS: Record<GenerationViewport, string> = {
|
|
83
|
+
mobile: "Mobile",
|
|
84
|
+
tablet: "Tablet",
|
|
85
|
+
desktop: "Desktop",
|
|
86
|
+
};
|
|
87
|
+
// Widest → narrowest. The widest requested device seeds the primary/base
|
|
88
|
+
// frame; only the narrower devices become breakpoint frames.
|
|
89
|
+
const DEVICE_WIDTH_ORDER: readonly GenerationViewport[] = [
|
|
90
|
+
"desktop",
|
|
91
|
+
"tablet",
|
|
92
|
+
"mobile",
|
|
93
|
+
];
|
|
82
94
|
const GENERATED_FRAME_GAP = 96;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
|
|
96
|
+
function widestGenerationDevice(
|
|
97
|
+
devices: readonly GenerationViewport[],
|
|
98
|
+
): GenerationViewport {
|
|
99
|
+
return (
|
|
100
|
+
DEVICE_WIDTH_ORDER.find((device) => devices.includes(device)) ??
|
|
101
|
+
DEFAULT_GENERATION_VIEWPORT
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Devices used when the caller omits `devices`: the requested primary form
|
|
106
|
+
// factor as the base plus Mobile, unless the primary already is Mobile. The
|
|
107
|
+
// default (desktop primary) therefore yields a Desktop base + Mobile
|
|
108
|
+
// breakpoint, matching the two-screen default.
|
|
109
|
+
function devicesForPrimaryViewport(
|
|
110
|
+
primaryViewport: GenerationViewport,
|
|
111
|
+
): GenerationViewport[] {
|
|
112
|
+
return primaryViewport === "mobile"
|
|
113
|
+
? ["mobile"]
|
|
114
|
+
: [primaryViewport, "mobile"];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Breakpoint frames = every requested device NARROWER than the primary
|
|
118
|
+
// (widest) one, ascending by width. The primary/widest width is never emitted,
|
|
119
|
+
// so a single-device request produces an empty set (one frame, no sub-frames)
|
|
120
|
+
// and no redundant frame ever lands at the base canvas width.
|
|
121
|
+
function breakpointSetForDevices(devices: readonly GenerationViewport[]) {
|
|
122
|
+
const primaryWidth =
|
|
123
|
+
GENERATION_VIEWPORT_SIZES[widestGenerationDevice(devices)].width;
|
|
124
|
+
return Array.from(new Set(devices))
|
|
125
|
+
.filter((device) => GENERATION_VIEWPORT_SIZES[device].width < primaryWidth)
|
|
126
|
+
.sort(
|
|
127
|
+
(a, b) =>
|
|
128
|
+
GENERATION_VIEWPORT_SIZES[a].width - GENERATION_VIEWPORT_SIZES[b].width,
|
|
129
|
+
)
|
|
130
|
+
.map((device) => {
|
|
131
|
+
const widthPx = GENERATION_VIEWPORT_SIZES[device].width;
|
|
132
|
+
return {
|
|
133
|
+
id: `generated-${widthPx}`,
|
|
134
|
+
label: GENERATION_VIEWPORT_LABELS[device],
|
|
135
|
+
widthPx,
|
|
136
|
+
prefix: widthToPrefix(widthPx),
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
89
140
|
|
|
90
141
|
const reuseLabelSchema = z
|
|
91
142
|
.object({
|
|
@@ -123,16 +174,6 @@ function hasBreakpointSet(value: unknown): boolean {
|
|
|
123
174
|
return Array.isArray(breakpoints) && breakpoints.length > 0;
|
|
124
175
|
}
|
|
125
176
|
|
|
126
|
-
function nextGeneratedFrameX(
|
|
127
|
-
frames: ReturnType<typeof parseCanvasFrameGeometryById>,
|
|
128
|
-
): number {
|
|
129
|
-
return Object.values(frames).reduce((furthestRight, frame) => {
|
|
130
|
-
const x = frame.x ?? 0;
|
|
131
|
-
const width = frame.width ?? 0;
|
|
132
|
-
return Math.max(furthestRight, x + width + GENERATED_FRAME_GAP);
|
|
133
|
-
}, 0);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
177
|
function jsonValuesEqual(left: unknown, right: unknown): boolean {
|
|
137
178
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
138
179
|
}
|
|
@@ -393,7 +434,7 @@ const generateDesignAgentParameters = {
|
|
|
393
434
|
type: "string",
|
|
394
435
|
description:
|
|
395
436
|
"Optional JSON array of overview-canvas placements keyed by filename or fileId. " +
|
|
396
|
-
"Pass explicit x/y/width/height for every generated screen; desktop is
|
|
437
|
+
"Pass explicit x/y/width/height for every generated screen; desktop is 1440x900.",
|
|
397
438
|
},
|
|
398
439
|
contextPackId: {
|
|
399
440
|
type: "string",
|
|
@@ -415,8 +456,20 @@ const generateDesignAgentParameters = {
|
|
|
415
456
|
type: "string",
|
|
416
457
|
enum: ["mobile", "tablet", "desktop"],
|
|
417
458
|
description:
|
|
418
|
-
"The requested primary form factor. Defaults to desktop (
|
|
419
|
-
"Set this from the intake answer when no explicit canvasFrames placement is supplied."
|
|
459
|
+
"The requested primary form factor. Defaults to desktop (1440x900). " +
|
|
460
|
+
"Set this from the intake answer when no explicit canvasFrames placement is supplied. " +
|
|
461
|
+
"Ignored when `devices` is provided (the widest device becomes primary).",
|
|
462
|
+
},
|
|
463
|
+
devices: {
|
|
464
|
+
type: "array",
|
|
465
|
+
items: { type: "string", enum: ["mobile", "tablet", "desktop"] },
|
|
466
|
+
description:
|
|
467
|
+
"Device set for responsive frames. Honor the devices the prompt " +
|
|
468
|
+
'explicitly names; omit to default to ["desktop","mobile"]. The widest ' +
|
|
469
|
+
"device becomes the primary/base frame and the narrower devices become " +
|
|
470
|
+
"breakpoint frames — never a duplicate of the base width and never an " +
|
|
471
|
+
"auto-added tablet. A single device yields one frame with no breakpoints. " +
|
|
472
|
+
"When provided, this overrides primaryViewport.",
|
|
420
473
|
},
|
|
421
474
|
},
|
|
422
475
|
required: ["designId", "prompt", "files"],
|
|
@@ -438,9 +491,12 @@ const generateDesignAction = defineAction({
|
|
|
438
491
|
"When `designSystemId` is provided, first use `get-design-system` and apply " +
|
|
439
492
|
"its `agentContext` tokens/docs before writing the file content; do not " +
|
|
440
493
|
"treat the id alone as enough design-system context. " +
|
|
441
|
-
"Every web design must be responsive.
|
|
442
|
-
"
|
|
443
|
-
"tablet
|
|
494
|
+
"Every web design must be responsive. This action adds responsive editor " +
|
|
495
|
+
"breakpoints: by default a Desktop 1440x900 base frame plus a Mobile " +
|
|
496
|
+
"breakpoint (no auto tablet, no duplicate desktop). Pass `devices` to honor " +
|
|
497
|
+
"the form factors the prompt explicitly names — the widest becomes the base " +
|
|
498
|
+
"frame and the narrower ones become breakpoint frames. Set `primaryViewport` " +
|
|
499
|
+
"for a mobile- or tablet-primary design when not passing `devices`. " +
|
|
444
500
|
"Do not report a design as ready until this action succeeds. " +
|
|
445
501
|
"When adding multiple screens or states, pass canvasFrames with filenames " +
|
|
446
502
|
"and x/y/width/height so the new screens appear placed on the overview canvas.",
|
|
@@ -599,8 +655,20 @@ const generateDesignAction = defineAction({
|
|
|
599
655
|
.optional()
|
|
600
656
|
.default(DEFAULT_GENERATION_VIEWPORT)
|
|
601
657
|
.describe(
|
|
602
|
-
"Primary generated viewport. Defaults to desktop (
|
|
603
|
-
"mobile or tablet only when that is the requested form factor."
|
|
658
|
+
"Primary generated viewport. Defaults to desktop (1440x900); set " +
|
|
659
|
+
"mobile or tablet only when that is the requested form factor. " +
|
|
660
|
+
"Ignored when `devices` is provided (the widest device wins).",
|
|
661
|
+
),
|
|
662
|
+
devices: z
|
|
663
|
+
.array(z.enum(["mobile", "tablet", "desktop"]))
|
|
664
|
+
.optional()
|
|
665
|
+
.describe(
|
|
666
|
+
"Explicit device set for responsive frames. Honor the devices the " +
|
|
667
|
+
'prompt names; omit to default to ["desktop","mobile"]. Widest ' +
|
|
668
|
+
"device = primary/base frame; narrower devices = breakpoint frames " +
|
|
669
|
+
"(never the base width, never an auto tablet). One device = a single " +
|
|
670
|
+
"frame with no breakpoints. When provided, this overrides " +
|
|
671
|
+
"primaryViewport.",
|
|
604
672
|
),
|
|
605
673
|
}),
|
|
606
674
|
mcpApp: {
|
|
@@ -622,6 +690,7 @@ const generateDesignAction = defineAction({
|
|
|
622
690
|
tweaks,
|
|
623
691
|
canvasFrames,
|
|
624
692
|
primaryViewport,
|
|
693
|
+
devices,
|
|
625
694
|
contextPackId,
|
|
626
695
|
contextModeOverride,
|
|
627
696
|
reuseLabels,
|
|
@@ -827,6 +896,15 @@ const generateDesignAction = defineAction({
|
|
|
827
896
|
...tweak,
|
|
828
897
|
type: tweak.type === "color-swatches" ? "color-swatch" : tweak.type,
|
|
829
898
|
}));
|
|
899
|
+
// An explicit `devices` list wins over primaryViewport; otherwise derive
|
|
900
|
+
// the device set from primaryViewport so the default stays Desktop base +
|
|
901
|
+
// Mobile breakpoint. The widest resolved device seeds the primary frame.
|
|
902
|
+
const resolvedDevices =
|
|
903
|
+
devices && devices.length > 0
|
|
904
|
+
? devices
|
|
905
|
+
: devicesForPrimaryViewport(primaryViewport);
|
|
906
|
+
const resolvedPrimaryViewport = widestGenerationDevice(resolvedDevices);
|
|
907
|
+
const generatedBreakpointSet = breakpointSetForDevices(resolvedDevices);
|
|
830
908
|
await mutateDesignData({
|
|
831
909
|
designId,
|
|
832
910
|
mutate: (prevData, { updatedAt }) => {
|
|
@@ -865,9 +943,77 @@ const generateDesignAction = defineAction({
|
|
|
865
943
|
: undefined;
|
|
866
944
|
},
|
|
867
945
|
});
|
|
868
|
-
const viewport = GENERATION_VIEWPORT_SIZES[
|
|
869
|
-
|
|
870
|
-
|
|
946
|
+
const viewport = GENERATION_VIEWPORT_SIZES[resolvedPrimaryViewport];
|
|
947
|
+
// Frames placed by an earlier call: never moved, and counted as
|
|
948
|
+
// occupied so a new screen is never dropped on top of one.
|
|
949
|
+
const preExistingFrameIds = new Set(
|
|
950
|
+
prevData.canvasFrames && typeof prevData.canvasFrames === "object"
|
|
951
|
+
? Object.keys(prevData.canvasFrames as Record<string, unknown>)
|
|
952
|
+
: [],
|
|
953
|
+
);
|
|
954
|
+
const rectOf = (frame: {
|
|
955
|
+
x?: number;
|
|
956
|
+
y?: number;
|
|
957
|
+
width?: number;
|
|
958
|
+
height?: number;
|
|
959
|
+
rotation?: number;
|
|
960
|
+
}) => {
|
|
961
|
+
const x = frame.x ?? 0;
|
|
962
|
+
const y = frame.y ?? 0;
|
|
963
|
+
const width = frame.width ?? 0;
|
|
964
|
+
const height = frame.height ?? 0;
|
|
965
|
+
const rotation = frame.rotation ?? 0;
|
|
966
|
+
if (!rotation || width <= 0 || height <= 0) {
|
|
967
|
+
return { x, y, width, height };
|
|
968
|
+
}
|
|
969
|
+
// Existing frames render rotated about their center; use the rotated
|
|
970
|
+
// rect's axis-aligned bounding box so a new screen isn't dropped over
|
|
971
|
+
// a rotated frame's real footprint.
|
|
972
|
+
const radians = (rotation * Math.PI) / 180;
|
|
973
|
+
const cos = Math.abs(Math.cos(radians));
|
|
974
|
+
const sin = Math.abs(Math.sin(radians));
|
|
975
|
+
const aabbWidth = width * cos + height * sin;
|
|
976
|
+
const aabbHeight = width * sin + height * cos;
|
|
977
|
+
return {
|
|
978
|
+
x: x + width / 2 - aabbWidth / 2,
|
|
979
|
+
y: y + height / 2 - aabbHeight / 2,
|
|
980
|
+
width: aabbWidth,
|
|
981
|
+
height: aabbHeight,
|
|
982
|
+
};
|
|
983
|
+
};
|
|
984
|
+
const framesOverlap = (
|
|
985
|
+
a: ReturnType<typeof rectOf>,
|
|
986
|
+
b: ReturnType<typeof rectOf>,
|
|
987
|
+
) =>
|
|
988
|
+
a.width > 0 &&
|
|
989
|
+
a.height > 0 &&
|
|
990
|
+
b.width > 0 &&
|
|
991
|
+
b.height > 0 &&
|
|
992
|
+
a.x < b.x + b.width &&
|
|
993
|
+
a.x + a.width > b.x &&
|
|
994
|
+
a.y < b.y + b.height &&
|
|
995
|
+
a.y + a.height > b.y;
|
|
996
|
+
const occupiedRects: Array<ReturnType<typeof rectOf>> = [];
|
|
997
|
+
for (const id of preExistingFrameIds) {
|
|
998
|
+
const frame = merged.canvasFrames[id];
|
|
999
|
+
if (frame) occupiedRects.push(rectOf(frame));
|
|
1000
|
+
}
|
|
1001
|
+
// Keep arg placements for files we're not regenerating; the regenerated
|
|
1002
|
+
// ones are (re)placed below, so rebuild their entries here rather than
|
|
1003
|
+
// carry a pre-relocation position that would fail the isApplied check.
|
|
1004
|
+
const regeneratedFileIds = new Set(savedFiles.map((file) => file.id));
|
|
1005
|
+
const generationFrames = merged.placedFrames.filter(
|
|
1006
|
+
(placed) => !regeneratedFileIds.has(placed.fileId),
|
|
1007
|
+
);
|
|
1008
|
+
for (const placed of generationFrames) {
|
|
1009
|
+
occupiedRects.push(rectOf(placed.frame));
|
|
1010
|
+
}
|
|
1011
|
+
// Relocate target: right of every occupied (rotation-aware) rect.
|
|
1012
|
+
let nextX = occupiedRects.reduce(
|
|
1013
|
+
(right, rect) =>
|
|
1014
|
+
Math.max(right, rect.x + rect.width + GENERATED_FRAME_GAP),
|
|
1015
|
+
0,
|
|
1016
|
+
);
|
|
871
1017
|
for (const file of savedFiles) {
|
|
872
1018
|
const source = files.find(
|
|
873
1019
|
(candidate) => candidate.filename === file.filename,
|
|
@@ -875,18 +1021,50 @@ const generateDesignAction = defineAction({
|
|
|
875
1021
|
if (!source || !isRenderableDesignFile(source)) continue;
|
|
876
1022
|
const current = merged.canvasFrames[file.id] ?? {};
|
|
877
1023
|
if (
|
|
1024
|
+
preExistingFrameIds.has(file.id) &&
|
|
878
1025
|
current.x !== undefined &&
|
|
879
1026
|
current.y !== undefined &&
|
|
880
1027
|
current.width !== undefined &&
|
|
881
1028
|
current.height !== undefined
|
|
882
1029
|
) {
|
|
1030
|
+
// An explicit device request resizes the primary frame to the
|
|
1031
|
+
// requested viewport (preserving position/rotation); otherwise a
|
|
1032
|
+
// regenerated screen keeps its exact stored geometry.
|
|
1033
|
+
if (devices && devices.length > 0) {
|
|
1034
|
+
merged.canvasFrames[file.id] = {
|
|
1035
|
+
...current,
|
|
1036
|
+
width: viewport.width,
|
|
1037
|
+
height: viewport.height,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
883
1040
|
continue;
|
|
884
1041
|
}
|
|
1042
|
+
const width = current.width ?? viewport.width;
|
|
1043
|
+
const height = current.height ?? viewport.height;
|
|
1044
|
+
let x = current.x ?? nextX;
|
|
1045
|
+
let y = current.y ?? 0;
|
|
1046
|
+
// Bump a new screen clear of everything if its requested/default spot
|
|
1047
|
+
// overlaps (e.g. a second screen defaulting to the same origin). The
|
|
1048
|
+
// candidate carries its own rotation so a rotated placement is tested
|
|
1049
|
+
// by its real footprint, not its unrotated rectangle.
|
|
1050
|
+
const candidateRect = rectOf({
|
|
1051
|
+
x,
|
|
1052
|
+
y,
|
|
1053
|
+
width,
|
|
1054
|
+
height,
|
|
1055
|
+
rotation: current.rotation,
|
|
1056
|
+
});
|
|
1057
|
+
if (
|
|
1058
|
+
occupiedRects.some((rect) => framesOverlap(candidateRect, rect))
|
|
1059
|
+
) {
|
|
1060
|
+
x = nextX;
|
|
1061
|
+
y = 0;
|
|
1062
|
+
}
|
|
885
1063
|
const frame = {
|
|
886
|
-
x
|
|
887
|
-
y
|
|
888
|
-
width
|
|
889
|
-
height
|
|
1064
|
+
x,
|
|
1065
|
+
y,
|
|
1066
|
+
width,
|
|
1067
|
+
height,
|
|
890
1068
|
z: current.z ?? generationFrames.length,
|
|
891
1069
|
...(current.rotation === undefined
|
|
892
1070
|
? {}
|
|
@@ -898,14 +1076,34 @@ const generateDesignAction = defineAction({
|
|
|
898
1076
|
filename: file.filename,
|
|
899
1077
|
frame,
|
|
900
1078
|
});
|
|
901
|
-
|
|
1079
|
+
occupiedRects.push(rectOf(frame));
|
|
1080
|
+
nextX = Math.max(nextX, frame.x + frame.width + GENERATED_FRAME_GAP);
|
|
902
1081
|
}
|
|
903
1082
|
mergedData.canvasFrames = merged.canvasFrames;
|
|
904
1083
|
placedFrames = generationFrames;
|
|
905
|
-
|
|
1084
|
+
// An explicit `devices` request is authoritative: replace the design's
|
|
1085
|
+
// breakpoint set with the derived one (or drop it for a single device),
|
|
1086
|
+
// so regenerating e.g. as [mobile,tablet,desktop] can't silently retain
|
|
1087
|
+
// a stale narrower set. Without explicit devices, only seed a default
|
|
1088
|
+
// set when none exists — a plain content regen never clobbers the
|
|
1089
|
+
// user's own breakpoints.
|
|
1090
|
+
if (devices && devices.length > 0) {
|
|
1091
|
+
if (generatedBreakpointSet.length > 0) {
|
|
1092
|
+
mergedData.breakpointSet = {
|
|
1093
|
+
id: "generated-responsive",
|
|
1094
|
+
breakpoints: generatedBreakpointSet,
|
|
1095
|
+
};
|
|
1096
|
+
} else {
|
|
1097
|
+
delete mergedData.breakpointSet;
|
|
1098
|
+
}
|
|
1099
|
+
mergedData.breakpointSetUpdatedAt = updatedAt;
|
|
1100
|
+
} else if (
|
|
1101
|
+
generatedBreakpointSet.length > 0 &&
|
|
1102
|
+
!hasBreakpointSet(mergedData.breakpointSet)
|
|
1103
|
+
) {
|
|
906
1104
|
mergedData.breakpointSet = {
|
|
907
1105
|
id: "generated-responsive",
|
|
908
|
-
breakpoints:
|
|
1106
|
+
breakpoints: generatedBreakpointSet,
|
|
909
1107
|
};
|
|
910
1108
|
mergedData.breakpointSetUpdatedAt = updatedAt;
|
|
911
1109
|
}
|
|
@@ -940,7 +1138,32 @@ const generateDesignAction = defineAction({
|
|
|
940
1138
|
);
|
|
941
1139
|
}),
|
|
942
1140
|
);
|
|
943
|
-
|
|
1141
|
+
// For an explicit `devices` request, verify the persisted breakpoint
|
|
1142
|
+
// widths actually match the requested set (not merely that some set
|
|
1143
|
+
// exists), so a partial/stale write is retried rather than accepted.
|
|
1144
|
+
const currentBreakpointWidths = (
|
|
1145
|
+
Array.isArray(
|
|
1146
|
+
(current.breakpointSet as { breakpoints?: unknown })?.breakpoints,
|
|
1147
|
+
)
|
|
1148
|
+
? (
|
|
1149
|
+
current.breakpointSet as {
|
|
1150
|
+
breakpoints: Array<{ widthPx?: number }>;
|
|
1151
|
+
}
|
|
1152
|
+
).breakpoints
|
|
1153
|
+
: []
|
|
1154
|
+
)
|
|
1155
|
+
.map((bp) => bp.widthPx)
|
|
1156
|
+
.filter((w): w is number => typeof w === "number")
|
|
1157
|
+
.sort((a, b) => a - b);
|
|
1158
|
+
const expectedBreakpointWidths = generatedBreakpointSet
|
|
1159
|
+
.map((bp) => bp.widthPx)
|
|
1160
|
+
.sort((a, b) => a - b);
|
|
1161
|
+
const breakpointSetApplied =
|
|
1162
|
+
devices && devices.length > 0
|
|
1163
|
+
? jsonValuesEqual(currentBreakpointWidths, expectedBreakpointWidths)
|
|
1164
|
+
: generatedBreakpointSet.length === 0 ||
|
|
1165
|
+
hasBreakpointSet(current.breakpointSet);
|
|
1166
|
+
return framesApplied && breakpointSetApplied;
|
|
944
1167
|
},
|
|
945
1168
|
});
|
|
946
1169
|
|
|
@@ -34,7 +34,7 @@ const DEVICE_REGION_SIZE: Record<
|
|
|
34
34
|
> = {
|
|
35
35
|
mobile: { width: 390, height: 844 },
|
|
36
36
|
tablet: { width: 768, height: 1024 },
|
|
37
|
-
desktop: { width: 1440, height:
|
|
37
|
+
desktop: { width: 1440, height: 900 },
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
function regionSizeForScreen(screen: {
|
|
@@ -135,10 +135,11 @@ const requestedScreenSchema = z
|
|
|
135
135
|
.enum(["mobile", "tablet", "desktop"])
|
|
136
136
|
.optional()
|
|
137
137
|
.describe(
|
|
138
|
-
"Intended viewport for this screen. Defaults to desktop-sized
|
|
139
|
-
"omitted
|
|
140
|
-
"
|
|
141
|
-
"
|
|
138
|
+
"Intended viewport for this DISTINCT screen. Defaults to desktop-sized " +
|
|
139
|
+
"when omitted. Use 'mobile' or 'tablet' only for a genuinely " +
|
|
140
|
+
"device-specific screen (e.g. a mobile-only page), NOT to create " +
|
|
141
|
+
"per-device copies of the same page — those are breakpoint frames of " +
|
|
142
|
+
"one document, chosen with generate-design's `devices` param.",
|
|
142
143
|
),
|
|
143
144
|
width: z
|
|
144
145
|
.number()
|
|
@@ -168,15 +169,20 @@ const requestedScreenSchema = z
|
|
|
168
169
|
|
|
169
170
|
export default defineAction({
|
|
170
171
|
description:
|
|
171
|
-
"Start a multi-screen generation session on the Design canvas
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
"
|
|
177
|
-
"
|
|
178
|
-
"
|
|
179
|
-
"
|
|
172
|
+
"Start a multi-screen generation session on the Design canvas for " +
|
|
173
|
+
"genuinely DISTINCT screens — different pages or states such as Home, " +
|
|
174
|
+
"Dashboard, and Checkout — each saved as its own screen file. " +
|
|
175
|
+
"Do NOT use this to model device responsiveness: mobile/tablet/desktop " +
|
|
176
|
+
"variants of the SAME page are breakpoint frames of one document, not " +
|
|
177
|
+
"separate screen files. Choose those device frames with generate-design's " +
|
|
178
|
+
"`devices` param instead of adding one screen per device here. " +
|
|
179
|
+
"It assigns non-overlapping canvas regions and returns per-frame " +
|
|
180
|
+
"generation instructions including canvasFrame placements. The session " +
|
|
181
|
+
"state is agent-facing planning state consumed by generate-design and " +
|
|
182
|
+
"view-screen. Set a screen's deviceType ('mobile', 'tablet', or 'desktop') " +
|
|
183
|
+
"only for a genuinely device-specific distinct screen (e.g. a mobile-only " +
|
|
184
|
+
"page) so its canvas region matches the content; it defaults to desktop, " +
|
|
185
|
+
"or pass explicit width/height for a non-standard viewport. " +
|
|
180
186
|
"After this action, fan out calls to generate-design for each returned " +
|
|
181
187
|
"frame, passing the returned canvasFrame values to generate-design so " +
|
|
182
188
|
"screens appear in the infinite overview canvas.",
|
|
@@ -326,6 +332,37 @@ export default defineAction({
|
|
|
326
332
|
.map((file) => file.filename)
|
|
327
333
|
.filter((filename): filename is string => Boolean(filename)),
|
|
328
334
|
);
|
|
335
|
+
|
|
336
|
+
// Region packing starts at x:0, so offset the whole batch past screens
|
|
337
|
+
// already on the board — otherwise a follow-up session stacks its screens
|
|
338
|
+
// on top of existing ones (generate-design's per-frame collision check is
|
|
339
|
+
// only a fallback; offsetting here keeps the batch's own layout intact).
|
|
340
|
+
const existingDesignRows = await db
|
|
341
|
+
.select({ data: schema.designs.data })
|
|
342
|
+
.from(schema.designs)
|
|
343
|
+
.where(eq(schema.designs.id, designId));
|
|
344
|
+
let boardRightEdge = 0;
|
|
345
|
+
try {
|
|
346
|
+
const raw = existingDesignRows[0]?.data;
|
|
347
|
+
const parsed = raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
|
|
348
|
+
const frames = (parsed.canvasFrames ?? {}) as Record<
|
|
349
|
+
string,
|
|
350
|
+
{ x?: number; width?: number }
|
|
351
|
+
>;
|
|
352
|
+
for (const frame of Object.values(frames)) {
|
|
353
|
+
const x = typeof frame?.x === "number" ? frame.x : 0;
|
|
354
|
+
const width = typeof frame?.width === "number" ? frame.width : 0;
|
|
355
|
+
boardRightEdge = Math.max(
|
|
356
|
+
boardRightEdge,
|
|
357
|
+
x + width + DEFAULT_ASSIGNED_REGION_GAP,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
// Malformed data — fall back to the origin.
|
|
362
|
+
}
|
|
363
|
+
if (boardRightEdge > 0) {
|
|
364
|
+
for (const region of regions) region.x += boardRightEdge;
|
|
365
|
+
}
|
|
329
366
|
// Dedupe any filename (explicit or auto-generated) so two screens can
|
|
330
367
|
// never resolve to the same target file, and so no target collides with
|
|
331
368
|
// an existing screen — otherwise generate-design silently overwrites the
|
|
@@ -29,10 +29,13 @@ const MOBILE_HEIGHT = 844;
|
|
|
29
29
|
const TABLET_WIDTH = 768;
|
|
30
30
|
const TABLET_HEIGHT = 1024;
|
|
31
31
|
const DESKTOP_WIDTH = 1440;
|
|
32
|
-
const DESKTOP_HEIGHT =
|
|
33
|
-
|
|
32
|
+
const DESKTOP_HEIGHT = 900;
|
|
33
|
+
// Desktop-base default: the primary/base frame is Desktop (1440), so the
|
|
34
|
+
// breakpoint set is Mobile only. The primary width is never included and no
|
|
35
|
+
// tablet is auto-added, matching generate-design's device derivation.
|
|
36
|
+
const DEFAULT_RESPONSIVE_BREAKPOINTS = [MOBILE_WIDTH].map((widthPx) => ({
|
|
34
37
|
id: `generated-${widthPx}`,
|
|
35
|
-
label:
|
|
38
|
+
label: "Mobile",
|
|
36
39
|
widthPx,
|
|
37
40
|
prefix: widthToPrefix(widthPx),
|
|
38
41
|
}));
|
|
@@ -66,6 +66,7 @@ import { zoomBridgeScript } from "../../../.generated/bridge/zoom.generated";
|
|
|
66
66
|
import { isTrustedCanvasBridgeMessage } from "./bridge-security";
|
|
67
67
|
import { captureAnnotatedScreenshot } from "./design-canvas/annotation-snapshot";
|
|
68
68
|
import { submitDesignAnnotations } from "./design-canvas/annotation-submit";
|
|
69
|
+
import { appendContentSizeReporter } from "./design-canvas/content-size-report";
|
|
69
70
|
import {
|
|
70
71
|
getScreenContentPointFromClient,
|
|
71
72
|
getZoomToCursorScrollDelta,
|
|
@@ -2181,6 +2182,13 @@ export function DesignCanvas({
|
|
|
2181
2182
|
].join("");
|
|
2182
2183
|
frameDocument = `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${frameStyle}</head><body>${iframeRenderContent}${bridgeToInject}</body></html>`;
|
|
2183
2184
|
}
|
|
2185
|
+
// Overview frames report their own content height so the canvas can
|
|
2186
|
+
// content-fit them (Framer-style). Embedded (overview) frames only — a
|
|
2187
|
+
// focused single-screen view fills the viewport and must keep native
|
|
2188
|
+
// 100vh/min-h-screen, which the reporter's guard would otherwise pin.
|
|
2189
|
+
if (isEmbeddedFrame) {
|
|
2190
|
+
frameDocument = appendContentSizeReporter(frameDocument);
|
|
2191
|
+
}
|
|
2184
2192
|
return injectSessionReplayIframeBootstrap(frameDocument);
|
|
2185
2193
|
// editorChromeScaleX/Y are intentionally NOT deps: they only seed the initial
|
|
2186
2194
|
// baked chrome scale. Live zoom updates flow through the set-editor-chrome-scale
|