@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
@@ -4,34 +4,50 @@ interface IconProps {
4
4
  className?: string;
5
5
  }
6
6
 
7
- // Gap icon: ]·[ two inward-facing C-brackets with a center dot
7
+ /** Text tool / text node. A serif T with end and foot serifs — the plain
8
+ * letterform reads as a letter, this reads as type. */
9
+ export function IconText({ className }: IconProps) {
10
+ return (
11
+ <svg
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ strokeWidth={1.5}
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ className={cn("size-4", className)}
19
+ aria-hidden="true"
20
+ >
21
+ <path d="M3 4H21" />
22
+ <path d="M3 4V8" />
23
+ <path d="M21 4V8" />
24
+ <path d="M12 4V20" />
25
+ <path d="M7 20H17" />
26
+ </svg>
27
+ );
28
+ }
29
+
30
+ // Gap: two items with a measured space between them.
8
31
  export function IconGap({ className }: IconProps) {
9
32
  return (
10
33
  <svg
11
34
  viewBox="0 0 24 24"
12
35
  fill="none"
13
36
  stroke="currentColor"
14
- strokeWidth={2}
37
+ strokeWidth={1.5}
15
38
  strokeLinecap="round"
16
39
  strokeLinejoin="round"
17
40
  className={cn("size-4", className)}
18
41
  aria-hidden="true"
19
42
  >
20
- {/* Left bracket ] vertical bar on right side, serifs pointing right */}
21
- <line x1="9" y1="7" x2="6" y2="7" />
22
- <line x1="6" y1="7" x2="6" y2="17" />
23
- <line x1="6" y1="17" x2="9" y2="17" />
24
- {/* Center dot */}
25
- <circle cx="12" cy="12" r="1" fill="currentColor" stroke="none" />
26
- {/* Right bracket [ — vertical bar on left side, serifs pointing left */}
27
- <line x1="15" y1="7" x2="18" y2="7" />
28
- <line x1="18" y1="7" x2="18" y2="17" />
29
- <line x1="18" y1="17" x2="15" y2="17" />
43
+ <rect x="2.5" y="5" width="6" height="14" rx="1" />
44
+ <rect x="15.5" y="5" width="6" height="14" rx="1" />
45
+ <path d="M12 7.5V16.5" />
30
46
  </svg>
31
47
  );
32
48
  }
33
49
 
34
- // Padding horizontal: square with thick left and right edges
50
+ // Padding: frame with the two insets for that axis marked.
35
51
  export function IconPaddingHorizontal({ className }: IconProps) {
36
52
  return (
37
53
  <svg
@@ -44,33 +60,13 @@ export function IconPaddingHorizontal({ className }: IconProps) {
44
60
  className={cn("size-4", className)}
45
61
  aria-hidden="true"
46
62
  >
47
- {/* Outer frame */}
48
- <rect x="3" y="4" width="18" height="16" rx="1.5" strokeWidth={1.5} />
49
- {/* Thick left edge fill */}
50
- <rect
51
- x="3"
52
- y="4"
53
- width="4"
54
- height="16"
55
- rx="1.5"
56
- fill="currentColor"
57
- stroke="none"
58
- />
59
- {/* Thick right edge fill */}
60
- <rect
61
- x="17"
62
- y="4"
63
- width="4"
64
- height="16"
65
- rx="1.5"
66
- fill="currentColor"
67
- stroke="none"
68
- />
63
+ <rect x="3" y="4" width="18" height="16" rx="1.5" />
64
+ <path d="M7.5 8V16" />
65
+ <path d="M16.5 8V16" />
69
66
  </svg>
70
67
  );
71
68
  }
72
69
 
73
- // Padding vertical: square with thick top and bottom edges
74
70
  export function IconPaddingVertical({ className }: IconProps) {
75
71
  return (
76
72
  <svg
@@ -83,34 +79,19 @@ export function IconPaddingVertical({ className }: IconProps) {
83
79
  className={cn("size-4", className)}
84
80
  aria-hidden="true"
85
81
  >
86
- {/* Outer frame */}
87
- <rect x="4" y="3" width="16" height="18" rx="1.5" strokeWidth={1.5} />
88
- {/* Thick top edge fill */}
89
- <rect
90
- x="4"
91
- y="3"
92
- width="16"
93
- height="4"
94
- rx="1.5"
95
- fill="currentColor"
96
- stroke="none"
97
- />
98
- {/* Thick bottom edge fill */}
99
- <rect
100
- x="4"
101
- y="17"
102
- width="16"
103
- height="4"
104
- rx="1.5"
105
- fill="currentColor"
106
- stroke="none"
107
- />
82
+ <rect x="4" y="3" width="16" height="18" rx="1.5" />
83
+ <path d="M8 7.5H16" />
84
+ <path d="M8 16.5H16" />
108
85
  </svg>
109
86
  );
110
87
  }
111
88
 
112
- // Flow horizontal: item + arrow. Keep the arrow two symmetric boxes read as
113
- // "this axis" but not "this direction", forcing a tooltip hover to disambiguate.
89
+ /** Shared item square. Every flow glyph must stay on this one grid and stroke
90
+ * weight; a filled bar or container frame among them reads as a different
91
+ * control and stops the four segments comparing at a glance. */
92
+ const FLOW_ITEM = { width: 8, height: 8, rx: 1 } as const;
93
+
94
+ // Flow horizontal: two items side by side, arrow running left → right beneath.
114
95
  export function IconFlowHorizontal({ className }: IconProps) {
115
96
  return (
116
97
  <svg
@@ -123,26 +104,15 @@ export function IconFlowHorizontal({ className }: IconProps) {
123
104
  className={cn("size-4", className)}
124
105
  aria-hidden="true"
125
106
  >
126
- {/* Outer container wide landscape frame */}
127
- <rect x="2" y="6" width="20" height="12" rx="2" strokeWidth={1.5} />
128
- {/* First item */}
129
- <rect
130
- x="4.5"
131
- y="9"
132
- width="4"
133
- height="6"
134
- rx="1"
135
- fill="currentColor"
136
- stroke="none"
137
- />
138
- {/* Direction arrow — left to right */}
139
- <path d="M11 12H18.5" />
140
- <path d="M16.3 9.8 18.5 12 16.3 14.2" />
107
+ <rect x="3" y="3" {...FLOW_ITEM} />
108
+ <rect x="13" y="3" {...FLOW_ITEM} />
109
+ <path d="M3 17H19.4" />
110
+ <path d="M17 14.8 19.4 17 17 19.2" />
141
111
  </svg>
142
112
  );
143
113
  }
144
114
 
145
- // Flow vertical: an item plus a downward arrow see IconFlowHorizontal.
115
+ // Flow vertical: items stacked, arrow running top bottom alongside.
146
116
  export function IconFlowVertical({ className }: IconProps) {
147
117
  return (
148
118
  <svg
@@ -155,21 +125,29 @@ export function IconFlowVertical({ className }: IconProps) {
155
125
  className={cn("size-4", className)}
156
126
  aria-hidden="true"
157
127
  >
158
- {/* Outer container tall portrait frame */}
159
- <rect x="6" y="2" width="12" height="20" rx="2" strokeWidth={1.5} />
160
- {/* First item */}
161
- <rect
162
- x="9"
163
- y="4.5"
164
- width="6"
165
- height="4"
166
- rx="1"
167
- fill="currentColor"
168
- stroke="none"
169
- />
170
- {/* Direction arrow — top to bottom */}
171
- <path d="M12 11V18.5" />
172
- <path d="M9.8 16.3 12 18.5 14.2 16.3" />
128
+ <rect x="3" y="3" {...FLOW_ITEM} />
129
+ <rect x="3" y="13" {...FLOW_ITEM} />
130
+ <path d="M17 3V19.4" />
131
+ <path d="M14.8 17 17 19.4 19.2 17" />
132
+ </svg>
133
+ );
134
+ }
135
+
136
+ // Flow normal: two items placed freely, no axis and no direction arrow.
137
+ export function IconFlowNormal({ className }: IconProps) {
138
+ return (
139
+ <svg
140
+ viewBox="0 0 24 24"
141
+ fill="none"
142
+ stroke="currentColor"
143
+ strokeWidth={1.5}
144
+ strokeLinecap="round"
145
+ strokeLinejoin="round"
146
+ className={cn("size-4", className)}
147
+ aria-hidden="true"
148
+ >
149
+ <rect x="3" y="3" {...FLOW_ITEM} />
150
+ <rect x="13" y="13" {...FLOW_ITEM} />
173
151
  </svg>
174
152
  );
175
153
  }
@@ -199,7 +177,7 @@ export function IconFlowWrap({ className }: IconProps) {
199
177
  );
200
178
  }
201
179
 
202
- // Flow grid: 2×2 equal grid of boxes
180
+ // Flow grid: the same item square in a 2×2.
203
181
  export function IconFlowGrid({ className }: IconProps) {
204
182
  return (
205
183
  <svg
@@ -212,14 +190,10 @@ export function IconFlowGrid({ className }: IconProps) {
212
190
  className={cn("size-4", className)}
213
191
  aria-hidden="true"
214
192
  >
215
- {/* Top-left */}
216
- <rect x="2" y="2" width="9" height="9" rx="1.5" strokeWidth={1.5} />
217
- {/* Top-right */}
218
- <rect x="13" y="2" width="9" height="9" rx="1.5" strokeWidth={1.5} />
219
- {/* Bottom-left */}
220
- <rect x="2" y="13" width="9" height="9" rx="1.5" strokeWidth={1.5} />
221
- {/* Bottom-right */}
222
- <rect x="13" y="13" width="9" height="9" rx="1.5" strokeWidth={1.5} />
193
+ <rect x="3" y="3" {...FLOW_ITEM} />
194
+ <rect x="13" y="3" {...FLOW_ITEM} />
195
+ <rect x="3" y="13" {...FLOW_ITEM} />
196
+ <rect x="13" y="13" {...FLOW_ITEM} />
223
197
  </svg>
224
198
  );
225
199
  }
@@ -580,7 +554,7 @@ export function IconSizingFixed({ className }: IconProps) {
580
554
  viewBox="0 0 24 24"
581
555
  fill="none"
582
556
  stroke="currentColor"
583
- strokeWidth={1.6}
557
+ strokeWidth={1.5}
584
558
  strokeLinecap="round"
585
559
  strokeLinejoin="round"
586
560
  className={cn("size-3.5", className)}
@@ -603,7 +577,7 @@ export function IconSizingHug({ className }: IconProps) {
603
577
  viewBox="0 0 24 24"
604
578
  fill="none"
605
579
  stroke="currentColor"
606
- strokeWidth={1.6}
580
+ strokeWidth={1.5}
607
581
  strokeLinecap="round"
608
582
  strokeLinejoin="round"
609
583
  className={cn("size-3.5", className)}
@@ -624,7 +598,7 @@ export function IconSizingFill({ className }: IconProps) {
624
598
  viewBox="0 0 24 24"
625
599
  fill="none"
626
600
  stroke="currentColor"
627
- strokeWidth={1.6}
601
+ strokeWidth={1.5}
628
602
  strokeLinecap="round"
629
603
  strokeLinejoin="round"
630
604
  className={cn("size-3.5", className)}
@@ -649,7 +623,7 @@ export function IconSizingMin({ className }: IconProps) {
649
623
  viewBox="0 0 24 24"
650
624
  fill="none"
651
625
  stroke="currentColor"
652
- strokeWidth={1.6}
626
+ strokeWidth={1.5}
653
627
  strokeLinecap="round"
654
628
  strokeLinejoin="round"
655
629
  className={cn("size-3.5", className)}
@@ -673,7 +647,7 @@ export function IconSizingMax({ className }: IconProps) {
673
647
  viewBox="0 0 24 24"
674
648
  fill="none"
675
649
  stroke="currentColor"
676
- strokeWidth={1.6}
650
+ strokeWidth={1.5}
677
651
  strokeLinecap="round"
678
652
  strokeLinejoin="round"
679
653
  className={cn("size-3.5", className)}
@@ -697,7 +671,7 @@ export function IconSizingVariable({ className }: IconProps) {
697
671
  viewBox="0 0 24 24"
698
672
  fill="none"
699
673
  stroke="currentColor"
700
- strokeWidth={1.6}
674
+ strokeWidth={1.5}
701
675
  strokeLinecap="round"
702
676
  strokeLinejoin="round"
703
677
  className={cn("size-3.5", className)}
@@ -196,6 +196,10 @@ export interface MultiScreenCanvasProps {
196
196
  }) => void;
197
197
  onCreateScreenFrame?: (geometry: FrameGeometry) => void;
198
198
  onDeleteSelection?: (ids: string[]) => boolean | void;
199
+ /** Return false to keep the arrow key: the host's real selection is an
200
+ * element inside a screen, and this canvas still lists that screen in
201
+ * `selectedIds`. Same veto `onDeleteSelection` uses. */
202
+ onNudgeSelection?: (ids: string[]) => boolean | void;
199
203
  onZoomChange?: (zoom: number) => void;
200
204
  renderScreenContent?: (
201
205
  screen: ScreenFile,
@@ -0,0 +1,75 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ import {
4
+ DEFAULT_EDITOR_PREFERENCES,
5
+ DESIGN_EDITOR_PREFERENCES_STORAGE_KEY,
6
+ parseEditorPreferences,
7
+ serializeEditorPreferences,
8
+ type DesignEditorPreferences,
9
+ } from "@/pages/design-editor/editor-preferences";
10
+
11
+ function readStoredPreferences(): DesignEditorPreferences {
12
+ if (typeof window === "undefined") return DEFAULT_EDITOR_PREFERENCES;
13
+ let raw: string | null;
14
+ try {
15
+ raw = window.localStorage.getItem(DESIGN_EDITOR_PREFERENCES_STORAGE_KEY);
16
+ } catch {
17
+ return DEFAULT_EDITOR_PREFERENCES;
18
+ }
19
+ const result = parseEditorPreferences(raw);
20
+ if (result.status === "invalid") {
21
+ console.warn(
22
+ `[design] discarding corrupt editor preferences: ${result.reason}`,
23
+ );
24
+ try {
25
+ window.localStorage.removeItem(DESIGN_EDITOR_PREFERENCES_STORAGE_KEY);
26
+ } catch (error) {
27
+ console.warn("[design] could not clear editor preferences", error);
28
+ }
29
+ }
30
+ return result.preferences;
31
+ }
32
+
33
+ export interface UseEditorPreferences {
34
+ preferences: DesignEditorPreferences;
35
+ setPreferences: (next: DesignEditorPreferences) => void;
36
+ }
37
+
38
+ /** Editor-chrome preferences (nudge amounts today) live in localStorage, not
39
+ * SQL: they are per-device input tuning, and a shared design opened by a
40
+ * collaborator must keep that collaborator's own nudge amounts. */
41
+ export function useEditorPreferences(): UseEditorPreferences {
42
+ const [preferences, setPreferencesState] = useState<DesignEditorPreferences>(
43
+ DEFAULT_EDITOR_PREFERENCES,
44
+ );
45
+
46
+ useEffect(() => {
47
+ setPreferencesState(readStoredPreferences());
48
+ }, []);
49
+
50
+ useEffect(() => {
51
+ if (typeof window === "undefined") return;
52
+ const onStorage = (event: StorageEvent) => {
53
+ if (event.key !== DESIGN_EDITOR_PREFERENCES_STORAGE_KEY) return;
54
+ setPreferencesState(readStoredPreferences());
55
+ };
56
+ window.addEventListener("storage", onStorage);
57
+ return () => window.removeEventListener("storage", onStorage);
58
+ }, []);
59
+
60
+ const setPreferences = useCallback((next: DesignEditorPreferences) => {
61
+ const serialized = serializeEditorPreferences(next);
62
+ setPreferencesState(parseEditorPreferences(serialized).preferences);
63
+ if (typeof window === "undefined") return;
64
+ try {
65
+ window.localStorage.setItem(
66
+ DESIGN_EDITOR_PREFERENCES_STORAGE_KEY,
67
+ serialized,
68
+ );
69
+ } catch (error) {
70
+ console.warn("[design] could not persist editor preferences", error);
71
+ }
72
+ }, []);
73
+
74
+ return { preferences, setPreferences };
75
+ }
@@ -746,6 +746,14 @@ const enUS = {
746
746
  close: "Close keyboard shortcuts",
747
747
  codeContext: "Code",
748
748
  screenContext: "Screen",
749
+ nudgeAmount: {
750
+ title: "Nudge amount",
751
+ small: "Small nudge",
752
+ big: "Big nudge",
753
+ unit: "px",
754
+ description:
755
+ "Arrow keys move by the small amount and Shift+arrow by the big one. Inside an auto layout frame, arrows reorder the layer instead of moving it.",
756
+ },
749
757
  keys: {
750
758
  or: "or",
751
759
  command: "Command",
@@ -12,6 +12,10 @@ interface KeyboardShortcutLabels {
12
12
  hideUiDescription: string;
13
13
  undoDescription: string;
14
14
  redoDescription: string;
15
+ nudgeAmountTitle: string;
16
+ nudgeAmountSmall: string;
17
+ nudgeAmountBig: string;
18
+ nudgeAmountDescription: string;
15
19
  }
16
20
 
17
21
  interface KeyboardKeyLabels {
@@ -50,6 +54,11 @@ export const keyboardShortcutLabels = {
50
54
  hideUiDescription: "立即按下以快速隱藏面板並專注於工作",
51
55
  undoDescription: "逐步回復最近的設計變更",
52
56
  redoDescription: "還原剛才復原的設計變更",
57
+ nudgeAmountTitle: "微調距離",
58
+ nudgeAmountSmall: "小距離",
59
+ nudgeAmountBig: "大距離",
60
+ nudgeAmountDescription:
61
+ "方向鍵以小距離移動,Shift+方向鍵以大距離移動。在自動版面配置框架內,方向鍵改為調整圖層順序。",
53
62
  },
54
63
  "zh-CN": {
55
64
  title: "键盘快捷键",
@@ -63,6 +72,11 @@ export const keyboardShortcutLabels = {
63
72
  hideUiDescription: "立即按下以快速隐藏面板并专注于工作",
64
73
  undoDescription: "逐步撤销最近的设计更改",
65
74
  redoDescription: "恢复刚刚撤销的设计更改",
75
+ nudgeAmountTitle: "微调距离",
76
+ nudgeAmountSmall: "小距离",
77
+ nudgeAmountBig: "大距离",
78
+ nudgeAmountDescription:
79
+ "方向键以小距离移动,Shift+方向键以大距离移动。在自动布局框架内,方向键改为调整图层顺序。",
66
80
  },
67
81
  "es-ES": {
68
82
  title: "Atajos de teclado",
@@ -77,6 +91,11 @@ export const keyboardShortcutLabels = {
77
91
  "Púlsalo ahora para ocultar los paneles y concentrarte en tu trabajo",
78
92
  undoDescription: "Retrocede por el cambio de diseño más reciente",
79
93
  redoDescription: "Restaura el cambio de diseño que acabas de deshacer",
94
+ nudgeAmountTitle: "Cantidad de desplazamiento",
95
+ nudgeAmountSmall: "Desplazamiento pequeño",
96
+ nudgeAmountBig: "Desplazamiento grande",
97
+ nudgeAmountDescription:
98
+ "Las flechas mueven la cantidad pequeña y Mayús+flecha la grande. Dentro de un marco de autodiseño, las flechas reordenan la capa.",
80
99
  },
81
100
  "fr-FR": {
82
101
  title: "Raccourcis clavier",
@@ -92,6 +111,11 @@ export const keyboardShortcutLabels = {
92
111
  undoDescription: "Revenez sur votre dernière modification de design",
93
112
  redoDescription:
94
113
  "Restaurez la modification de design que vous venez d’annuler",
114
+ nudgeAmountTitle: "Valeur de déplacement",
115
+ nudgeAmountSmall: "Petit déplacement",
116
+ nudgeAmountBig: "Grand déplacement",
117
+ nudgeAmountDescription:
118
+ "Les flèches déplacent de la petite valeur et Maj+flèche de la grande. Dans un cadre en disposition automatique, les flèches réorganisent le calque.",
95
119
  },
96
120
  "de-DE": {
97
121
  title: "Tastenkürzel",
@@ -107,6 +131,11 @@ export const keyboardShortcutLabels = {
107
131
  undoDescription: "Mache die letzte Designänderung rückgängig",
108
132
  redoDescription:
109
133
  "Stelle die soeben rückgängig gemachte Designänderung wieder her",
134
+ nudgeAmountTitle: "Verschiebeschrittweite",
135
+ nudgeAmountSmall: "Kleiner Schritt",
136
+ nudgeAmountBig: "Großer Schritt",
137
+ nudgeAmountDescription:
138
+ "Pfeiltasten verschieben um den kleinen Wert, Umschalt+Pfeil um den großen. In einem Auto-Layout-Rahmen ordnen die Pfeiltasten die Ebene neu an.",
110
139
  },
111
140
  "ja-JP": {
112
141
  title: "キーボードショートカット",
@@ -120,6 +149,11 @@ export const keyboardShortcutLabels = {
120
149
  hideUiDescription: "今すぐ押してパネルを隠し、作業に集中できます",
121
150
  undoDescription: "直前のデザイン変更を元に戻します",
122
151
  redoDescription: "元に戻したデザイン変更を復元します",
152
+ nudgeAmountTitle: "移動量",
153
+ nudgeAmountSmall: "小さい移動量",
154
+ nudgeAmountBig: "大きい移動量",
155
+ nudgeAmountDescription:
156
+ "矢印キーは小さい移動量、Shift+矢印キーは大きい移動量で移動します。オートレイアウトフレーム内では、矢印キーはレイヤーの順序を変更します。",
123
157
  },
124
158
  "ko-KR": {
125
159
  title: "키보드 단축키",
@@ -133,6 +167,11 @@ export const keyboardShortcutLabels = {
133
167
  hideUiDescription: "지금 눌러 패널을 빠르게 숨기고 작업에 집중하세요",
134
168
  undoDescription: "가장 최근 디자인 변경을 되돌립니다",
135
169
  redoDescription: "방금 실행 취소한 디자인 변경을 복원합니다",
170
+ nudgeAmountTitle: "이동 간격",
171
+ nudgeAmountSmall: "작은 이동",
172
+ nudgeAmountBig: "큰 이동",
173
+ nudgeAmountDescription:
174
+ "화살표 키는 작은 간격으로, Shift+화살표 키는 큰 간격으로 이동합니다. 오토 레이아웃 프레임 안에서는 화살표 키가 레이어 순서를 변경합니다.",
136
175
  },
137
176
  "pt-BR": {
138
177
  title: "Atalhos de teclado",
@@ -148,6 +187,11 @@ export const keyboardShortcutLabels = {
148
187
  undoDescription: "Desfaça a alteração de design mais recente",
149
188
  redoDescription:
150
189
  "Restaure a alteração de design que você acabou de desfazer",
190
+ nudgeAmountTitle: "Distância de deslocamento",
191
+ nudgeAmountSmall: "Deslocamento pequeno",
192
+ nudgeAmountBig: "Deslocamento grande",
193
+ nudgeAmountDescription:
194
+ "As setas movem pela distância pequena e Shift+seta pela grande. Dentro de um frame com layout automático, as setas reordenam a camada.",
151
195
  },
152
196
  "hi-IN": {
153
197
  title: "कीबोर्ड शॉर्टकट",
@@ -161,6 +205,11 @@ export const keyboardShortcutLabels = {
161
205
  hideUiDescription: "पैनल तुरंत छिपाकर अपने काम पर ध्यान देने के लिए इसे अभी दबाएँ",
162
206
  undoDescription: "सबसे हाल के डिज़ाइन बदलाव को वापस करें",
163
207
  redoDescription: "अभी वापस किए गए डिज़ाइन बदलाव को फिर से लागू करें",
208
+ nudgeAmountTitle: "नज की दूरी",
209
+ nudgeAmountSmall: "छोटी नज",
210
+ nudgeAmountBig: "बड़ी नज",
211
+ nudgeAmountDescription:
212
+ "ऐरो कुंजियाँ छोटी दूरी से और Shift+ऐरो बड़ी दूरी से ले जाती हैं। ऑटो लेआउट फ़्रेम के अंदर ऐरो कुंजियाँ लेयर का क्रम बदलती हैं।",
164
213
  },
165
214
  "ar-SA": {
166
215
  title: "اختصارات لوحة المفاتيح",
@@ -174,6 +223,11 @@ export const keyboardShortcutLabels = {
174
223
  hideUiDescription: "اضغطه الآن لإخفاء اللوحات بسرعة والتركيز على عملك",
175
224
  undoDescription: "تراجع عن أحدث تغيير في التصميم",
176
225
  redoDescription: "استعد تغيير التصميم الذي تراجعت عنه للتو",
226
+ nudgeAmountTitle: "مقدار الإزاحة",
227
+ nudgeAmountSmall: "إزاحة صغيرة",
228
+ nudgeAmountBig: "إزاحة كبيرة",
229
+ nudgeAmountDescription:
230
+ "تحرّك مفاتيح الأسهم بالمقدار الصغير، وShift+سهم بالمقدار الكبير. داخل إطار التخطيط التلقائي، تعيد مفاتيح الأسهم ترتيب الطبقة.",
177
231
  },
178
232
  } satisfies Record<Exclude<LocaleCode, "en-US">, KeyboardShortcutLabels>;
179
233
 
@@ -483,6 +537,13 @@ export function attachLocalizedKeyboardShortcuts<
483
537
  close: `${d.close}: ${labels.title}`,
484
538
  codeContext: edit.sections.codeConfidence,
485
539
  screenContext: layers.screens,
540
+ nudgeAmount: {
541
+ title: labels.nudgeAmountTitle,
542
+ small: labels.nudgeAmountSmall,
543
+ big: labels.nudgeAmountBig,
544
+ unit: "px",
545
+ description: labels.nudgeAmountDescription,
546
+ },
486
547
  keys: keyLabels,
487
548
  descriptions: {
488
549
  toggleUi: labels.hideUiDescription,
@@ -0,0 +1,98 @@
1
+ import { DEFAULT_NUDGE_AMOUNTS, type NudgeAmounts } from "./nudge-intent";
2
+
3
+ export const DESIGN_EDITOR_PREFERENCES_STORAGE_KEY =
4
+ "agent-native.design.editor-preferences";
5
+
6
+ export interface DesignEditorPreferences {
7
+ nudge: NudgeAmounts;
8
+ }
9
+
10
+ export const DEFAULT_EDITOR_PREFERENCES: DesignEditorPreferences = {
11
+ nudge: DEFAULT_NUDGE_AMOUNTS,
12
+ };
13
+
14
+ /** Figma allows 1-1000 for both nudge amounts and rejects 0 — a 0 nudge makes
15
+ * the arrow keys look broken rather than doing nothing on purpose. */
16
+ export const MIN_NUDGE_AMOUNT = 1;
17
+ export const MAX_NUDGE_AMOUNT = 1000;
18
+
19
+ export function normalizeNudgeAmount(value: unknown, fallback: number): number {
20
+ const parsed = typeof value === "string" ? Number(value) : value;
21
+ if (typeof parsed !== "number" || !Number.isFinite(parsed)) return fallback;
22
+ return Math.min(
23
+ MAX_NUDGE_AMOUNT,
24
+ Math.max(MIN_NUDGE_AMOUNT, Math.round(parsed)),
25
+ );
26
+ }
27
+
28
+ export type ParseEditorPreferencesResult =
29
+ | { status: "absent"; preferences: DesignEditorPreferences }
30
+ | { status: "ok"; preferences: DesignEditorPreferences }
31
+ | {
32
+ status: "invalid";
33
+ preferences: DesignEditorPreferences;
34
+ reason: string;
35
+ };
36
+
37
+ /** Never collapses "nothing stored yet" and "stored value is corrupt" into the
38
+ * same result: the caller must be able to tell a first run from a store it
39
+ * should overwrite rather than keep re-reading. */
40
+ export function parseEditorPreferences(
41
+ raw: string | null | undefined,
42
+ ): ParseEditorPreferencesResult {
43
+ if (raw === null || raw === undefined || raw === "") {
44
+ return { status: "absent", preferences: DEFAULT_EDITOR_PREFERENCES };
45
+ }
46
+ let decoded: unknown;
47
+ try {
48
+ decoded = JSON.parse(raw);
49
+ } catch (error) {
50
+ return {
51
+ status: "invalid",
52
+ preferences: DEFAULT_EDITOR_PREFERENCES,
53
+ reason: String(error),
54
+ };
55
+ }
56
+ if (typeof decoded !== "object" || decoded === null) {
57
+ return {
58
+ status: "invalid",
59
+ preferences: DEFAULT_EDITOR_PREFERENCES,
60
+ reason: "expected an object",
61
+ };
62
+ }
63
+ const nudge = (decoded as { nudge?: unknown }).nudge;
64
+ if (nudge !== undefined && (typeof nudge !== "object" || nudge === null)) {
65
+ return {
66
+ status: "invalid",
67
+ preferences: DEFAULT_EDITOR_PREFERENCES,
68
+ reason: "expected nudge to be an object",
69
+ };
70
+ }
71
+ const source = (nudge ?? {}) as { small?: unknown; big?: unknown };
72
+ return {
73
+ status: "ok",
74
+ preferences: {
75
+ nudge: {
76
+ small: normalizeNudgeAmount(source.small, DEFAULT_NUDGE_AMOUNTS.small),
77
+ big: normalizeNudgeAmount(source.big, DEFAULT_NUDGE_AMOUNTS.big),
78
+ },
79
+ },
80
+ };
81
+ }
82
+
83
+ export function serializeEditorPreferences(
84
+ preferences: DesignEditorPreferences,
85
+ ): string {
86
+ return JSON.stringify({
87
+ nudge: {
88
+ small: normalizeNudgeAmount(
89
+ preferences.nudge.small,
90
+ DEFAULT_NUDGE_AMOUNTS.small,
91
+ ),
92
+ big: normalizeNudgeAmount(
93
+ preferences.nudge.big,
94
+ DEFAULT_NUDGE_AMOUNTS.big,
95
+ ),
96
+ },
97
+ });
98
+ }