@agent-native/core 0.131.2 → 0.131.4
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 +34 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/types.ts +6 -0
- package/corpus/core/src/action.ts +13 -0
- package/corpus/core/src/client/AgentPanel.tsx +58 -34
- package/corpus/core/src/localization/default-messages.ts +6 -0
- package/corpus/core/src/server/agent-capabilities.ts +43 -8
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +63 -1
- package/corpus/templates/analytics/AGENTS.md +5 -0
- package/corpus/templates/analytics/actions/hubspot-deals.ts +1 -0
- package/corpus/templates/analytics/actions/hubspot-records.ts +1 -0
- package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +2 -0
- package/corpus/templates/calendar/actions/get-event.ts +1 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +1 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +7 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +14 -19
- package/corpus/templates/calendar/app/hooks/use-events.ts +7 -0
- package/corpus/templates/calendar/app/lib/event-form-utils.ts +13 -0
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +21 -14
- package/corpus/templates/calendar/changelog/2026-07-29-unnamed-events-now-use-a-title-placeholder.md +6 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +2 -0
- package/corpus/templates/calendar/server/lib/ical-fetcher.ts +1 -0
- package/corpus/templates/calendar/shared/api.ts +2 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +3 -1
- package/corpus/templates/design/actions/get-design.ts +11 -3
- package/corpus/templates/design/actions/present-design-variants.ts +62 -5
- package/corpus/templates/design/app/components/design/BreakpointBar.tsx +64 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +199 -25
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +160 -46
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +7 -1
- package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +59 -12
- package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +1 -1
- package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +34 -29
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +2 -2
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +28 -10
- package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +15 -28
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +4 -3
- package/corpus/templates/design/app/components/design/multi-screen/chrome-transitions.ts +12 -8
- package/corpus/templates/design/app/components/design/multi-screen/culling.ts +34 -9
- package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +13 -27
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +8 -1
- package/corpus/templates/design/app/components/design/multi-screen/zoom-gesture.ts +95 -0
- package/corpus/templates/design/app/i18n-data.ts +121 -10
- package/corpus/templates/design/app/pages/design-editor/data-operations.ts +45 -0
- package/corpus/templates/design/app/pages/design-editor/layout-operations.ts +59 -0
- package/corpus/templates/design/app/pages/design-editor/text-edit-utils.ts +115 -40
- package/corpus/templates/design/changelog/2026-07-28-generated-design-variants-no-longer-overlap-each-other-on-th.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-auto-layout-direction-icons-show-an-arrow.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-breakpoints-no-longer-overlap-neighbouring-screens.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-canvas-is-more-responsive-when-selecting-and-adding-frames.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-canvas-zoom-is-smoother-and-labels-hold-their-size.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-double-click-a-frame-now-drills-into-its-layers.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-export-preview-shows-the-real-render.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-figma-parity-shortcuts.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-more-breakpoint-width-presets.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-resize-a-selected-frame-to-a-device-preset.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-text-tool-reaches-the-right-surface.md +6 -0
- package/corpus/templates/design/shared/canvas-frames.ts +23 -0
- package/corpus/templates/design/shared/code-layer.ts +54 -2
- package/corpus/templates/design/shared/responsive-frame-layout.ts +55 -0
- package/corpus/templates/slides/.agents/skills/analytics-data-for-decks/SKILL.md +48 -0
- package/corpus/templates/slides/AGENTS.md +3 -0
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +72 -8
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/changelog/2026-07-29-google-slides-exports-can-connect-your-google-account-direct.md +6 -0
- package/corpus/templates/slides/server/handlers/google-docs-auth.ts +4 -2
- package/dist/a2a/types.d.ts +6 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/action.d.ts +13 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +16 -5
- package/dist/client/AgentPanel.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/localization/default-messages.d.ts +6 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +6 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-capabilities.d.ts.map +1 -1
- package/dist/server/agent-capabilities.js +32 -3
- package/dist/server/agent-capabilities.js.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.d.ts +17 -1
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +50 -1
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
- package/src/a2a/types.ts +6 -0
- package/src/action.ts +13 -0
- package/src/client/AgentPanel.tsx +58 -34
- package/src/localization/default-messages.ts +6 -0
- package/src/server/agent-capabilities.ts +43 -8
- package/src/server/agent-chat/action-filters-a2a.ts +63 -1
|
@@ -86,8 +86,11 @@ import { useViewPreferences } from "@/hooks/use-view-preferences";
|
|
|
86
86
|
import { resolveEventAccountEmail } from "@/lib/event-account-selection";
|
|
87
87
|
import { getGoogleEventColorHex } from "@/lib/event-colors";
|
|
88
88
|
import {
|
|
89
|
+
buildEventTitleUpdate,
|
|
89
90
|
dateTimeInTimezoneToIso,
|
|
91
|
+
getEditableEventTitle,
|
|
90
92
|
getLocalTimezone,
|
|
93
|
+
UNNAMED_EVENT_TITLE,
|
|
91
94
|
} from "@/lib/event-form-utils";
|
|
92
95
|
import { buildDeleteEventMutationInput } from "@/lib/event-mutation-inputs";
|
|
93
96
|
import { getLocationSuggestions } from "@/lib/location-suggestions";
|
|
@@ -204,11 +207,15 @@ function draftToCalendarEvent(
|
|
|
204
207
|
fallbackDate: Date,
|
|
205
208
|
): CalendarEvent {
|
|
206
209
|
const { start, end } = draftRange(draft, fallbackDate);
|
|
210
|
+
const editableTitle = draft.title?.trim() ?? "";
|
|
207
211
|
return {
|
|
208
212
|
id: calendarDraftEventId(draft.id),
|
|
209
213
|
title:
|
|
210
|
-
|
|
211
|
-
(draft.eventType === "outOfOffice"
|
|
214
|
+
editableTitle ||
|
|
215
|
+
(draft.eventType === "outOfOffice"
|
|
216
|
+
? "Out of office"
|
|
217
|
+
: UNNAMED_EVENT_TITLE),
|
|
218
|
+
titleIsGenerated: !editableTitle,
|
|
212
219
|
description: draft.description ?? "",
|
|
213
220
|
start: start.toISOString(),
|
|
214
221
|
end: end.toISOString(),
|
|
@@ -650,14 +657,10 @@ export default function CalendarView() {
|
|
|
650
657
|
setEventDraft(draft);
|
|
651
658
|
persistCalendarDraft(draft);
|
|
652
659
|
}
|
|
653
|
-
const
|
|
660
|
+
const editableTitle = draft.title?.trim() ?? "";
|
|
654
661
|
const eventType = draft.eventType ?? "default";
|
|
655
662
|
const title =
|
|
656
|
-
|
|
657
|
-
? trimmedTitle
|
|
658
|
-
: eventType === "outOfOffice"
|
|
659
|
-
? "Out of office"
|
|
660
|
-
: "";
|
|
663
|
+
editableTitle || (eventType === "outOfOffice" ? "Out of office" : "");
|
|
661
664
|
if (!title && !isSlotDraftId(draftId)) {
|
|
662
665
|
committingDraftIdsRef.current.delete(draftId);
|
|
663
666
|
toast.error(t("calendarView.addTitleBeforeCreate"));
|
|
@@ -703,6 +706,7 @@ export default function CalendarView() {
|
|
|
703
706
|
const payload: Parameters<typeof createEvent.mutate>[0] = {
|
|
704
707
|
_tempId: eventId,
|
|
705
708
|
title,
|
|
709
|
+
titleIsGenerated: !editableTitle,
|
|
706
710
|
description:
|
|
707
711
|
eventType === "outOfOffice" ? "" : (draft.description ?? ""),
|
|
708
712
|
start: semanticFullDay ? draft.start! : start.toISOString(),
|
|
@@ -1359,8 +1363,9 @@ export default function CalendarView() {
|
|
|
1359
1363
|
const handleQuickEditSave = useCallback(
|
|
1360
1364
|
async (eventId: string, title: string, accountEmail?: string) => {
|
|
1361
1365
|
setQuickEditEventId(null);
|
|
1366
|
+
const trimmedTitle = title.trim();
|
|
1362
1367
|
if (calendarDraftIdFromEventId(eventId)) {
|
|
1363
|
-
updateDraftEvent(eventId, { title:
|
|
1368
|
+
updateDraftEvent(eventId, { title: trimmedTitle });
|
|
1364
1369
|
return;
|
|
1365
1370
|
}
|
|
1366
1371
|
setQuickEditTempIds((current) => {
|
|
@@ -1368,9 +1373,9 @@ export default function CalendarView() {
|
|
|
1368
1373
|
const { [eventId]: _removed, ...next } = current;
|
|
1369
1374
|
return next;
|
|
1370
1375
|
});
|
|
1371
|
-
if (
|
|
1376
|
+
if (trimmedTitle) {
|
|
1372
1377
|
const event = events.find((e) => e.id === eventId);
|
|
1373
|
-
const updates =
|
|
1378
|
+
const updates = buildEventTitleUpdate(trimmedTitle);
|
|
1374
1379
|
const guestNotification = event
|
|
1375
1380
|
? await promptGuestNotification({
|
|
1376
1381
|
event,
|
|
@@ -1392,12 +1397,14 @@ export default function CalendarView() {
|
|
|
1392
1397
|
|
|
1393
1398
|
const handleTitleSave = useCallback(
|
|
1394
1399
|
async (eventId: string, title: string, accountEmail?: string) => {
|
|
1400
|
+
const trimmedTitle = title.trim();
|
|
1401
|
+
if (!trimmedTitle) return;
|
|
1395
1402
|
if (calendarDraftIdFromEventId(eventId)) {
|
|
1396
|
-
updateDraftEvent(eventId, { title });
|
|
1403
|
+
updateDraftEvent(eventId, { title: trimmedTitle });
|
|
1397
1404
|
return;
|
|
1398
1405
|
}
|
|
1399
1406
|
const event = events.find((e) => e.id === eventId);
|
|
1400
|
-
const updates =
|
|
1407
|
+
const updates = buildEventTitleUpdate(trimmedTitle);
|
|
1401
1408
|
const guestNotification = event
|
|
1402
1409
|
? await promptGuestNotification({
|
|
1403
1410
|
event,
|
|
@@ -1430,7 +1437,7 @@ export default function CalendarView() {
|
|
|
1430
1437
|
});
|
|
1431
1438
|
// Delete the event if title was never set
|
|
1432
1439
|
const ev = events.find((e) => e.id === eventId);
|
|
1433
|
-
if (!ev || ev.
|
|
1440
|
+
if (!ev || !getEditableEventTitle(ev).trim()) {
|
|
1434
1441
|
deleteEvent.mutate(
|
|
1435
1442
|
buildDeleteEventMutationInput(
|
|
1436
1443
|
{
|
|
@@ -881,6 +881,7 @@ export async function listEvents(
|
|
|
881
881
|
return {
|
|
882
882
|
id: `google-${event.id}`,
|
|
883
883
|
title: event.summary || "Untitled",
|
|
884
|
+
titleIsGenerated: !event.summary,
|
|
884
885
|
description: event.description || "",
|
|
885
886
|
start: event.start?.dateTime || event.start?.date || "",
|
|
886
887
|
end: event.end?.dateTime || event.end?.date || "",
|
|
@@ -1153,6 +1154,7 @@ export async function getEvent(
|
|
|
1153
1154
|
return {
|
|
1154
1155
|
id: `google-${event.id}`,
|
|
1155
1156
|
title: event.summary || "Untitled",
|
|
1157
|
+
titleIsGenerated: !event.summary,
|
|
1156
1158
|
description: event.description || "",
|
|
1157
1159
|
start: event.start?.dateTime || event.start?.date || "",
|
|
1158
1160
|
end: event.end?.dateTime || event.end?.date || "",
|
|
@@ -8199,7 +8199,7 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
8199
8199
|
if (e.data.type === "agent-native:text-edit-status") {
|
|
8200
8200
|
var textEditStatusCorrelationId = typeof e.data.correlationId === "string" ? e.data.correlationId : "";
|
|
8201
8201
|
var textEditStatusNodeId = typeof e.data.nodeId === "string" ? e.data.nodeId : "";
|
|
8202
|
-
var textEditStatus =
|
|
8202
|
+
var textEditStatus = "missing";
|
|
8203
8203
|
if (textEditStatusNodeId) {
|
|
8204
8204
|
var escapedTextEditStatusNodeId = textEditStatusNodeId.replace(/\\\\/g, "\\\\\\\\").replace(/"/g, '\\\\"');
|
|
8205
8205
|
var textEditStatusNode = document.querySelector(
|
|
@@ -8212,6 +8212,8 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
8212
8212
|
textEditStatus = "active";
|
|
8213
8213
|
} else if (textEditStatusNode && (textEditStatusNode.textContent ?? "").trim().length > 0) {
|
|
8214
8214
|
textEditStatus = "done";
|
|
8215
|
+
} else if (textEditStatusNode) {
|
|
8216
|
+
textEditStatus = false;
|
|
8215
8217
|
}
|
|
8216
8218
|
}
|
|
8217
8219
|
window.parent.postMessage(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { resolveAccess } from "@agent-native/core/sharing";
|
|
3
|
-
import { eq } from "drizzle-orm";
|
|
3
|
+
import { asc, eq } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
@@ -26,11 +26,19 @@ export default defineAction({
|
|
|
26
26
|
const row = access.resource;
|
|
27
27
|
const db = getDb();
|
|
28
28
|
|
|
29
|
-
// Fetch associated files
|
|
29
|
+
// Fetch associated files in a stable order. This array feeds the overview
|
|
30
|
+
// canvas's screen stack and each screen's index within its layout group, so
|
|
31
|
+
// unordered rows (Postgres returns heap order, which an UPDATE can change)
|
|
32
|
+
// meant the same design could lay itself out differently on two loads.
|
|
33
|
+
// Note this is deterministic, not creation-ordered: files written in one
|
|
34
|
+
// batch share a `createdAt` to the millisecond and fall back to the id
|
|
35
|
+
// tiebreak. Nothing may depend on the index matching the order a generator
|
|
36
|
+
// wrote in — see the order-independence case in variant-lineup.test.ts.
|
|
30
37
|
const files = await db
|
|
31
38
|
.select()
|
|
32
39
|
.from(schema.designFiles)
|
|
33
|
-
.where(eq(schema.designFiles.designId, id))
|
|
40
|
+
.where(eq(schema.designFiles.designId, id))
|
|
41
|
+
.orderBy(asc(schema.designFiles.createdAt), asc(schema.designFiles.id));
|
|
34
42
|
|
|
35
43
|
return {
|
|
36
44
|
id: row.id,
|
|
@@ -16,11 +16,16 @@ import { getDb, schema } from "../server/db/index.js";
|
|
|
16
16
|
import { mutateDesignData } from "../server/lib/design-data-mutation.js";
|
|
17
17
|
import {
|
|
18
18
|
mergeCanvasFramePlacements,
|
|
19
|
+
nextFreeCanvasRowY,
|
|
19
20
|
type CanvasFramePlacement,
|
|
20
21
|
} from "../shared/canvas-frames.js";
|
|
21
22
|
import { isUniqueConstraintViolation } from "../shared/db-conflict.js";
|
|
22
23
|
import { assertDesignHtmlWellFormed } from "../shared/html-integrity.js";
|
|
23
24
|
import { widthToPrefix } from "../shared/responsive-classes.js";
|
|
25
|
+
import {
|
|
26
|
+
getResponsiveGroupWidth,
|
|
27
|
+
visibleBreakpointWidths,
|
|
28
|
+
} from "../shared/responsive-frame-layout.js";
|
|
24
29
|
import { annotateScreenHtmlForPersist } from "../shared/screen-annotation.js";
|
|
25
30
|
|
|
26
31
|
const VARIANT_GAP = 96;
|
|
@@ -735,10 +740,24 @@ ${compact ? ".sidebar { padding: 16px; } .nav { grid-template-columns: repeat(2,
|
|
|
735
740
|
</html>`;
|
|
736
741
|
}
|
|
737
742
|
|
|
738
|
-
|
|
743
|
+
/**
|
|
744
|
+
* Lays the generated directions out in rows of up to three.
|
|
745
|
+
*
|
|
746
|
+
* Each cell reserves the screen's WHOLE painted footprint, not just its
|
|
747
|
+
* primary frame: this action also installs a design-wide breakpoint set, and
|
|
748
|
+
* the overview paints one preview per breakpoint to the right of every primary
|
|
749
|
+
* frame. Spacing by `width + VARIANT_GAP` alone drops the next direction on top
|
|
750
|
+
* of the previous one's breakpoint row.
|
|
751
|
+
*/
|
|
752
|
+
function placeVariantScreens(
|
|
753
|
+
screens: VariantScreen[],
|
|
754
|
+
breakpointWidths: readonly number[],
|
|
755
|
+
/** Y to start the lineup at, so an additional set clears the existing one. */
|
|
756
|
+
originY = 0,
|
|
757
|
+
) {
|
|
739
758
|
const placements: CanvasFramePlacement[] = [];
|
|
740
759
|
const columns = Math.min(MAX_COLUMNS, Math.max(1, screens.length));
|
|
741
|
-
let rowY =
|
|
760
|
+
let rowY = originY;
|
|
742
761
|
|
|
743
762
|
for (let rowStart = 0; rowStart < screens.length; rowStart += columns) {
|
|
744
763
|
const row = screens.slice(rowStart, rowStart + columns);
|
|
@@ -755,7 +774,17 @@ function placeVariantScreens(screens: VariantScreen[]) {
|
|
|
755
774
|
height: screen.height,
|
|
756
775
|
z: rowStart + offset,
|
|
757
776
|
});
|
|
758
|
-
x +=
|
|
777
|
+
x +=
|
|
778
|
+
getResponsiveGroupWidth({
|
|
779
|
+
primaryWidth: screen.width,
|
|
780
|
+
// Frames are placed at their natural device width, so the breakpoint
|
|
781
|
+
// previews beside them are drawn unscaled.
|
|
782
|
+
scale: 1,
|
|
783
|
+
visibleWidths: visibleBreakpointWidths(
|
|
784
|
+
breakpointWidths,
|
|
785
|
+
screen.width,
|
|
786
|
+
),
|
|
787
|
+
}) + VARIANT_GAP;
|
|
759
788
|
rowHeight = Math.max(rowHeight, screen.height);
|
|
760
789
|
}
|
|
761
790
|
|
|
@@ -765,6 +794,22 @@ function placeVariantScreens(screens: VariantScreen[]) {
|
|
|
765
794
|
return placements;
|
|
766
795
|
}
|
|
767
796
|
|
|
797
|
+
/** Widths the overview will paint beside each primary frame once this call
|
|
798
|
+
* settles: the design's own set when it has one, otherwise the set this action
|
|
799
|
+
* is about to install. */
|
|
800
|
+
function effectiveBreakpointWidths(currentBreakpointSet: unknown): number[] {
|
|
801
|
+
const breakpoints = hasBreakpointSet(currentBreakpointSet)
|
|
802
|
+
? ((currentBreakpointSet as { breakpoints: Array<{ widthPx?: unknown }> })
|
|
803
|
+
.breakpoints ?? [])
|
|
804
|
+
: DEFAULT_RESPONSIVE_BREAKPOINTS;
|
|
805
|
+
return breakpoints
|
|
806
|
+
.map((breakpoint) => breakpoint.widthPx)
|
|
807
|
+
.filter(
|
|
808
|
+
(widthPx): widthPx is number =>
|
|
809
|
+
typeof widthPx === "number" && Number.isFinite(widthPx) && widthPx > 0,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
768
813
|
export default defineAction({
|
|
769
814
|
description:
|
|
770
815
|
"Present generated design directions as normal screens on the Design " +
|
|
@@ -933,13 +978,25 @@ export default defineAction({
|
|
|
933
978
|
});
|
|
934
979
|
}
|
|
935
980
|
|
|
936
|
-
const placements = placeVariantScreens(screens);
|
|
937
981
|
await mutateDesignData({
|
|
938
982
|
designId,
|
|
939
983
|
mutate: (current, { updatedAt }) => {
|
|
984
|
+
// Placement depends on the breakpoint set in effect after this call, so
|
|
985
|
+
// it is resolved here (inside the compare-and-set body) rather than
|
|
986
|
+
// against a design snapshot that a concurrent write may have moved on
|
|
987
|
+
// from.
|
|
940
988
|
const mergedFrames = mergeCanvasFramePlacements({
|
|
941
989
|
existing: current.canvasFrames,
|
|
942
|
-
placements
|
|
990
|
+
placements: placeVariantScreens(
|
|
991
|
+
screens,
|
|
992
|
+
effectiveBreakpointWidths(current.breakpointSet),
|
|
993
|
+
// Start below what is already on the board. The set being written is
|
|
994
|
+
// excluded so re-running the same set lands where it was instead of
|
|
995
|
+
// marching further down each time.
|
|
996
|
+
nextFreeCanvasRowY(current.canvasFrames, VARIANT_GAP, {
|
|
997
|
+
ignoreFileIds: screens.map((screen) => screen.id),
|
|
998
|
+
}),
|
|
999
|
+
),
|
|
943
1000
|
resolveFileId: (placement) => placement.fileId,
|
|
944
1001
|
});
|
|
945
1002
|
const previousMetadata = isRecord(current.screenMetadata)
|
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
import { Switch } from "@/components/ui/switch";
|
|
27
27
|
import { cn } from "@/lib/utils";
|
|
28
28
|
|
|
29
|
+
import { FRAME_SIZE_PRESET_CATEGORIES } from "./inspector/frame-size-presets";
|
|
30
|
+
|
|
29
31
|
/** Framer's default breakpoint widths, offered by the "+" affordance. */
|
|
30
32
|
export const FRAMER_BREAKPOINT_PRESETS: ReadonlyArray<{
|
|
31
33
|
labelKey: "desktop" | "tablet" | "phone";
|
|
@@ -45,6 +47,35 @@ export function availableBreakpointPresets(
|
|
|
45
47
|
);
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Device widths offered beyond the three Framer defaults, drawn from the frame
|
|
52
|
+
* tool's own preset catalog so both surfaces agree on what a "Tablet" is.
|
|
53
|
+
*
|
|
54
|
+
* Without these the "+" popover empties out as soon as Desktop/Tablet/Phone are
|
|
55
|
+
* added — `availableBreakpointPresets` filters by exact width — leaving a bare
|
|
56
|
+
* number input and no way to pick a real device size. Only width matters for a
|
|
57
|
+
* breakpoint, so widths are de-duplicated (many devices share one) and the
|
|
58
|
+
* first device at each width names it. Device names are untranslated product
|
|
59
|
+
* literals, matching frame-size-presets' documented policy.
|
|
60
|
+
*/
|
|
61
|
+
export function extraBreakpointWidthPresets(
|
|
62
|
+
existingWidths: readonly number[],
|
|
63
|
+
): Array<{ name: string; widthPx: number }> {
|
|
64
|
+
const taken = new Set<number>(existingWidths);
|
|
65
|
+
FRAMER_BREAKPOINT_PRESETS.forEach((preset) => taken.add(preset.widthPx));
|
|
66
|
+
const seen = new Set<number>();
|
|
67
|
+
const out: Array<{ name: string; widthPx: number }> = [];
|
|
68
|
+
for (const category of FRAME_SIZE_PRESET_CATEGORIES) {
|
|
69
|
+
for (const preset of category.presets) {
|
|
70
|
+
if (taken.has(preset.width) || seen.has(preset.width)) continue;
|
|
71
|
+
if (preset.width < 320 || preset.width > 3840) continue;
|
|
72
|
+
seen.add(preset.width);
|
|
73
|
+
out.push({ name: preset.name, widthPx: preset.width });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out.sort((a, b) => b.widthPx - a.widthPx);
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
/** Default English label for a preset/custom width (used for add-breakpoint). */
|
|
49
80
|
export function breakpointLabelForWidth(widthPx: number): string {
|
|
50
81
|
if (widthPx >= 1024) return "Desktop";
|
|
@@ -128,6 +159,7 @@ export function BreakpointDeviceControl({
|
|
|
128
159
|
const ordered = [...breakpoints].sort((a, b) => b.widthPx - a.widthPx);
|
|
129
160
|
const existingWidths = ordered.map((bp) => bp.widthPx);
|
|
130
161
|
const presets = availableBreakpointPresets(existingWidths);
|
|
162
|
+
const devicePresets = extraBreakpointWidthPresets(existingWidths);
|
|
131
163
|
const baseActive = activeWidthPx === undefined;
|
|
132
164
|
|
|
133
165
|
const submitCustomWidth = () => {
|
|
@@ -323,7 +355,38 @@ export function BreakpointDeviceControl({
|
|
|
323
355
|
</span>
|
|
324
356
|
</button>
|
|
325
357
|
))}
|
|
326
|
-
{
|
|
358
|
+
{devicePresets.length > 0 ? (
|
|
359
|
+
<>
|
|
360
|
+
{presets.length > 0 ? (
|
|
361
|
+
<div className="my-1 h-px bg-border" />
|
|
362
|
+
) : null}
|
|
363
|
+
<div className="max-h-48 overflow-y-auto overscroll-contain">
|
|
364
|
+
{devicePresets.map((preset) => (
|
|
365
|
+
<button
|
|
366
|
+
key={preset.widthPx}
|
|
367
|
+
type="button"
|
|
368
|
+
className="flex w-full cursor-pointer items-center justify-between gap-2 rounded-md px-2 py-1.5 text-left !text-[12px] hover:bg-muted"
|
|
369
|
+
onClick={() => {
|
|
370
|
+
onAdd(
|
|
371
|
+
preset.widthPx,
|
|
372
|
+
breakpointLabelForWidth(preset.widthPx),
|
|
373
|
+
);
|
|
374
|
+
setAddOpen(false);
|
|
375
|
+
}}
|
|
376
|
+
>
|
|
377
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
378
|
+
<DeviceIcon widthPx={preset.widthPx} />
|
|
379
|
+
<span className="truncate">{preset.name}</span>
|
|
380
|
+
</span>
|
|
381
|
+
<span className="shrink-0 tabular-nums text-muted-foreground/60">
|
|
382
|
+
{preset.widthPx}
|
|
383
|
+
</span>
|
|
384
|
+
</button>
|
|
385
|
+
))}
|
|
386
|
+
</div>
|
|
387
|
+
</>
|
|
388
|
+
) : null}
|
|
389
|
+
{presets.length > 0 || devicePresets.length > 0 ? (
|
|
327
390
|
<div className="my-1 h-px bg-border" />
|
|
328
391
|
) : null}
|
|
329
392
|
<form
|