@agent-native/core 0.136.4 → 0.137.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.
Files changed (170) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +53 -0
  3. package/corpus/templates/clips/actions/import-loom-recording.ts +7 -0
  4. package/corpus/templates/clips/actions/lib/loom-import-job.ts +24 -4
  5. package/corpus/templates/clips/app/components/import-menu.tsx +109 -0
  6. package/corpus/templates/clips/app/components/library/library-grid.tsx +8 -18
  7. package/corpus/templates/clips/app/components/library/library-layout.tsx +14 -50
  8. package/corpus/templates/clips/app/components/library/sort-menu.tsx +20 -6
  9. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +10 -34
  10. package/corpus/templates/clips/app/routes/_app.library._index.tsx +0 -2
  11. package/corpus/templates/clips/changelog/2026-08-03-restart-during-a-recording-now-immediately-starts-a-fresh-ta.md +6 -0
  12. package/corpus/templates/clips/changelog/2026-08-04-cleaner-library-controls-with-an-icon-only-sort-button.md +6 -0
  13. package/corpus/templates/clips/changelog/2026-08-04-upload-and-loom-imports-now-live-under-one-import-menu.md +6 -0
  14. package/corpus/templates/clips/desktop/src/app.tsx +81 -30
  15. package/corpus/templates/clips/desktop/src/lib/recorder.ts +436 -158
  16. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +14 -1
  17. package/corpus/templates/clips/server/plugins/auth.ts +9 -0
  18. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +13 -0
  19. package/corpus/templates/content/actions/_database-utils.ts +6 -0
  20. package/corpus/templates/content/actions/bind-content-database-source-field.ts +273 -97
  21. package/corpus/templates/content/actions/change-content-database-source-role.ts +65 -29
  22. package/corpus/templates/content/actions/configure-document-property.ts +113 -32
  23. package/corpus/templates/content/actions/delete-content-database.ts +5 -7
  24. package/corpus/templates/content/actions/delete-document-property.ts +109 -62
  25. package/corpus/templates/content/actions/delete-document.ts +178 -36
  26. package/corpus/templates/content/actions/duplicate-database-item.ts +27 -0
  27. package/corpus/templates/content/actions/duplicate-database-items.ts +31 -1
  28. package/corpus/templates/content/actions/permanently-delete-document.ts +4 -6
  29. package/corpus/templates/content/actions/remove-database-items.ts +13 -0
  30. package/corpus/templates/content/actions/set-document-property.ts +69 -1
  31. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +814 -0
  32. package/corpus/templates/content/parity/matrix.md +1 -1
  33. package/corpus/templates/content/parity/matrix.ts +1 -0
  34. package/corpus/templates/content/server/db/schema.ts +27 -0
  35. package/corpus/templates/content/server/plugins/db.ts +24 -0
  36. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +122 -114
  37. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
  38. package/corpus/templates/design/app/components/design/EditPanel.tsx +68 -34
  39. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +78 -4
  40. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +8 -0
  41. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +5 -2
  42. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +10 -9
  43. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +31 -69
  44. package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +80 -106
  45. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
  46. package/corpus/templates/design/app/hooks/use-editor-preferences.ts +75 -0
  47. package/corpus/templates/design/app/i18n-data.ts +8 -0
  48. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +61 -0
  49. package/corpus/templates/design/app/pages/design-editor/editor-preferences.ts +98 -0
  50. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +491 -0
  51. package/corpus/templates/design/app/pages/design-editor/paste-placement.ts +133 -0
  52. package/corpus/templates/design/app/pages/design-editor/png-export-render.ts +8 -0
  53. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -12
  54. package/corpus/templates/design/changelog/2026-08-03-arrow-keys-now-reorder-a-layer-inside-an-auto-layout-frame.md +6 -0
  55. package/corpus/templates/design/changelog/2026-08-03-auto-layout-flow-icons-show-the-direction-at-a-glance-instea.md +6 -0
  56. package/corpus/templates/design/changelog/2026-08-03-auto-layout-icons-share-one-outlined-square-style-so-flow-ga.md +6 -0
  57. package/corpus/templates/design/changelog/2026-08-03-export-has-a-named-preview-section-you-can-expand-replacing-.md +6 -0
  58. package/corpus/templates/design/changelog/2026-08-03-export-preview-shows-the-selected-element-instead-of-the-who.md +6 -0
  59. package/corpus/templates/design/changelog/2026-08-03-inspector-sections-show-an-expand-indicator-so-it-is-clear-w.md +6 -0
  60. package/corpus/templates/design/changelog/2026-08-03-paste-lands-inside-a-selected-frame-instead-of-beside-it.md +6 -0
  61. package/corpus/templates/design/changelog/2026-08-03-the-add-breakpoint-button-sits-outside-the-breakpoint-select.md +6 -0
  62. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-keep-space-below-the-continu.md +6 -0
  63. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-share-the-chat-panel-s-backg.md +6 -0
  64. package/corpus/templates/design/changelog/2026-08-03-the-export-panel-no-longer-stacks-a-duplicate-export-block-o.md +6 -0
  65. package/corpus/templates/design/changelog/2026-08-03-the-text-tool-uses-a-t-icon-matching-the-letter-it-inserts.md +6 -0
  66. package/corpus/templates/factory/.env.example +17 -0
  67. package/corpus/templates/factory/AGENTS.md +27 -17
  68. package/corpus/templates/factory/README.md +19 -9
  69. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +90 -0
  70. package/corpus/templates/factory/actions/get-triage-config.ts +14 -3
  71. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +386 -0
  72. package/corpus/templates/factory/actions/list-factory-automations.ts +53 -0
  73. package/corpus/templates/factory/actions/list-triage-items.ts +7 -2
  74. package/corpus/templates/factory/actions/poll-github-sources.ts +203 -0
  75. package/corpus/templates/factory/actions/poll-sentry-errors.ts +161 -0
  76. package/corpus/templates/factory/actions/poll-slack-channel.ts +4 -2
  77. package/corpus/templates/factory/actions/run-factory-automation.ts +42 -0
  78. package/corpus/templates/factory/actions/save-factory-automation.ts +69 -0
  79. package/corpus/templates/factory/actions/save-triage-config.ts +28 -2
  80. package/corpus/templates/factory/actions/start-builder-for-item.ts +411 -0
  81. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +14 -3
  82. package/corpus/templates/factory/app/components/ui/checkbox.tsx +1 -0
  83. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +7 -0
  84. package/corpus/templates/factory/app/i18n/en-US.ts +35 -1
  85. package/corpus/templates/factory/app/routes/factory.tsx +462 -9
  86. package/corpus/templates/factory/changelog/2026-08-04-added-organization-owned-factory-automations-for-slack-bug-t.md +6 -0
  87. package/corpus/templates/factory/changelog/2026-08-04-automation-editor-controls-and-model-picker.md +6 -0
  88. package/corpus/templates/factory/netlify.toml +2 -2
  89. package/corpus/templates/factory/server/connectors/credentials.ts +6 -5
  90. package/corpus/templates/factory/server/connectors/slack.ts +133 -5
  91. package/corpus/templates/factory/server/db/schema.ts +6 -0
  92. package/corpus/templates/factory/server/lib/require-factory-automation.ts +65 -0
  93. package/corpus/templates/factory/server/plugins/agent-chat.ts +29 -11
  94. package/corpus/templates/factory/server/plugins/auth.ts +1 -0
  95. package/corpus/templates/factory/server/plugins/factory-migrations.ts +12 -0
  96. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +218 -67
  97. package/corpus/templates/factory/server/triage/builder-executor.ts +4 -0
  98. package/corpus/templates/factory/server/triage/contracts.ts +14 -1
  99. package/corpus/templates/factory/server/triage/github-client.ts +404 -0
  100. package/corpus/templates/factory/server/triage/metadata.ts +44 -0
  101. package/corpus/templates/factory/server/triage/pr-babysit.ts +5 -5
  102. package/corpus/templates/factory/server/triage/pr-policy.ts +144 -0
  103. package/corpus/templates/factory/server/triage/sentry-client.ts +180 -0
  104. package/corpus/templates/factory/server/triage/slack-client.ts +43 -0
  105. package/corpus/templates/factory/server/triage/slack-poller.ts +9 -2
  106. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +27 -0
  107. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +14 -4
  108. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -0
  109. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -0
  110. package/corpus/templates/plan/actions/create-plan-design.ts +8 -6
  111. package/corpus/templates/plan/actions/create-prototype-plan.ts +8 -6
  112. package/corpus/templates/plan/actions/create-ui-plan.ts +7 -7
  113. package/corpus/templates/plan/actions/get-plan-blocks.ts +7 -1
  114. package/corpus/templates/plan/actions/validate-plan-input.ts +39 -0
  115. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +167 -11
  116. package/corpus/templates/plan/app/i18n/en-US.ts +1 -0
  117. package/corpus/templates/plan/changelog/2026-08-04-canvas-zoom-controls-now-explain-the-command-ctrl-plus-scrol.md +6 -0
  118. package/corpus/templates/plan/changelog/2026-08-04-pinch-to-zoom-is-now-supported-on-touchscreens-in-visual-pla.md +6 -0
  119. package/corpus/templates/plan/shared/plan-content.ts +17 -2
  120. package/dist/cli/skills-content/canvas.d.ts +3 -3
  121. package/dist/cli/skills-content/canvas.js +16 -5
  122. package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
  123. package/dist/cli/skills-content/visual-plan-skill.js +27 -0
  124. package/dist/cli/skills-content/wireframe.d.ts +2 -2
  125. package/dist/cli/skills-content/wireframe.js +7 -0
  126. package/dist/client/AgentPanel.js +23 -8
  127. package/dist/client/api-path.d.ts +6 -0
  128. package/dist/client/api-path.js +55 -0
  129. package/dist/client/onboarding/FirstRunOnboarding.d.ts +3 -0
  130. package/dist/client/onboarding/FirstRunOnboarding.js +93 -0
  131. package/dist/client/onboarding/first-run-enabled.d.ts +2 -0
  132. package/dist/client/onboarding/first-run-enabled.js +9 -0
  133. package/dist/client/onboarding/index.d.ts +2 -1
  134. package/dist/client/onboarding/index.js +1 -0
  135. package/dist/client/onboarding/use-onboarding.d.ts +6 -1
  136. package/dist/client/onboarding/use-onboarding.js +32 -1
  137. package/dist/client/route-state.js +10 -0
  138. package/dist/client/settings/SecretsSection.js +32 -8
  139. package/dist/collab/struct-routes.d.ts +1 -1
  140. package/dist/connections/catalog.d.ts +4 -4
  141. package/dist/connections/catalog.js +3 -3
  142. package/dist/db/index.d.ts +1 -1
  143. package/dist/db/index.js +1 -1
  144. package/dist/localization/default-messages.d.ts +5 -0
  145. package/dist/localization/default-messages.js +5 -0
  146. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  147. package/dist/observability/routes.d.ts +3 -3
  148. package/dist/onboarding/app-profile.d.ts +4 -0
  149. package/dist/onboarding/app-profile.js +378 -0
  150. package/dist/onboarding/index.d.ts +2 -1
  151. package/dist/onboarding/index.js +1 -0
  152. package/dist/onboarding/plugin.d.ts +2 -0
  153. package/dist/onboarding/plugin.js +21 -1
  154. package/dist/onboarding/types.d.ts +19 -0
  155. package/dist/progress/routes.d.ts +1 -1
  156. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  157. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  158. package/dist/secrets/routes.d.ts +2 -2
  159. package/dist/secrets/routes.js +41 -17
  160. package/dist/server/auth.js +21 -1
  161. package/dist/server/better-auth-instance.d.ts +3 -2
  162. package/dist/server/better-auth-instance.js +9 -7
  163. package/dist/server/email-markdown.js +3 -8
  164. package/dist/server/transcribe-voice.d.ts +1 -1
  165. package/dist/shared/first-run-onboarding.d.ts +3 -0
  166. package/dist/shared/first-run-onboarding.js +3 -0
  167. package/dist/triggers/index.d.ts +3 -0
  168. package/dist/triggers/index.js +5 -0
  169. package/docs/content/cross-app-sso.mdx +26 -0
  170. package/package.json +1 -1
@@ -0,0 +1,491 @@
1
+ import { buildCodeLayerProjection } from "@shared/code-layer";
2
+
3
+ import type { ElementInfo } from "@/components/design/types";
4
+
5
+ import { resolveCodeLayerNodeFromElementInfo } from "./code-layer-state";
6
+
7
+ /**
8
+ * Figma parity — an arrow key reorders a flow child and translates everything
9
+ * else. Writing left/top on a flow child is the wrong operation: under
10
+ * `position: static` it does nothing, and under `relative` it detaches the
11
+ * element from its neighbours instead of moving through them.
12
+ */
13
+
14
+ export type NudgeDirection = "up" | "right" | "down" | "left";
15
+
16
+ export interface NudgeAmounts {
17
+ small: number;
18
+ big: number;
19
+ }
20
+
21
+ export const DEFAULT_NUDGE_AMOUNTS: NudgeAmounts = { small: 1, big: 10 };
22
+
23
+ export type FlowAxis = "horizontal" | "vertical";
24
+
25
+ export interface FlowContainerInfo {
26
+ kind: "flex" | "grid" | "none";
27
+ /** The axis DOM order advances along. */
28
+ axis: FlowAxis;
29
+ /** Visual order runs opposite to DOM order (`*-reverse`). */
30
+ reversed: boolean;
31
+ wraps: boolean;
32
+ /** Items per line when statically knowable (grid tracks); null otherwise. */
33
+ lineLength: number | null;
34
+ }
35
+
36
+ export const NO_FLOW_CONTAINER: FlowContainerInfo = {
37
+ kind: "none",
38
+ axis: "horizontal",
39
+ reversed: false,
40
+ wraps: false,
41
+ lineLength: null,
42
+ };
43
+
44
+ export type NudgeIntent =
45
+ | { kind: "translate"; dx: number; dy: number }
46
+ | { kind: "reorder"; fromIndex: number; toIndex: number }
47
+ | { kind: "none" };
48
+
49
+ export interface FlowContainerStyleSource {
50
+ style: Partial<Record<string, string>>;
51
+ classes?: readonly string[];
52
+ }
53
+
54
+ function toPositiveInteger(value: string | undefined): number | null {
55
+ if (!value) return null;
56
+ const parsed = Number.parseInt(value, 10);
57
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
58
+ }
59
+
60
+ /** Tailwind utilities only count unprefixed: `md:flex` is conditional on a
61
+ * breakpoint we are not resolving here, so treating it as the current layout
62
+ * would reorder against a container shape the user cannot see. */
63
+ function utilitySet(classes: readonly string[] | undefined): Set<string> {
64
+ const set = new Set<string>();
65
+ for (const raw of classes ?? []) {
66
+ const token = raw.trim();
67
+ if (!token || token.includes(":")) continue;
68
+ set.add(token);
69
+ }
70
+ return set;
71
+ }
72
+
73
+ /**
74
+ * `grid-template-columns: repeat(4, minmax(0,1fr))` and
75
+ * `grid-template-columns: 1fr 1fr 1fr` both describe three-or-four column
76
+ * tracks; counting top-level tokens (with `repeat(n, …)` expanded to n) is
77
+ * enough for reorder arithmetic and never needs layout.
78
+ */
79
+ export function countGridTracks(value: string | undefined): number | null {
80
+ if (!value) return null;
81
+ const trimmed = value.trim();
82
+ if (!trimmed || trimmed === "none") return null;
83
+ let count = 0;
84
+ let depth = 0;
85
+ let token = "";
86
+ let unresolved = false;
87
+ const flush = () => {
88
+ const item = token.trim();
89
+ token = "";
90
+ if (!item) return;
91
+ const repeat = /^repeat\(\s*([^,]+?)\s*,(.*)\)$/is.exec(item);
92
+ if (repeat) {
93
+ const times = Number.parseInt(repeat[1]!, 10);
94
+ // `auto-fit`/`auto-fill` resolve against the container's width, so the
95
+ // track count is only knowable from layout. Counting the repeat as one
96
+ // track would make a cross-axis arrow move one sibling, not one row.
97
+ if (!Number.isSafeInteger(times) || times <= 0) {
98
+ unresolved = true;
99
+ return;
100
+ }
101
+ count += times * (countGridTracks(repeat[2]) ?? 1);
102
+ return;
103
+ }
104
+ count += 1;
105
+ };
106
+ for (const char of trimmed) {
107
+ if (char === "(") depth += 1;
108
+ if (char === ")") depth -= 1;
109
+ if (depth === 0 && /\s/.test(char)) {
110
+ flush();
111
+ continue;
112
+ }
113
+ token += char;
114
+ }
115
+ flush();
116
+ if (unresolved) return null;
117
+ return count > 0 ? count : null;
118
+ }
119
+
120
+ /** Read the flow shape of a would-be parent from its authored styles and
121
+ * Tailwind utilities. Accepts anything shaped like a `CodeLayerNode` or an
122
+ * `ElementInfo`'s computed styles. */
123
+ export function describeFlowContainer(
124
+ source: FlowContainerStyleSource | null | undefined,
125
+ ): FlowContainerInfo {
126
+ if (!source) return NO_FLOW_CONTAINER;
127
+ const style = source.style ?? {};
128
+ const classes = utilitySet(source.classes);
129
+
130
+ const display =
131
+ style.display ??
132
+ (classes.has("flex")
133
+ ? "flex"
134
+ : classes.has("inline-flex")
135
+ ? "inline-flex"
136
+ : classes.has("grid")
137
+ ? "grid"
138
+ : classes.has("inline-grid")
139
+ ? "inline-grid"
140
+ : undefined);
141
+
142
+ if (display === "flex" || display === "inline-flex") {
143
+ const direction =
144
+ style["flex-direction"] ??
145
+ (classes.has("flex-row-reverse")
146
+ ? "row-reverse"
147
+ : classes.has("flex-col-reverse")
148
+ ? "column-reverse"
149
+ : classes.has("flex-col")
150
+ ? "column"
151
+ : classes.has("flex-row")
152
+ ? "row"
153
+ : "row");
154
+ const wrap =
155
+ style["flex-wrap"] ??
156
+ (classes.has("flex-wrap-reverse")
157
+ ? "wrap-reverse"
158
+ : classes.has("flex-wrap")
159
+ ? "wrap"
160
+ : classes.has("flex-nowrap")
161
+ ? "nowrap"
162
+ : "nowrap");
163
+ return {
164
+ kind: "flex",
165
+ axis: direction.startsWith("column") ? "vertical" : "horizontal",
166
+ reversed: direction.endsWith("-reverse"),
167
+ wraps: wrap === "wrap" || wrap === "wrap-reverse",
168
+ lineLength: null,
169
+ };
170
+ }
171
+
172
+ if (display === "grid" || display === "inline-grid") {
173
+ const autoFlow = style["grid-auto-flow"] ?? "";
174
+ const columnFlow =
175
+ autoFlow.includes("column") || classes.has("grid-flow-col");
176
+ const templateColumns =
177
+ style["grid-template-columns"] ??
178
+ gridTemplateFromUtilities(classes, "grid-cols-");
179
+ const templateRows =
180
+ style["grid-template-rows"] ??
181
+ gridTemplateFromUtilities(classes, "grid-rows-");
182
+ return {
183
+ kind: "grid",
184
+ axis: columnFlow ? "vertical" : "horizontal",
185
+ reversed: false,
186
+ // A grid always continues onto the next track line; there is no
187
+ // grid equivalent of `flex-wrap: nowrap`.
188
+ wraps: true,
189
+ lineLength: columnFlow
190
+ ? countGridTracks(templateRows)
191
+ : countGridTracks(templateColumns),
192
+ };
193
+ }
194
+
195
+ return NO_FLOW_CONTAINER;
196
+ }
197
+
198
+ function gridTemplateFromUtilities(
199
+ classes: Set<string>,
200
+ prefix: string,
201
+ ): string | undefined {
202
+ for (const token of classes) {
203
+ if (!token.startsWith(prefix)) continue;
204
+ const count = toPositiveInteger(token.slice(prefix.length));
205
+ if (count) return `repeat(${count}, 1fr)`;
206
+ }
207
+ return undefined;
208
+ }
209
+
210
+ /** A declared flex/grid `order`, or null when the child leaves it at the
211
+ * default. Tailwind's `order-first`/`order-last` map to the sentinels the
212
+ * utility generates. */
213
+ const GRID_PLACEMENT_PROPERTIES = [
214
+ "grid-row",
215
+ "grid-column",
216
+ "grid-area",
217
+ "grid-row-start",
218
+ "grid-column-start",
219
+ ];
220
+
221
+ const GRID_PLACEMENT_UTILITY_PREFIXES = [
222
+ "col-span-",
223
+ "row-span-",
224
+ "col-start-",
225
+ "row-start-",
226
+ "col-end-",
227
+ "row-end-",
228
+ ];
229
+
230
+ /** Explicit grid placement opts a child out of auto-placement, so moving it in
231
+ * the DOM leaves it in the same cell. */
232
+ export function hasExplicitGridPlacement(
233
+ source: FlowContainerStyleSource,
234
+ ): boolean {
235
+ if (GRID_PLACEMENT_PROPERTIES.some((property) => source.style?.[property])) {
236
+ return true;
237
+ }
238
+ for (const token of utilitySet(source.classes)) {
239
+ if (GRID_PLACEMENT_UTILITY_PREFIXES.some((p) => token.startsWith(p))) {
240
+ return true;
241
+ }
242
+ }
243
+ return false;
244
+ }
245
+
246
+ export function declaredFlexOrder(
247
+ source: FlowContainerStyleSource,
248
+ ): number | null {
249
+ const inline = source.style?.order;
250
+ if (inline !== undefined && inline !== "") {
251
+ const parsed = Number.parseInt(inline, 10);
252
+ return Number.isSafeInteger(parsed) ? parsed : null;
253
+ }
254
+ for (const token of utilitySet(source.classes)) {
255
+ if (token === "order-first") return -9999;
256
+ if (token === "order-last") return 9999;
257
+ if (token === "order-none") return 0;
258
+ if (!token.startsWith("order-")) continue;
259
+ const parsed = Number.parseInt(token.slice("order-".length), 10);
260
+ if (Number.isSafeInteger(parsed)) return parsed;
261
+ }
262
+ return null;
263
+ }
264
+
265
+ /** `position: absolute|fixed` takes a child out of its parent's flow — Figma
266
+ * calls the same escape hatch "Ignore auto layout". `sticky` and `relative`
267
+ * still occupy a flow slot, so they keep reordering. */
268
+ export function escapesFlow(
269
+ position: string | null | undefined,
270
+ classes?: readonly string[],
271
+ ): boolean {
272
+ const value =
273
+ position ??
274
+ (utilitySet(classes).has("absolute")
275
+ ? "absolute"
276
+ : utilitySet(classes).has("fixed")
277
+ ? "fixed"
278
+ : undefined);
279
+ return value === "absolute" || value === "fixed";
280
+ }
281
+
282
+ export interface ResolveNudgeIntentArgs {
283
+ direction: NudgeDirection;
284
+ largeStep: boolean;
285
+ amounts?: NudgeAmounts;
286
+ container?: FlowContainerInfo;
287
+ /** The selected node's own `position`. */
288
+ position?: string | null;
289
+ /** Its 0-based index among its siblings in DOM order. */
290
+ siblingIndex?: number;
291
+ /** How many siblings share the container, including the selection. */
292
+ siblingCount?: number;
293
+ }
294
+
295
+ export function resolveNudgeIntent(args: ResolveNudgeIntentArgs): NudgeIntent {
296
+ const amounts = args.amounts ?? DEFAULT_NUDGE_AMOUNTS;
297
+ const step = args.largeStep ? amounts.big : amounts.small;
298
+ const forward = args.direction === "right" || args.direction === "down";
299
+ const arrowAxis: FlowAxis =
300
+ args.direction === "left" || args.direction === "right"
301
+ ? "horizontal"
302
+ : "vertical";
303
+
304
+ const translate = (): NudgeIntent => ({
305
+ kind: "translate",
306
+ dx: arrowAxis === "horizontal" ? (forward ? step : -step) : 0,
307
+ dy: arrowAxis === "vertical" ? (forward ? step : -step) : 0,
308
+ });
309
+
310
+ const container = args.container ?? NO_FLOW_CONTAINER;
311
+ if (container.kind === "none" || escapesFlow(args.position)) {
312
+ return translate();
313
+ }
314
+
315
+ const fromIndex = args.siblingIndex;
316
+ const siblingCount = args.siblingCount;
317
+ if (
318
+ fromIndex === undefined ||
319
+ siblingCount === undefined ||
320
+ fromIndex < 0 ||
321
+ siblingCount < 2
322
+ ) {
323
+ return { kind: "none" };
324
+ }
325
+
326
+ let delta: number;
327
+ if (arrowAxis === container.axis) {
328
+ delta = (forward ? 1 : -1) * (container.reversed ? -1 : 1);
329
+ } else {
330
+ const lineLength = container.lineLength;
331
+ if (!container.wraps || !lineLength || lineLength < 1) {
332
+ return { kind: "none" };
333
+ }
334
+ delta = (forward ? 1 : -1) * lineLength;
335
+ }
336
+
337
+ const toIndex = Math.min(Math.max(fromIndex + delta, 0), siblingCount - 1);
338
+ if (toIndex === fromIndex) return { kind: "none" };
339
+ return { kind: "reorder", fromIndex, toIndex };
340
+ }
341
+
342
+ export interface ReorderAnchor {
343
+ anchorIndex: number;
344
+ placement: "before" | "after";
345
+ }
346
+
347
+ /** Translate a `reorder` intent into the anchor sibling + placement the
348
+ * `moveNode` visual edit takes. Moving later must anchor AFTER the sibling
349
+ * currently at the destination index, and moving earlier BEFORE it — the
350
+ * moved node vacates its own slot, so anchoring on the wrong side lands it
351
+ * one position short. */
352
+ export function reorderAnchorFor(intent: {
353
+ fromIndex: number;
354
+ toIndex: number;
355
+ }): ReorderAnchor {
356
+ return {
357
+ anchorIndex: intent.toIndex,
358
+ placement: intent.toIndex > intent.fromIndex ? "after" : "before",
359
+ };
360
+ }
361
+
362
+ export type ElementNudgeIntent =
363
+ | { kind: "translate"; dx: number; dy: number }
364
+ | {
365
+ kind: "reorder";
366
+ /** The snapshot the node ids below were resolved against — the caller
367
+ * must apply its `moveNode` edit to THIS string, not re-read the file,
368
+ * or the ids can address a document that has since changed. */
369
+ content: string;
370
+ targetNodeId: string;
371
+ anchorNodeId: string;
372
+ placement: "before" | "after";
373
+ }
374
+ | { kind: "none" };
375
+
376
+ /** Rendered `display` from the canvas bridge (`getComputedStyle`), which sees
377
+ * stylesheet rules and the active breakpoint that authored-style parsing
378
+ * cannot. */
379
+ function isRenderedFlowDisplay(display: string | null | undefined): boolean {
380
+ return (
381
+ display === "flex" ||
382
+ display === "inline-flex" ||
383
+ display === "grid" ||
384
+ display === "inline-grid"
385
+ );
386
+ }
387
+
388
+ export interface ResolveElementNudgeIntentArgs {
389
+ content: string;
390
+ selectedElement: ElementInfo;
391
+ direction: NudgeDirection;
392
+ largeStep: boolean;
393
+ amounts?: NudgeAmounts;
394
+ }
395
+
396
+ /** Resolve an arrow key against a selected element's real position in its
397
+ * source document. Falls back to a plain translate whenever the document or
398
+ * the node cannot be resolved (live/localhost screens have no authored source
399
+ * here), so an unreadable projection never silently swallows the keypress. */
400
+ export function resolveElementNudgeIntent(
401
+ args: ResolveElementNudgeIntentArgs,
402
+ ): ElementNudgeIntent {
403
+ const translate = resolveNudgeIntent({
404
+ direction: args.direction,
405
+ largeStep: args.largeStep,
406
+ amounts: args.amounts,
407
+ }) as { kind: "translate"; dx: number; dy: number };
408
+
409
+ if (!args.content) return translate;
410
+ const projection = buildCodeLayerProjection(args.content);
411
+ const node = resolveCodeLayerNodeFromElementInfo(
412
+ projection,
413
+ args.selectedElement,
414
+ );
415
+ if (!node) return translate;
416
+
417
+ const parent = node.parentId
418
+ ? (projection.nodes.find((candidate) => candidate.id === node.parentId) ??
419
+ null)
420
+ : null;
421
+ const siblingIds = parent?.children ?? projection.rootNodeIds;
422
+ const siblingIndex = siblingIds.indexOf(node.id);
423
+ if (siblingIndex < 0) return translate;
424
+
425
+ const position =
426
+ node.style.position ??
427
+ (escapesFlow(undefined, node.classes) ? "absolute" : undefined) ??
428
+ args.selectedElement.computedStyles?.position;
429
+
430
+ const container = describeFlowContainer(parent);
431
+ // Flex/grid paint children by `order` and explicit grid placement, not DOM
432
+ // position, so moving the node would write a source change that produces no
433
+ // visible movement.
434
+ if (
435
+ container.kind !== "none" &&
436
+ siblingIds.some((id) => {
437
+ const sibling = projection.nodes.find((candidate) => candidate.id === id);
438
+ if (!sibling) return false;
439
+ const order = declaredFlexOrder(sibling);
440
+ if (order !== null && order !== 0) return true;
441
+ return container.kind === "grid" && hasExplicitGridPlacement(sibling);
442
+ })
443
+ ) {
444
+ return { kind: "none" };
445
+ }
446
+
447
+ // A `.row { display: flex }` parent parses as no container. Translating
448
+ // would write left/top onto a flex child and detach it from its
449
+ // neighbours, so do nothing rather than corrupt the layout.
450
+ if (
451
+ container.kind === "none" &&
452
+ !escapesFlow(position) &&
453
+ isRenderedFlowDisplay(args.selectedElement.parentDisplay)
454
+ ) {
455
+ return { kind: "none" };
456
+ }
457
+
458
+ // Rendered `order` from the bridge sees stylesheet rules that the authored
459
+ // styles above cannot.
460
+ const renderedOrder = args.selectedElement.computedStyles?.order;
461
+ if (
462
+ container.kind !== "none" &&
463
+ renderedOrder !== undefined &&
464
+ renderedOrder !== "" &&
465
+ renderedOrder !== "0"
466
+ ) {
467
+ return { kind: "none" };
468
+ }
469
+
470
+ const intent = resolveNudgeIntent({
471
+ direction: args.direction,
472
+ largeStep: args.largeStep,
473
+ amounts: args.amounts,
474
+ container,
475
+ position,
476
+ siblingIndex,
477
+ siblingCount: siblingIds.length,
478
+ });
479
+
480
+ if (intent.kind !== "reorder") return intent;
481
+ const anchor = reorderAnchorFor(intent);
482
+ const anchorNodeId = siblingIds[anchor.anchorIndex];
483
+ if (!anchorNodeId) return { kind: "none" };
484
+ return {
485
+ kind: "reorder",
486
+ content: args.content,
487
+ targetNodeId: node.id,
488
+ anchorNodeId,
489
+ placement: anchor.placement,
490
+ };
491
+ }
@@ -0,0 +1,133 @@
1
+ import {
2
+ buildCodeLayerProjection,
3
+ type CodeLayerNode,
4
+ } from "@shared/code-layer";
5
+
6
+ import type { ElementInfo } from "@/components/design/types";
7
+
8
+ import { resolveCodeLayerNodeFromElementInfo } from "./code-layer-state";
9
+ import { describeFlowContainer, type FlowContainerInfo } from "./nudge-intent";
10
+
11
+ /**
12
+ * Figma parity — paste goes INSIDE a selected frame and AFTER a selected
13
+ * object. Treating every selection as an object is the difference between
14
+ * "paste into this card" and "paste a second card beside it".
15
+ */
16
+
17
+ /** Elements that render their own content and can never host a pasted layer. */
18
+ const REPLACED_TAGS = new Set([
19
+ "area",
20
+ "audio",
21
+ "br",
22
+ "canvas",
23
+ "circle",
24
+ "embed",
25
+ "hr",
26
+ "iframe",
27
+ "img",
28
+ "input",
29
+ "object",
30
+ "path",
31
+ "polygon",
32
+ "rect",
33
+ "select",
34
+ "source",
35
+ "svg",
36
+ "textarea",
37
+ "track",
38
+ "video",
39
+ "wbr",
40
+ ]);
41
+
42
+ /** Elements a designer reads as a text object rather than a frame, even when
43
+ * markup nests inline children inside them. */
44
+ const TEXT_LEAF_TAGS = new Set([
45
+ "a",
46
+ "b",
47
+ "blockquote",
48
+ "button",
49
+ "caption",
50
+ "code",
51
+ "em",
52
+ "figcaption",
53
+ "h1",
54
+ "h2",
55
+ "h3",
56
+ "h4",
57
+ "h5",
58
+ "h6",
59
+ "i",
60
+ "label",
61
+ "legend",
62
+ "li",
63
+ "option",
64
+ "p",
65
+ "pre",
66
+ "small",
67
+ "span",
68
+ "strong",
69
+ "summary",
70
+ "td",
71
+ "th",
72
+ ]);
73
+
74
+ export interface PasteTargetNode {
75
+ tag: string;
76
+ hasElementChildren: boolean;
77
+ hasText: boolean;
78
+ container: FlowContainerInfo;
79
+ primitiveKind?: string | null;
80
+ }
81
+
82
+ export function isPasteContainer(node: PasteTargetNode): boolean {
83
+ const tag = node.tag.toLowerCase();
84
+ if (REPLACED_TAGS.has(tag)) return false;
85
+ if (TEXT_LEAF_TAGS.has(tag)) return false;
86
+ if (node.primitiveKind && node.primitiveKind !== "frame") return false;
87
+ if (node.container.kind !== "none") return true;
88
+ if (node.hasElementChildren) return true;
89
+ return !node.hasText;
90
+ }
91
+
92
+ export type PastePlacement = "inside" | "after";
93
+
94
+ export interface PastePlacementDecision {
95
+ placement: PastePlacement;
96
+ targetNodeId: string;
97
+ }
98
+
99
+ function resolvePastePlacement(
100
+ node: PasteTargetNode & { targetNodeId: string },
101
+ ): PastePlacementDecision {
102
+ return {
103
+ placement: isPasteContainer(node) ? "inside" : "after",
104
+ targetNodeId: node.targetNodeId,
105
+ };
106
+ }
107
+
108
+ function pasteTargetFromCodeLayerNode(
109
+ node: CodeLayerNode,
110
+ ): PasteTargetNode & { targetNodeId: string } {
111
+ return {
112
+ tag: node.tag,
113
+ hasElementChildren: node.children.length > 0,
114
+ hasText: Boolean(node.textSnippet && node.textSnippet.trim()),
115
+ container: describeFlowContainer(node),
116
+ primitiveKind: node.dataAttributes["data-an-primitive"] ?? null,
117
+ targetNodeId: node.id,
118
+ };
119
+ }
120
+
121
+ export function resolvePastePlacementForSelection(args: {
122
+ content: string;
123
+ selectedElement: ElementInfo | null | undefined;
124
+ }): PastePlacementDecision | null {
125
+ if (!args.content || !args.selectedElement) return null;
126
+ const projection = buildCodeLayerProjection(args.content);
127
+ const node = resolveCodeLayerNodeFromElementInfo(
128
+ projection,
129
+ args.selectedElement,
130
+ );
131
+ if (!node) return null;
132
+ return resolvePastePlacement(pasteTargetFromCodeLayerNode(node));
133
+ }
@@ -331,6 +331,14 @@ export async function renderExportDocumentCanvas({
331
331
  }
332
332
  }
333
333
 
334
+ /**
335
+ * What a raster capture is of. Overview mode resolves a different iframe and a
336
+ * different crop per scope, so "the current selection" is not one thing: Copy
337
+ * as PNG wants the selected screen, the export preview wants the selected
338
+ * element inside it.
339
+ */
340
+ export type PngCaptureScope = "document" | "screens" | "element";
341
+
334
342
  export type PngCaptureErrorCode =
335
343
  | "no-preview"
336
344
  | "external-preview"
@@ -149,19 +149,12 @@ export function isScreenRootElementInfo(info: ElementInfo | null | undefined) {
149
149
  }
150
150
 
151
151
  /**
152
- * Whether a plain Delete in overview mode targets an ELEMENT inside a screen
153
- * rather than the screen frames themselves. MultiScreenCanvas keeps the owning
154
- * screen in its `selectedIds` while an element inside it is selected (frame
155
- * z-order and "topmost screen" depend on that), so Delete reaches the editor as
156
- * "delete these screens" even when the real selection is one node. The more
157
- * specific target wins — the same precedence shouldSuppressFrameSelectionBox
158
- * already applies to the selection chrome.
159
- *
160
- * Layer ids are filtered the way getSelectedLayerSnapshots filters its
161
- * candidates: a screen's own file-id row and the `__`-prefixed pseudo rows are
162
- * screen/frame selections, not element selections.
152
+ * MultiScreenCanvas keeps the owning screen in `selectedIds` while an element
153
+ * inside it is selected, so every overview command reading that array (Delete,
154
+ * arrow-key nudge) sees "the screens" when the real target is one node. The
155
+ * more specific target wins. `__`-prefixed and file-id rows are frames.
163
156
  */
164
- export function overviewDeleteTargetsElement(args: {
157
+ export function overviewSelectionTargetsElement(args: {
165
158
  selectedElement: ElementInfo | null | undefined;
166
159
  selectedLayerIds: readonly string[];
167
160
  fileIds: readonly string[];
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-03
4
+ ---
5
+
6
+ Arrow keys now reorder a layer inside an auto layout or grid frame the way Figma does, and nudge free-placed layers by an amount you can set in the Cursor tab of the keyboard shortcuts panel.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-03
4
+ ---
5
+
6
+ Auto layout flow icons show the direction at a glance instead of needing a hover.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-03
4
+ ---
5
+
6
+ Auto layout icons share one outlined-square style, so flow, gap and padding read as one set.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-03
4
+ ---
5
+
6
+ Export has a named Preview section you can expand, replacing the unlabeled image icon.