@agent-native/core 0.80.0 → 0.80.2

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 (228) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  4. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +14 -1
  5. package/corpus/core/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  6. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +15 -1
  7. package/corpus/core/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  8. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +15 -1
  9. package/corpus/core/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  10. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +15 -1
  11. package/corpus/core/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  12. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +15 -1
  13. package/corpus/core/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  14. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +16 -1
  15. package/corpus/core/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  16. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +14 -1
  17. package/corpus/core/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  18. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +14 -1
  19. package/corpus/core/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  20. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +14 -1
  21. package/corpus/core/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  22. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +14 -1
  23. package/corpus/core/docs/content/skills-guide.mdx +5 -0
  24. package/corpus/core/docs/content/template-design.mdx +14 -1
  25. package/corpus/core/package.json +1 -1
  26. package/corpus/core/src/application-state/handlers.ts +1 -1
  27. package/corpus/core/src/brand-kit/index.ts +1 -1
  28. package/corpus/core/src/cli/skills.ts +18 -0
  29. package/corpus/core/src/client/AgentPanel.tsx +1 -1
  30. package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
  31. package/corpus/core/src/client/extensions/portable-extension.ts +1 -1
  32. package/corpus/core/src/code-agents/transcript-normalizer.ts +1 -1
  33. package/corpus/core/src/extensions/path.ts +1 -4
  34. package/corpus/core/src/integrations/a2a-continuation-processor.ts +1 -1
  35. package/corpus/core/src/mcp/build-server.ts +2 -2
  36. package/corpus/core/src/mcp/embed-app.ts +35 -1
  37. package/corpus/core/src/org/handlers.ts +5 -5
  38. package/corpus/core/src/org/plugin.ts +2 -2
  39. package/corpus/core/src/scripts/call-agent.ts +1 -1
  40. package/corpus/core/src/scripts/db/safety.ts +1 -1
  41. package/corpus/core/src/server/auth.ts +3 -0
  42. package/corpus/core/src/server/builder-design-systems.ts +308 -0
  43. package/corpus/core/src/server/core-routes-plugin.ts +29 -3
  44. package/corpus/core/src/server/index.ts +8 -0
  45. package/corpus/core/src/styles/agent-native.css +14 -7
  46. package/corpus/core/src/usage/store.ts +2 -2
  47. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +298 -15
  48. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +9 -4
  49. package/corpus/templates/analytics/actions/mutate-dashboard.ts +18 -2
  50. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +12 -2
  51. package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +66 -0
  52. package/corpus/templates/analytics/app/i18n/zh-TW.ts +37 -0
  53. package/corpus/templates/analytics/app/i18n-data.ts +404 -0
  54. package/corpus/templates/analytics/app/pages/Settings.tsx +333 -0
  55. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -1
  56. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +121 -3
  57. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +49 -26
  58. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +137 -36
  59. package/corpus/templates/analytics/changelog/2026-06-29-analytics-agents-can-now-place-new-charts-into-requested-das.md +6 -0
  60. package/corpus/templates/analytics/changelog/2026-06-29-analytics-queries-now-report-failed-source-query-errors-inst.md +6 -0
  61. package/corpus/templates/analytics/changelog/2026-06-29-charts-can-now-be-dropped-between-full-dashboard-rows-to-cre.md +6 -0
  62. package/corpus/templates/analytics/changelog/2026-06-29-dashboard-email-reports-include-the-full-dashboard-image-wit.md +6 -0
  63. package/corpus/templates/analytics/changelog/2026-06-29-main-surface-border-reaches-the-top.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-06-29-session-replay-now-records-playable-sessions-in-production-a.md +6 -0
  65. package/corpus/templates/analytics/changelog/2026-06-29-set-up-session-replay-storage-from-settings-connect-builder-.md +6 -0
  66. package/corpus/templates/analytics/scripts/cleanup-empty-session-recordings.ts +102 -0
  67. package/corpus/templates/analytics/server/lib/dashboard-report.ts +49 -26
  68. package/corpus/templates/analytics/server/lib/real-data-actions.ts +81 -20
  69. package/corpus/templates/analytics/server/lib/replay-storage.ts +79 -0
  70. package/corpus/templates/analytics/server/lib/s3-upload-provider.ts +255 -0
  71. package/corpus/templates/analytics/server/lib/session-replay.ts +29 -8
  72. package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -2
  73. package/corpus/templates/analytics/server/plugins/onboarding.ts +97 -0
  74. package/corpus/templates/clips/desktop/src/lib/recorder.ts +24 -0
  75. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +105 -5
  76. package/corpus/templates/content/actions/set-image-alt-text.ts +1 -1
  77. package/corpus/templates/content/shared/document-export.ts +1 -1
  78. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -18
  79. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +17 -0
  80. package/corpus/templates/design/actions/add-localhost-screens.ts +32 -5
  81. package/corpus/templates/design/actions/index-design-system-with-builder.ts +259 -0
  82. package/corpus/templates/design/actions/update-file.ts +9 -2
  83. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +33 -75
  84. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +72 -9
  85. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +41 -39
  86. package/corpus/templates/design/app/components/design/EditPanel.tsx +1029 -249
  87. package/corpus/templates/design/app/components/design/LayersPanel.tsx +450 -101
  88. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +978 -154
  89. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +7 -9
  90. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +133 -66
  91. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +1234 -344
  92. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +144 -94
  93. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +301 -72
  94. package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +876 -286
  95. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +601 -0
  96. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +242 -0
  97. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +30 -7
  98. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +212 -155
  99. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +423 -0
  100. package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +487 -107
  101. package/corpus/templates/design/app/components/design/inspector/index.ts +27 -0
  102. package/corpus/templates/design/app/global.css +29 -3
  103. package/corpus/templates/design/app/i18n/zh-TW.ts +13 -0
  104. package/corpus/templates/design/app/i18n-data.ts +239 -1
  105. package/corpus/templates/design/app/pages/DesignEditor.tsx +1023 -353
  106. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +57 -221
  107. package/corpus/templates/design/changelog/2026-06-29-canvas-selection-no-longer-flashes-broken-prototype-html-whe.md +6 -0
  108. package/corpus/templates/design/changelog/2026-06-29-command-menu-selections-are-clearer-in-dark-mode.md +6 -0
  109. package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +6 -0
  110. package/corpus/templates/design/changelog/2026-06-29-element-selection-now-stays-on-the-intended-layer-after-sele.md +6 -0
  111. package/corpus/templates/design/changelog/2026-06-29-screen-overview-full-view-controls-now-sit-above-frames-inst.md +6 -0
  112. package/corpus/templates/design/changelog/2026-06-29-screen-overview-tools-can-draw-shapes-create-screens-and-kee.md +6 -0
  113. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-now-creates-anchor-based-bezier-vectors-with-ha.md +6 -0
  114. package/corpus/templates/design/server/handlers/import-figma-system.ts +38 -85
  115. package/corpus/templates/design/server/lib/design-snapshot.ts +9 -1
  116. package/corpus/templates/design/shared/code-layer.ts +209 -9
  117. package/corpus/templates/design/shared/html-content.ts +21 -0
  118. package/corpus/templates/design/shared/pen-path.ts +207 -0
  119. package/corpus/templates/mail/app/lib/utils.ts +2 -2
  120. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +9 -10
  121. package/corpus/templates/slides/AGENTS.md +3 -2
  122. package/corpus/templates/slides/actions/import-file.ts +26 -17
  123. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +259 -0
  124. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +35 -167
  125. package/corpus/templates/slides/app/components/design-system/fig-import-response.ts +9 -13
  126. package/corpus/templates/slides/app/context/DeckContext.tsx +1 -1
  127. package/corpus/templates/slides/app/i18n/ar-SA.ts +9 -37
  128. package/corpus/templates/slides/app/i18n/de-DE.ts +10 -25
  129. package/corpus/templates/slides/app/i18n/en-US.ts +9 -26
  130. package/corpus/templates/slides/app/i18n/es-ES.ts +10 -28
  131. package/corpus/templates/slides/app/i18n/fr-FR.ts +10 -28
  132. package/corpus/templates/slides/app/i18n/hi-IN.ts +9 -26
  133. package/corpus/templates/slides/app/i18n/ja-JP.ts +10 -22
  134. package/corpus/templates/slides/app/i18n/ko-KR.ts +9 -23
  135. package/corpus/templates/slides/app/i18n/pt-BR.ts +10 -28
  136. package/corpus/templates/slides/app/i18n/zh-CN.ts +9 -22
  137. package/corpus/templates/slides/app/i18n/zh-TW.ts +9 -22
  138. package/corpus/templates/slides/server/handlers/import-figma-system.ts +44 -12
  139. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  140. package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +1 -1
  141. package/dist/application-state/handlers.js +1 -1
  142. package/dist/application-state/handlers.js.map +1 -1
  143. package/dist/brand-kit/index.d.ts +1 -1
  144. package/dist/brand-kit/index.js +1 -1
  145. package/dist/brand-kit/index.js.map +1 -1
  146. package/dist/cli/skills.d.ts.map +1 -1
  147. package/dist/cli/skills.js +18 -0
  148. package/dist/cli/skills.js.map +1 -1
  149. package/dist/client/AgentPanel.js +1 -1
  150. package/dist/client/AgentPanel.js.map +1 -1
  151. package/dist/client/MultiTabAssistantChat.js +1 -1
  152. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  153. package/dist/client/extensions/portable-extension.d.ts +1 -1
  154. package/dist/client/extensions/portable-extension.d.ts.map +1 -1
  155. package/dist/client/extensions/portable-extension.js.map +1 -1
  156. package/dist/code-agents/transcript-normalizer.js +1 -1
  157. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  158. package/dist/collab/awareness.d.ts +2 -2
  159. package/dist/collab/awareness.d.ts.map +1 -1
  160. package/dist/collab/routes.d.ts +1 -1
  161. package/dist/extensions/path.d.ts +1 -1
  162. package/dist/extensions/path.d.ts.map +1 -1
  163. package/dist/extensions/path.js.map +1 -1
  164. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  165. package/dist/integrations/a2a-continuation-processor.js +1 -1
  166. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  167. package/dist/mcp/build-server.d.ts +1 -1
  168. package/dist/mcp/build-server.d.ts.map +1 -1
  169. package/dist/mcp/build-server.js +1 -1
  170. package/dist/mcp/build-server.js.map +1 -1
  171. package/dist/mcp/embed-app.d.ts.map +1 -1
  172. package/dist/mcp/embed-app.js +35 -1
  173. package/dist/mcp/embed-app.js.map +1 -1
  174. package/dist/notifications/routes.d.ts +3 -3
  175. package/dist/observability/routes.d.ts +8 -8
  176. package/dist/org/handlers.js +5 -5
  177. package/dist/org/handlers.js.map +1 -1
  178. package/dist/org/plugin.js +2 -2
  179. package/dist/org/plugin.js.map +1 -1
  180. package/dist/resources/handlers.d.ts +1 -1
  181. package/dist/scripts/call-agent.js +1 -1
  182. package/dist/scripts/call-agent.js.map +1 -1
  183. package/dist/scripts/db/safety.js +1 -1
  184. package/dist/scripts/db/safety.js.map +1 -1
  185. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  186. package/dist/server/auth.d.ts.map +1 -1
  187. package/dist/server/auth.js +3 -0
  188. package/dist/server/auth.js.map +1 -1
  189. package/dist/server/builder-design-systems.d.ts +28 -0
  190. package/dist/server/builder-design-systems.d.ts.map +1 -0
  191. package/dist/server/builder-design-systems.js +217 -0
  192. package/dist/server/builder-design-systems.js.map +1 -0
  193. package/dist/server/core-routes-plugin.d.ts +1 -0
  194. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  195. package/dist/server/core-routes-plugin.js +20 -3
  196. package/dist/server/core-routes-plugin.js.map +1 -1
  197. package/dist/server/index.d.ts +1 -0
  198. package/dist/server/index.d.ts.map +1 -1
  199. package/dist/server/index.js +1 -0
  200. package/dist/server/index.js.map +1 -1
  201. package/dist/styles/agent-native.css +14 -7
  202. package/dist/usage/store.js +2 -2
  203. package/dist/usage/store.js.map +1 -1
  204. package/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  205. package/docs/content/locales/ar-SA/template-design.mdx +14 -1
  206. package/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  207. package/docs/content/locales/de-DE/template-design.mdx +15 -1
  208. package/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  209. package/docs/content/locales/es-ES/template-design.mdx +15 -1
  210. package/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  211. package/docs/content/locales/fr-FR/template-design.mdx +15 -1
  212. package/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  213. package/docs/content/locales/hi-IN/template-design.mdx +15 -1
  214. package/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  215. package/docs/content/locales/ja-JP/template-design.mdx +16 -1
  216. package/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  217. package/docs/content/locales/ko-KR/template-design.mdx +14 -1
  218. package/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  219. package/docs/content/locales/pt-BR/template-design.mdx +14 -1
  220. package/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  221. package/docs/content/locales/zh-CN/template-design.mdx +14 -1
  222. package/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  223. package/docs/content/locales/zh-TW/template-design.mdx +14 -1
  224. package/docs/content/skills-guide.mdx +5 -0
  225. package/docs/content/template-design.mdx +14 -1
  226. package/package.json +1 -1
  227. package/corpus/templates/design/actions/import-figma-file.ts +0 -166
  228. package/corpus/templates/slides/server/lib/fig-design-system.ts +0 -110
@@ -1,4 +1,5 @@
1
1
  import { IconDownload, IconPlus, IconX } from "@tabler/icons-react";
2
+ import { useState } from "react";
2
3
 
3
4
  import { Button } from "@/components/ui/button";
4
5
  import { Input } from "@/components/ui/input";
@@ -12,9 +13,10 @@ import {
12
13
  } from "@/components/ui/select";
13
14
  import { cn } from "@/lib/utils";
14
15
 
15
- import { ScrubInput } from "./ScrubInput";
16
+ export type ExportFormat = "png" | "jpg" | "svg" | "pdf" | "webp";
16
17
 
17
- export type ExportFormat = "png" | "jpg" | "svg" | "pdf";
18
+ /** Scale preset value "custom" means the user typed a freeform multiplier */
19
+ export type ExportScale = "0.5" | "1" | "2" | "3" | "4" | "custom";
18
20
 
19
21
  export interface ExportSettingsValue {
20
22
  scale: number;
@@ -28,6 +30,8 @@ export interface ExportSettingsPanelLabels {
28
30
  format: string;
29
31
  suffix: string;
30
32
  export: string;
33
+ addExport: string;
34
+ removeExport: string;
31
35
  }
32
36
 
33
37
  export interface ExportSettingsPanelProps {
@@ -47,9 +51,100 @@ const DEFAULT_LABELS: ExportSettingsPanelLabels = {
47
51
  format: "Format", // i18n-ignore fallback component label
48
52
  suffix: "Suffix", // i18n-ignore fallback component label
49
53
  export: "Export", // i18n-ignore fallback component label
54
+ addExport: "Add export", // i18n-ignore fallback component label
55
+ removeExport: "Remove export", // i18n-ignore fallback component label
50
56
  };
51
57
 
52
- const DEFAULT_FORMATS: ExportFormat[] = ["png", "jpg", "svg", "pdf"];
58
+ const DEFAULT_FORMATS: ExportFormat[] = ["png", "jpg", "svg", "pdf", "webp"];
59
+
60
+ /** Preset scale options shown in the scale dropdown — matches Figma's presets */
61
+ const SCALE_PRESETS: { label: string; value: ExportScale }[] = [
62
+ { label: "0.5x", value: "0.5" },
63
+ { label: "1x", value: "1" },
64
+ { label: "2x", value: "2" },
65
+ { label: "3x", value: "3" },
66
+ { label: "4x", value: "4" },
67
+ { label: "Custom…", value: "custom" }, // i18n-ignore fixed scale preset label
68
+ ];
69
+
70
+ /**
71
+ * Map a numeric scale to the nearest preset key, or "custom" if it doesn't
72
+ * match any of the five standard multipliers.
73
+ */
74
+ function scaleToPreset(scale: number): ExportScale {
75
+ const hit = SCALE_PRESETS.find(
76
+ (p) => p.value !== "custom" && Number(p.value) === scale,
77
+ );
78
+ return hit ? hit.value : "custom";
79
+ }
80
+
81
+ /** A single in-progress export row (multi-row internal state) */
82
+ interface ExportRow {
83
+ id: number;
84
+ scale: number;
85
+ format: ExportFormat;
86
+ suffix: string;
87
+ /** Whether the user has switched to freeform scale entry */
88
+ customScale: boolean;
89
+ }
90
+
91
+ let _nextId = 1;
92
+ function nextId() {
93
+ return _nextId++;
94
+ }
95
+
96
+ function rowFromValue(v: ExportSettingsValue): ExportRow {
97
+ return {
98
+ id: nextId(),
99
+ scale: v.scale,
100
+ format: v.format,
101
+ suffix: v.suffix,
102
+ customScale: scaleToPreset(v.scale) === "custom",
103
+ };
104
+ }
105
+
106
+ /** Scale dropdown — shows Figma's 0.5x/1x/2x/3x/4x presets + Custom */
107
+ function ScaleSelect({
108
+ scale,
109
+ customScale,
110
+ disabled,
111
+ onPresetChange,
112
+ }: {
113
+ scale: number;
114
+ customScale: boolean;
115
+ disabled: boolean;
116
+ onPresetChange: (preset: ExportScale, raw: number) => void;
117
+ }) {
118
+ const preset = customScale ? "custom" : scaleToPreset(scale);
119
+
120
+ return (
121
+ <Select
122
+ value={preset}
123
+ disabled={disabled}
124
+ onValueChange={(v) => {
125
+ const p = v as ExportScale;
126
+ if (p === "custom") {
127
+ onPresetChange("custom", scale);
128
+ } else {
129
+ onPresetChange(p, Number(p));
130
+ }
131
+ }}
132
+ >
133
+ <SelectTrigger className="h-6 w-14 shrink-0 px-1.5 text-[11px]">
134
+ <SelectValue />
135
+ </SelectTrigger>
136
+ <SelectContent>
137
+ <SelectGroup>
138
+ {SCALE_PRESETS.map((p) => (
139
+ <SelectItem key={p.value} value={p.value} className="text-[11px]">
140
+ {p.label}
141
+ </SelectItem>
142
+ ))}
143
+ </SelectGroup>
144
+ </SelectContent>
145
+ </Select>
146
+ );
147
+ }
53
148
 
54
149
  export function ExportSettingsPanel({
55
150
  value,
@@ -64,6 +159,83 @@ export function ExportSettingsPanel({
64
159
  const copy = { ...DEFAULT_LABELS, ...labels };
65
160
  const isDisabled = disabled || exporting;
66
161
 
162
+ // Multi-row internal state — primary row is kept in sync with the `value` prop
163
+ const [rows, setRows] = useState<ExportRow[]>(() => [rowFromValue(value)]);
164
+
165
+ // Keep primary row in sync when the controlled value prop changes externally
166
+ const primaryValue = rows[0];
167
+ const needsSync =
168
+ !primaryValue.customScale &&
169
+ (primaryValue.scale !== value.scale ||
170
+ primaryValue.format !== value.format ||
171
+ primaryValue.suffix !== value.suffix);
172
+
173
+ if (needsSync) {
174
+ setRows((prev) => [
175
+ {
176
+ ...prev[0],
177
+ scale: value.scale,
178
+ format: value.format,
179
+ suffix: value.suffix,
180
+ },
181
+ ...prev.slice(1),
182
+ ]);
183
+ }
184
+
185
+ function patchRow(id: number, patch: Partial<ExportRow>) {
186
+ setRows((prev) => prev.map((r) => (r.id === id ? { ...r, ...patch } : r)));
187
+ // Report primary row changes to the parent via `onChange`
188
+ if (id === rows[0]?.id) {
189
+ const next = { ...rows[0], ...patch };
190
+ const parent: Partial<ExportSettingsValue> = {};
191
+ if ("scale" in patch) parent.scale = next.scale;
192
+ if ("format" in patch) parent.format = next.format as ExportFormat;
193
+ if ("suffix" in patch) parent.suffix = next.suffix;
194
+ if (Object.keys(parent).length > 0) onChange(parent);
195
+ }
196
+ }
197
+
198
+ function addRow() {
199
+ setRows((prev) => [
200
+ ...prev,
201
+ {
202
+ id: nextId(),
203
+ scale: 1,
204
+ format: "png" as ExportFormat,
205
+ suffix: "",
206
+ customScale: false,
207
+ },
208
+ ]);
209
+ }
210
+
211
+ function removeRow(id: number) {
212
+ setRows((prev) => {
213
+ // Must keep at least one row
214
+ if (prev.length <= 1) return prev;
215
+ const next = prev.filter((r) => r.id !== id);
216
+ // If the primary (first) row was removed, the new first row becomes primary
217
+ if (prev[0]?.id === id && next[0]) {
218
+ onChange({
219
+ scale: next[0].scale,
220
+ format: next[0].format,
221
+ suffix: next[0].suffix,
222
+ });
223
+ }
224
+ return next;
225
+ });
226
+ }
227
+
228
+ function handleExport() {
229
+ // Pass the primary row's settings to onExport; rows array is internal
230
+ const primary = rows[0];
231
+ if (!primary) return;
232
+ onExport({
233
+ scale: primary.scale,
234
+ format: primary.format,
235
+ suffix: primary.suffix,
236
+ });
237
+ }
238
+
67
239
  return (
68
240
  <div className={cn("space-y-1.5", className)}>
69
241
  {/* Section header: title left, "+" right — matches Figma export header */}
@@ -73,89 +245,35 @@ export function ExportSettingsPanel({
73
245
  </span>
74
246
  <button
75
247
  type="button"
76
- aria-label={copy.export}
248
+ aria-label={copy.addExport}
77
249
  disabled={isDisabled}
78
250
  className="flex size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
79
- onClick={() => {
80
- /* single-value interface: noop — the setting already exists */
81
- }}
251
+ onClick={addRow}
82
252
  >
83
253
  <IconPlus className="size-3.5" />
84
254
  </button>
85
255
  </div>
86
256
 
87
- {/* Export row: [scale] [format] [suffix] [×] — Figma compact inline layout */}
88
- <div className="flex items-center gap-1">
89
- {/* Scale scrub — compact, no label, unit "x" shown inline */}
90
- <ScrubInput
91
- label=""
92
- value={value.scale}
93
- onChange={(scale) => onChange({ scale })}
94
- unit="x"
95
- min={0.1}
96
- max={10}
97
- step={0.5}
98
- precision={2}
99
- disabled={isDisabled}
100
- inputClassName="h-6 w-12 text-[11px]"
101
- className="shrink-0"
102
- />
103
-
104
- {/* Format dropdown */}
105
- <Select
106
- value={value.format}
107
- disabled={isDisabled}
108
- onValueChange={(format) =>
109
- onChange({ format: format as ExportFormat })
110
- }
111
- >
112
- <SelectTrigger className="h-6 min-w-0 flex-1 px-1.5 text-[11px]">
113
- <SelectValue />
114
- </SelectTrigger>
115
- <SelectContent>
116
- <SelectGroup>
117
- {formats.map((format) => (
118
- <SelectItem
119
- key={format}
120
- value={format}
121
- className="text-[11px] uppercase"
122
- >
123
- {format.toUpperCase()}
124
- </SelectItem>
125
- ))}
126
- </SelectGroup>
127
- </SelectContent>
128
- </Select>
129
-
130
- {/* Suffix input */}
131
- <Input
132
- value={value.suffix}
133
- disabled={isDisabled}
134
- onChange={(event) => onChange({ suffix: event.target.value })}
135
- placeholder="@2x"
136
- className="h-6 min-w-0 flex-1 px-1.5 text-[11px]"
257
+ {/* Export rows: [scale] [format] [suffix] [×] — Figma compact inline layout */}
258
+ {rows.map((row) => (
259
+ <ExportRow
260
+ key={row.id}
261
+ row={row}
262
+ formats={formats}
263
+ labels={copy}
264
+ isDisabled={isDisabled}
265
+ canRemove={rows.length > 1}
266
+ onPatchRow={patchRow}
267
+ onRemoveRow={removeRow}
137
268
  />
138
-
139
- {/* Remove row button — matches Figma's × on each export entry */}
140
- <button
141
- type="button"
142
- aria-label={copy.export}
143
- disabled={isDisabled}
144
- className="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
145
- onClick={() => {
146
- /* single-value interface: noop */
147
- }}
148
- >
149
- <IconX className="size-3" />
150
- </button>
151
- </div>
269
+ ))}
152
270
 
153
271
  {/* Export button — full width at bottom, Figma style */}
154
272
  <Button
155
273
  type="button"
156
274
  variant="outline"
157
275
  disabled={isDisabled}
158
- onClick={() => onExport(value)}
276
+ onClick={handleExport}
159
277
  className="h-6 w-full px-2 text-[11px]"
160
278
  >
161
279
  <IconDownload className="size-3.5" />
@@ -164,3 +282,114 @@ export function ExportSettingsPanel({
164
282
  </div>
165
283
  );
166
284
  }
285
+
286
+ /** A single export row: [scale ▾ or custom input] [format ▾] [suffix] [×] */
287
+ function ExportRow({
288
+ row,
289
+ formats,
290
+ labels,
291
+ isDisabled,
292
+ canRemove,
293
+ onPatchRow,
294
+ onRemoveRow,
295
+ }: {
296
+ row: ExportRow;
297
+ formats: ExportFormat[];
298
+ labels: ExportSettingsPanelLabels;
299
+ isDisabled: boolean;
300
+ canRemove: boolean;
301
+ onPatchRow: (id: number, patch: Partial<ExportRow>) => void;
302
+ onRemoveRow: (id: number) => void;
303
+ }) {
304
+ return (
305
+ <div className="flex items-center gap-1">
306
+ {/* Scale — dropdown (presets) or freeform input when "Custom…" selected */}
307
+ {row.customScale ? (
308
+ <Input
309
+ type="number"
310
+ value={row.scale}
311
+ disabled={isDisabled}
312
+ min={0.01}
313
+ max={100}
314
+ step={0.5}
315
+ onChange={(e) => {
316
+ const parsed = parseFloat(e.target.value);
317
+ if (!Number.isNaN(parsed) && parsed > 0) {
318
+ onPatchRow(row.id, { scale: parsed });
319
+ }
320
+ }}
321
+ onBlur={() => {
322
+ // Switch back to preset dropdown if the typed value matches a preset
323
+ if (scaleToPreset(row.scale) !== "custom") {
324
+ onPatchRow(row.id, { customScale: false });
325
+ }
326
+ }}
327
+ className="h-6 w-14 shrink-0 px-1.5 text-[11px] tabular-nums"
328
+ aria-label="Scale"
329
+ // eslint-disable-next-line jsx-a11y/no-autofocus
330
+ autoFocus
331
+ />
332
+ ) : (
333
+ <ScaleSelect
334
+ scale={row.scale}
335
+ customScale={false}
336
+ disabled={isDisabled}
337
+ onPresetChange={(preset, raw) => {
338
+ if (preset === "custom") {
339
+ onPatchRow(row.id, { customScale: true });
340
+ } else {
341
+ onPatchRow(row.id, { scale: raw, customScale: false });
342
+ }
343
+ }}
344
+ />
345
+ )}
346
+
347
+ {/* Format dropdown — PNG / JPG / SVG / PDF / WEBP */}
348
+ <Select
349
+ value={row.format}
350
+ disabled={isDisabled}
351
+ onValueChange={(format) =>
352
+ onPatchRow(row.id, { format: format as ExportFormat })
353
+ }
354
+ >
355
+ <SelectTrigger className="h-6 min-w-0 flex-1 px-1.5 text-[11px]">
356
+ <SelectValue />
357
+ </SelectTrigger>
358
+ <SelectContent>
359
+ <SelectGroup>
360
+ {formats.map((format) => (
361
+ <SelectItem
362
+ key={format}
363
+ value={format}
364
+ className="text-[11px] uppercase"
365
+ >
366
+ {format.toUpperCase()}
367
+ </SelectItem>
368
+ ))}
369
+ </SelectGroup>
370
+ </SelectContent>
371
+ </Select>
372
+
373
+ {/* Suffix input — e.g. "@2x", "-mobile" */}
374
+ <Input
375
+ value={row.suffix}
376
+ disabled={isDisabled}
377
+ onChange={(e) => onPatchRow(row.id, { suffix: e.target.value })}
378
+ placeholder={labels.suffix}
379
+ className="h-6 min-w-0 flex-1 px-1.5 text-[11px]"
380
+ aria-label={labels.suffix}
381
+ />
382
+
383
+ {/* Remove row button — matches Figma's × on each export entry */}
384
+ <button
385
+ type="button"
386
+ aria-label={labels.removeExport}
387
+ disabled={isDisabled || !canRemove}
388
+ className="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
389
+ onClick={() => onRemoveRow(row.id)}
390
+ >
391
+ <IconX className="size-3" />
392
+ </button>
393
+ </div>
394
+ );
395
+ }