@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,17 +1,14 @@
1
- import {
2
- IconArrowBackUp,
3
- IconArrowsDiagonal,
4
- IconLink,
5
- IconUnlink,
6
- } from "@tabler/icons-react";
7
- import type { ReactNode } from "react";
1
+ import { IconArrowBackUp } from "@tabler/icons-react";
2
+ import { type ReactNode, useState } from "react";
8
3
 
9
4
  import { Button } from "@/components/ui/button";
10
5
  import { Checkbox } from "@/components/ui/checkbox";
11
6
  import {
12
7
  DropdownMenu,
8
+ DropdownMenuCheckboxItem,
13
9
  DropdownMenuContent,
14
10
  DropdownMenuItem,
11
+ DropdownMenuSeparator,
15
12
  DropdownMenuTrigger,
16
13
  } from "@/components/ui/dropdown-menu";
17
14
  import {
@@ -22,20 +19,26 @@ import {
22
19
  } from "@/components/ui/tooltip";
23
20
  import { cn } from "@/lib/utils";
24
21
 
25
- import {
26
- AlignmentMatrix,
27
- type AlignmentMatrixValue,
28
- type DistributionAxis,
22
+ import type {
23
+ AlignmentHorizontal,
24
+ AlignmentMatrixValue,
25
+ AlignmentVertical,
26
+ DistributionAxis,
29
27
  } from "./AlignmentMatrix";
30
28
  import {
31
29
  IconFlowGrid,
32
30
  IconFlowHorizontal,
33
31
  IconFlowVertical,
34
- IconFlowWrap,
35
32
  IconGap,
36
- IconLayoutSettings,
37
33
  IconPaddingHorizontal,
38
34
  IconPaddingVertical,
35
+ IconSizingFill,
36
+ IconSizingFixed,
37
+ IconSizingHug,
38
+ IconSizingMax,
39
+ IconSizingMin,
40
+ IconSizingRemove,
41
+ IconSizingVariable,
39
42
  } from "./figma-icons";
40
43
  import { ScrubInput } from "./ScrubInput";
41
44
 
@@ -44,6 +47,12 @@ export type AutoLayoutWrap = "nowrap" | "wrap";
44
47
  export type AutoLayoutSizing = "hug" | "fill" | "fixed";
45
48
  export type AutoLayoutSizingAxis = "horizontal" | "vertical";
46
49
 
50
+ /**
51
+ * The active flow option. "normal" represents block / normal-flow layout for
52
+ * non-flex containers; the other four map to flex direction + wrap state.
53
+ */
54
+ export type AutoLayoutFlow = "normal" | "vertical" | "horizontal" | "grid";
55
+
47
56
  export interface AutoLayoutPadding {
48
57
  top: number;
49
58
  right: number;
@@ -67,6 +76,22 @@ export interface AutoLayoutMatrixValue {
67
76
  horizontal: AutoLayoutSizing;
68
77
  vertical: AutoLayoutSizing;
69
78
  };
79
+ /**
80
+ * Currently-set min/max constraints per axis, in px. `null` means the
81
+ * constraint is not set (Figma shows the "Add min/max…" menu item instead of
82
+ * a sub-row). Optional so existing callers are unaffected.
83
+ */
84
+ childMinMax?: {
85
+ horizontal?: { min?: number | null; max?: number | null };
86
+ vertical?: { min?: number | null; max?: number | null };
87
+ };
88
+ /**
89
+ * Optional layout-mode hint. When "block" the control renders the
90
+ * normal-flow (non-flex) state — the first flow icon is active and gap is
91
+ * treated as disabled. Defaults to flex when omitted so existing callers are
92
+ * unaffected.
93
+ */
94
+ display?: "flex" | "block";
70
95
  }
71
96
 
72
97
  export interface AutoLayoutMatrixLabels {
@@ -90,6 +115,20 @@ export interface AutoLayoutMatrixLabels {
90
115
  fill: string;
91
116
  fixed: string;
92
117
  clipContent: string;
118
+ fixedWidth: string;
119
+ fixedHeight: string;
120
+ hugContents: string;
121
+ fillContainer: string;
122
+ addMinWidth: string;
123
+ addMaxWidth: string;
124
+ addMinHeight: string;
125
+ addMaxHeight: string;
126
+ minWidth: string;
127
+ maxWidth: string;
128
+ minHeight: string;
129
+ maxHeight: string;
130
+ applyVariable: string;
131
+ removeConstraint: string;
93
132
  }
94
133
 
95
134
  export interface AutoLayoutMatrixProps {
@@ -106,6 +145,29 @@ export interface AutoLayoutMatrixProps {
106
145
  axis: AutoLayoutSizingAxis,
107
146
  sizing: AutoLayoutSizing,
108
147
  ) => void;
148
+ /**
149
+ * Set or clear a min/max constraint on an axis. `value === null` clears it.
150
+ * Optional — when omitted the "Add min/max…" rows and constraint sub-rows are
151
+ * hidden, so existing callers are unaffected.
152
+ */
153
+ onChildMinMaxChange?: (
154
+ axis: AutoLayoutSizingAxis,
155
+ kind: "min" | "max",
156
+ value: number | null,
157
+ ) => void;
158
+ /**
159
+ * Invoked when the user picks "Apply variable…". Optional — when omitted the
160
+ * variable row is still shown but disabled (placeholder), matching Figma when
161
+ * no variable collections exist.
162
+ */
163
+ onApplyVariable?: (axis: AutoLayoutSizingAxis) => void;
164
+ /**
165
+ * Emitted when the user picks a flow that changes the layout mode between
166
+ * normal-flow (block) and flex. Pass this so selecting the first ("normal")
167
+ * flow icon can turn auto layout off, and selecting a flex flow can turn it
168
+ * on. Optional — when omitted the control still works in pure-flex mode.
169
+ */
170
+ onDisplayChange?: (display: "flex" | "block") => void;
109
171
  availableChildSizing?: Partial<
110
172
  Record<AutoLayoutSizingAxis, AutoLayoutSizing[]>
111
173
  >;
@@ -114,7 +176,8 @@ export interface AutoLayoutMatrixProps {
114
176
  className?: string;
115
177
  }
116
178
 
117
- const DEFAULT_LABELS: AutoLayoutMatrixLabels = {
179
+ /** Default English labels for the auto-layout matrix and SizingField. */
180
+ export const DEFAULT_AUTO_LAYOUT_LABELS: AutoLayoutMatrixLabels = {
118
181
  title: "Auto layout", // i18n-ignore fallback component label
119
182
  alignment: "Alignment", // i18n-ignore fallback component label
120
183
  direction: "Direction", // i18n-ignore fallback component label
@@ -135,17 +198,29 @@ const DEFAULT_LABELS: AutoLayoutMatrixLabels = {
135
198
  fill: "Fill", // i18n-ignore fallback component label
136
199
  fixed: "Fixed", // i18n-ignore fallback component label
137
200
  clipContent: "Clip content", // i18n-ignore fallback component label
201
+ fixedWidth: "Fixed width", // i18n-ignore fallback component label
202
+ fixedHeight: "Fixed height", // i18n-ignore fallback component label
203
+ hugContents: "Hug contents", // i18n-ignore fallback component label
204
+ fillContainer: "Fill container", // i18n-ignore fallback component label
205
+ addMinWidth: "Add min width…", // i18n-ignore fallback component label
206
+ addMaxWidth: "Add max width…", // i18n-ignore fallback component label
207
+ addMinHeight: "Add min height…", // i18n-ignore fallback component label
208
+ addMaxHeight: "Add max height…", // i18n-ignore fallback component label
209
+ minWidth: "Min width", // i18n-ignore fallback component label
210
+ maxWidth: "Max width", // i18n-ignore fallback component label
211
+ minHeight: "Min height", // i18n-ignore fallback component label
212
+ maxHeight: "Max height", // i18n-ignore fallback component label
213
+ applyVariable: "Apply variable…", // i18n-ignore fallback component label
214
+ removeConstraint: "Remove", // i18n-ignore fallback component label
138
215
  };
139
216
 
140
217
  const SIZING_OPTIONS: AutoLayoutSizing[] = ["hug", "fill", "fixed"];
141
218
 
142
- /** Derive the "active flow" option from direction + wrap state. */
143
- function getFlowOption(
144
- direction: AutoLayoutDirection,
145
- wrap: AutoLayoutWrap,
146
- ): "horizontal" | "vertical" | "wrap" | "grid" {
147
- if (wrap === "wrap") return "wrap";
148
- if (direction === "vertical") return "vertical";
219
+ /** Derive the active flow option from display + direction + wrap state. */
220
+ function getFlowOption(value: AutoLayoutMatrixValue): AutoLayoutFlow {
221
+ if (value.display === "block") return "normal";
222
+ if (value.wrap === "wrap") return "grid";
223
+ if (value.direction === "vertical") return "vertical";
149
224
  return "horizontal";
150
225
  }
151
226
 
@@ -160,12 +235,15 @@ export function AutoLayoutMatrix({
160
235
  onClipContentChange,
161
236
  onDistribute,
162
237
  onChildSizingChange,
238
+ onChildMinMaxChange,
239
+ onApplyVariable,
240
+ onDisplayChange,
163
241
  availableChildSizing,
164
242
  labels,
165
243
  disabled = false,
166
244
  className,
167
245
  }: AutoLayoutMatrixProps) {
168
- const copy = { ...DEFAULT_LABELS, ...labels };
246
+ const copy = { ...DEFAULT_AUTO_LAYOUT_LABELS, ...labels };
169
247
 
170
248
  const horizontalPaddingValue = Math.round(
171
249
  (value.padding.left + value.padding.right) / 2,
@@ -174,67 +252,75 @@ export function AutoLayoutMatrix({
174
252
  (value.padding.top + value.padding.bottom) / 2,
175
253
  );
176
254
 
177
- const activeFlow = getFlowOption(value.direction, value.wrap);
255
+ const activeFlow = getFlowOption(value);
256
+ const isBlock = activeFlow === "normal";
257
+
258
+ /** Apply a flow choice, coordinating display + direction + wrap. */
259
+ const selectFlow = (flow: AutoLayoutFlow) => {
260
+ if (flow === "normal") {
261
+ onDisplayChange?.("block");
262
+ return;
263
+ }
264
+ // Any flex flow turns auto layout on.
265
+ onDisplayChange?.("flex");
266
+ if (flow === "vertical") {
267
+ onDirectionChange("vertical");
268
+ onWrapChange("nowrap");
269
+ } else if (flow === "horizontal") {
270
+ onDirectionChange("horizontal");
271
+ onWrapChange("nowrap");
272
+ } else {
273
+ // grid → horizontal direction with wrap
274
+ onDirectionChange("horizontal");
275
+ onWrapChange("wrap");
276
+ }
277
+ };
178
278
 
179
279
  return (
180
280
  <TooltipProvider delayDuration={250}>
181
- <div className={cn("space-y-2", className)}>
281
+ <div className={cn("space-y-3", className)}>
182
282
  {/* ── Flow ── */}
183
283
  <div className="space-y-1.5">
184
- <span className="text-[11px] font-medium text-muted-foreground">
284
+ <ControlLabel>
185
285
  {"Flow" /* i18n-ignore Figma inspector label */}
186
- </span>
286
+ </ControlLabel>
187
287
  <div className="flex items-center gap-1.5">
188
- {/* 4-segment flow bar: horizontal / vertical / wrap / grid */}
189
- <div className="flex h-6 flex-1 overflow-hidden rounded-md bg-[var(--design-editor-control-bg)]">
288
+ {/* 4-segment flow bar: normal / vertical / horizontal / grid */}
289
+ <div className="flex h-7 flex-1 items-center gap-0.5 rounded-md bg-[var(--design-editor-control-bg)] p-0.5">
190
290
  <FlowButton
191
- label={copy.horizontal}
192
- active={activeFlow === "horizontal"}
291
+ label={"Normal flow" /* i18n-ignore Figma inspector label */}
292
+ active={activeFlow === "normal"}
193
293
  disabled={disabled}
194
- onClick={() => {
195
- onDirectionChange("horizontal");
196
- onWrapChange("nowrap");
197
- }}
198
- position="first"
294
+ onClick={() => selectFlow("normal")}
199
295
  >
200
- <IconFlowHorizontal className="size-3.5" />
296
+ <IconFlowNormal />
201
297
  </FlowButton>
202
298
  <FlowButton
203
299
  label={copy.vertical}
204
300
  active={activeFlow === "vertical"}
205
301
  disabled={disabled}
206
- onClick={() => {
207
- onDirectionChange("vertical");
208
- onWrapChange("nowrap");
209
- }}
302
+ onClick={() => selectFlow("vertical")}
210
303
  >
211
304
  <IconFlowVertical className="size-3.5" />
212
305
  </FlowButton>
213
306
  <FlowButton
214
- label={copy.wrap}
215
- active={activeFlow === "wrap"}
307
+ label={copy.horizontal}
308
+ active={activeFlow === "horizontal"}
216
309
  disabled={disabled}
217
- onClick={() => {
218
- onDirectionChange("horizontal");
219
- onWrapChange("wrap");
220
- }}
310
+ onClick={() => selectFlow("horizontal")}
221
311
  >
222
- <IconFlowWrap className="size-3.5" />
312
+ <IconFlowHorizontal className="size-3.5" />
223
313
  </FlowButton>
224
314
  <FlowButton
225
315
  label={"Grid" /* i18n-ignore Figma inspector label */}
226
316
  active={activeFlow === "grid"}
227
317
  disabled={disabled}
228
- onClick={() => {
229
- onDirectionChange("horizontal");
230
- onWrapChange("wrap");
231
- }}
232
- position="last"
318
+ onClick={() => selectFlow("grid")}
233
319
  >
234
320
  <IconFlowGrid className="size-3.5" />
235
321
  </FlowButton>
236
322
  </div>
237
- {/* Reset button same slot as before */}
323
+ {/* Reset / reverse-flow button */}
238
324
  <Tooltip>
239
325
  <TooltipTrigger asChild>
240
326
  <Button
@@ -245,13 +331,10 @@ export function AutoLayoutMatrix({
245
331
  aria-label={
246
332
  "Reset auto layout flow" /* i18n-ignore inspector tooltip */
247
333
  }
248
- onClick={() => {
249
- onDirectionChange("horizontal");
250
- onWrapChange("nowrap");
251
- }}
252
- className="size-6 shrink-0 rounded-md text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground"
334
+ onClick={() => selectFlow("horizontal")}
335
+ className="size-7 shrink-0 rounded-md text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground"
253
336
  >
254
- <IconArrowBackUp className="size-3.5" />
337
+ <IconArrowBackUp className="size-4" />
255
338
  </Button>
256
339
  </TooltipTrigger>
257
340
  <TooltipContent>
@@ -263,27 +346,41 @@ export function AutoLayoutMatrix({
263
346
 
264
347
  {/* ── Resizing ── */}
265
348
  <div className="space-y-1.5">
266
- <span className="text-[11px] font-medium text-muted-foreground">
349
+ <ControlLabel>
267
350
  {"Resizing" /* i18n-ignore Figma inspector label */}
268
- </span>
269
- <div className="grid grid-cols-[1fr_1fr_1.5rem] items-center gap-1">
351
+ </ControlLabel>
352
+ <div className="grid grid-cols-[1fr_1fr_auto] items-start gap-1.5">
270
353
  <SizingField
271
354
  axis="W"
355
+ sizingAxis="horizontal"
272
356
  value={value.childSizing.horizontal}
273
357
  resolvedSize={value.resolvedSize?.horizontal}
274
- options={availableChildSizing?.horizontal ?? SIZING_OPTIONS}
358
+ minMax={value.childMinMax?.horizontal}
359
+ options={resolveSizingOptions(
360
+ availableChildSizing?.horizontal,
361
+ value.childSizing.horizontal,
362
+ )}
275
363
  labels={copy}
276
364
  disabled={disabled}
277
365
  onChange={(next) => onChildSizingChange("horizontal", next)}
366
+ onMinMaxChange={onChildMinMaxChange}
367
+ onApplyVariable={onApplyVariable}
278
368
  />
279
369
  <SizingField
280
370
  axis="H"
371
+ sizingAxis="vertical"
281
372
  value={value.childSizing.vertical}
282
373
  resolvedSize={value.resolvedSize?.vertical}
283
- options={availableChildSizing?.vertical ?? SIZING_OPTIONS}
374
+ minMax={value.childMinMax?.vertical}
375
+ options={resolveSizingOptions(
376
+ availableChildSizing?.vertical,
377
+ value.childSizing.vertical,
378
+ )}
284
379
  labels={copy}
285
380
  disabled={disabled}
286
381
  onChange={(next) => onChildSizingChange("vertical", next)}
382
+ onMinMaxChange={onChildMinMaxChange}
383
+ onApplyVariable={onApplyVariable}
287
384
  />
288
385
  {/* Resize-to-fit icon button */}
289
386
  <Tooltip>
@@ -300,9 +397,9 @@ export function AutoLayoutMatrix({
300
397
  onChildSizingChange("horizontal", "hug");
301
398
  onChildSizingChange("vertical", "hug");
302
399
  }}
303
- className="size-6 rounded-md text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground"
400
+ className="size-7 rounded-md text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground"
304
401
  >
305
- <IconArrowsDiagonal className="size-3.5" />
402
+ <IconResizeToFitMini />
306
403
  </Button>
307
404
  </TooltipTrigger>
308
405
  <TooltipContent>
@@ -312,223 +409,124 @@ export function AutoLayoutMatrix({
312
409
  </div>
313
410
  </div>
314
411
 
315
- {/* ── Alignment + Gap (two-column row) ── */}
316
- <div className="grid grid-cols-[84px_1fr] items-start gap-2">
317
- {/* Left: Alignment label + 3×3 matrix */}
412
+ {/* ── Alignment + Gap (single label row) ── */}
413
+ <div className="grid grid-cols-[78px_1fr] items-start gap-3">
414
+ {/* Left: Alignment label + compact 3×3 matrix (no border box) */}
318
415
  <div className="space-y-1.5">
319
- <span className="text-[11px] font-medium text-muted-foreground">
416
+ <ControlLabel>
320
417
  {"Alignment" /* i18n-ignore Figma inspector label */}
321
- </span>
322
- <AlignmentMatrix
418
+ </ControlLabel>
419
+ <CompactAlignmentMatrix
323
420
  value={value.alignment}
324
421
  onChange={onAlignmentChange}
422
+ direction={value.direction}
423
+ disabled={disabled || isBlock}
325
424
  onDistribute={onDistribute}
326
- disabled={disabled}
327
425
  />
328
426
  </div>
329
427
 
330
- {/* Right: Gap label + input */}
428
+ {/* Right: Gap label + [icon] value [▾] + sliders */}
331
429
  <div className="space-y-1.5">
332
- <span className="text-[11px] font-medium text-muted-foreground">
333
- {copy.gap}
334
- </span>
335
- <div className="flex items-center gap-1">
336
- {/* Gap scrub input with ]·[ icon */}
337
- <div className="min-w-0 flex-1">
338
- <ScrubInput
339
- label={copy.gap}
340
- ariaLabel={copy.gap}
341
- value={value.gap}
342
- onChange={(next) => onGapChange(next)}
343
- icon={IconGap}
344
- unit="px"
345
- min={0}
346
- step={1}
347
- precision={1}
348
- disabled={disabled}
349
- labelClassName="w-5 shrink-0 [&>span]:hidden"
350
- inputClassName="h-6 text-[11px]"
351
- />
352
- </div>
353
- {/* Gap options / layout settings button */}
354
- <Tooltip>
355
- <TooltipTrigger asChild>
356
- <Button
357
- type="button"
358
- variant="ghost"
359
- size="icon"
360
- disabled={disabled}
361
- aria-label={
362
- "Layout settings" /* i18n-ignore inspector tooltip */
363
- }
364
- className="size-6 shrink-0 rounded-md text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground"
365
- >
366
- <IconLayoutSettings className="size-3.5" />
367
- </Button>
368
- </TooltipTrigger>
369
- <TooltipContent>
370
- {"Layout settings" /* i18n-ignore inspector tooltip */}
371
- </TooltipContent>
372
- </Tooltip>
373
- </div>
430
+ <ControlLabel>{copy.gap}</ControlLabel>
431
+ <GapField
432
+ value={value.gap}
433
+ onGapChange={onGapChange}
434
+ onDistribute={onDistribute}
435
+ label={copy.gap}
436
+ disabled={disabled || isBlock}
437
+ />
374
438
  </div>
375
439
  </div>
376
440
 
377
441
  {/* ── Padding ── */}
378
442
  <div className="space-y-1.5">
379
- <span className="text-[11px] font-medium text-muted-foreground">
380
- {copy.padding}
381
- </span>
382
- <div className="grid grid-cols-[1fr_1fr_1.5rem] items-center gap-1">
383
- {/* Horizontal padding (left + right) */}
384
- <ScrubInput
385
- label={copy.padding}
386
- ariaLabel={copy.paddingLeft + " / " + copy.paddingRight}
387
- value={
388
- value.paddingLinked ? value.padding.top : horizontalPaddingValue
389
- }
390
- onChange={(next) => {
391
- if (value.paddingLinked) {
443
+ <ControlLabel>{copy.padding}</ControlLabel>
444
+ {value.paddingLinked ? (
445
+ /* Default linked state: 2 compact fields + link toggle */
446
+ <div className="grid grid-cols-[1fr_1fr_auto] items-center gap-1.5">
447
+ <PaddingField
448
+ icon={IconPaddingHorizontal}
449
+ ariaLabel={copy.paddingLeft + " / " + copy.paddingRight}
450
+ value={horizontalPaddingValue}
451
+ onChange={(next) =>
392
452
  onPaddingChange({
393
- top: next,
394
- right: next,
395
- bottom: next,
453
+ top: value.padding.top,
454
+ bottom: value.padding.bottom,
396
455
  left: next,
397
- });
398
- return;
399
- }
400
- onPaddingChange({
401
- ...value.padding,
402
- left: next,
403
- right: next,
404
- });
405
- }}
406
- icon={IconPaddingHorizontal}
407
- unit="px"
408
- min={0}
409
- step={1}
410
- precision={1}
411
- disabled={disabled}
412
- labelClassName="w-5 shrink-0 [&>span]:hidden"
413
- inputClassName="h-6 text-[11px]"
414
- />
415
- {/* Vertical padding (top + bottom) */}
416
- <ScrubInput
417
- label={copy.padding}
418
- ariaLabel={copy.paddingTop + " / " + copy.paddingBottom}
419
- value={
420
- value.paddingLinked ? value.padding.top : verticalPaddingValue
421
- }
422
- onChange={(next) => {
423
- if (value.paddingLinked) {
424
- onPaddingChange({
425
- top: next,
426
456
  right: next,
427
- bottom: next,
428
- left: next,
429
- });
430
- return;
431
- }
432
- onPaddingChange({
433
- ...value.padding,
434
- top: next,
435
- bottom: next,
436
- });
437
- }}
438
- icon={IconPaddingVertical}
439
- unit="px"
440
- min={0}
441
- step={1}
442
- precision={1}
443
- disabled={disabled}
444
- labelClassName="w-5 shrink-0 [&>span]:hidden"
445
- inputClassName="h-6 text-[11px]"
446
- />
447
- {/* Link/unlink independent padding */}
448
- <Tooltip>
449
- <TooltipTrigger asChild>
450
- <Button
451
- type="button"
452
- variant="ghost"
453
- size="icon"
454
- disabled={disabled}
455
- aria-label={
456
- value.paddingLinked ? copy.unlinkPadding : copy.linkPadding
457
- }
458
- onClick={() => onPaddingLinkedChange(!value.paddingLinked)}
459
- className="size-6 rounded-md text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground"
460
- >
461
- {value.paddingLinked ? (
462
- <IconLink className="size-3.5" />
463
- ) : (
464
- <IconUnlink className="size-3.5" />
465
- )}
466
- </Button>
467
- </TooltipTrigger>
468
- <TooltipContent>
469
- {value.paddingLinked ? copy.unlinkPadding : copy.linkPadding}
470
- </TooltipContent>
471
- </Tooltip>
472
- </div>
473
-
474
- {/* Expanded 4-field padding when unlinked */}
475
- {!value.paddingLinked && (
476
- <div className="mt-1 grid grid-cols-2 gap-1.5">
477
- <ScrubInput
478
- label={copy.paddingTop}
479
- value={value.padding.top}
480
- onChange={(next) =>
481
- onPaddingChange({ ...value.padding, top: next })
457
+ })
482
458
  }
483
- unit="px"
484
- min={0}
485
- step={1}
486
- precision={1}
487
459
  disabled={disabled}
488
- labelClassName="w-8"
489
- inputClassName="h-6 text-[11px]"
490
460
  />
491
- <ScrubInput
492
- label={copy.paddingRight}
493
- value={value.padding.right}
461
+ <PaddingField
462
+ icon={IconPaddingVertical}
463
+ ariaLabel={copy.paddingTop + " / " + copy.paddingBottom}
464
+ value={verticalPaddingValue}
494
465
  onChange={(next) =>
495
- onPaddingChange({ ...value.padding, right: next })
466
+ onPaddingChange({
467
+ top: next,
468
+ bottom: next,
469
+ left: value.padding.left,
470
+ right: value.padding.right,
471
+ })
496
472
  }
497
- unit="px"
498
- min={0}
499
- step={1}
500
- precision={1}
501
473
  disabled={disabled}
502
- labelClassName="w-8"
503
- inputClassName="h-6 text-[11px]"
504
474
  />
505
- <ScrubInput
506
- label={copy.paddingBottom}
507
- value={value.padding.bottom}
508
- onChange={(next) =>
509
- onPaddingChange({ ...value.padding, bottom: next })
510
- }
511
- unit="px"
512
- min={0}
513
- step={1}
514
- precision={1}
475
+ <PaddingLinkButton
476
+ linked
515
477
  disabled={disabled}
516
- labelClassName="w-8"
517
- inputClassName="h-6 text-[11px]"
478
+ linkLabel={copy.linkPadding}
479
+ unlinkLabel={copy.unlinkPadding}
480
+ onToggle={() => onPaddingLinkedChange(false)}
518
481
  />
519
- <ScrubInput
520
- label={copy.paddingLeft}
521
- value={value.padding.left}
522
- onChange={(next) =>
523
- onPaddingChange({ ...value.padding, left: next })
524
- }
525
- unit="px"
526
- min={0}
527
- step={1}
528
- precision={1}
482
+ </div>
483
+ ) : (
484
+ /* Unlinked state: expand to 4 separate T / R / B / L fields */
485
+ <div className="grid grid-cols-[1fr_1fr_auto] items-center gap-1.5">
486
+ <div className="col-span-2 grid grid-cols-2 gap-1.5">
487
+ <PaddingField
488
+ icon={IconPaddingTopMini}
489
+ ariaLabel={copy.paddingTop}
490
+ value={value.padding.top}
491
+ onChange={(next) =>
492
+ onPaddingChange({ ...value.padding, top: next })
493
+ }
494
+ disabled={disabled}
495
+ />
496
+ <PaddingField
497
+ icon={IconPaddingRightMini}
498
+ ariaLabel={copy.paddingRight}
499
+ value={value.padding.right}
500
+ onChange={(next) =>
501
+ onPaddingChange({ ...value.padding, right: next })
502
+ }
503
+ disabled={disabled}
504
+ />
505
+ <PaddingField
506
+ icon={IconPaddingBottomMini}
507
+ ariaLabel={copy.paddingBottom}
508
+ value={value.padding.bottom}
509
+ onChange={(next) =>
510
+ onPaddingChange({ ...value.padding, bottom: next })
511
+ }
512
+ disabled={disabled}
513
+ />
514
+ <PaddingField
515
+ icon={IconPaddingLeftMini}
516
+ ariaLabel={copy.paddingLeft}
517
+ value={value.padding.left}
518
+ onChange={(next) =>
519
+ onPaddingChange({ ...value.padding, left: next })
520
+ }
521
+ disabled={disabled}
522
+ />
523
+ </div>
524
+ <PaddingLinkButton
525
+ linked={false}
529
526
  disabled={disabled}
530
- labelClassName="w-8"
531
- inputClassName="h-6 text-[11px]"
527
+ linkLabel={copy.linkPadding}
528
+ unlinkLabel={copy.unlinkPadding}
529
+ onToggle={() => onPaddingLinkedChange(true)}
532
530
  />
533
531
  </div>
534
532
  )}
@@ -542,7 +540,7 @@ export function AutoLayoutMatrix({
542
540
  onCheckedChange={(checked) =>
543
541
  onClipContentChange?.(checked === true)
544
542
  }
545
- className="size-4 rounded-sm"
543
+ className="size-3.5 rounded-[3px]"
546
544
  />
547
545
  <span>{copy.clipContent}</span>
548
546
  </label>
@@ -551,10 +549,238 @@ export function AutoLayoutMatrix({
551
549
  );
552
550
  }
553
551
 
552
+ // ─────────────────────────────────────────────────
553
+ // Helpers
554
+ // ─────────────────────────────────────────────────
555
+
556
+ /** Keep the current value selectable even if it's not in the available list. */
557
+ function resolveSizingOptions(
558
+ available: AutoLayoutSizing[] | undefined,
559
+ current: AutoLayoutSizing,
560
+ ): AutoLayoutSizing[] {
561
+ if (!available) return SIZING_OPTIONS;
562
+ return available.includes(current) ? available : [...available, current];
563
+ }
564
+
565
+ const ALIGNMENT_CELLS: Array<{
566
+ horizontal: AlignmentHorizontal;
567
+ vertical: AlignmentVertical;
568
+ }> = [
569
+ { horizontal: "left", vertical: "top" },
570
+ { horizontal: "center", vertical: "top" },
571
+ { horizontal: "right", vertical: "top" },
572
+ { horizontal: "left", vertical: "middle" },
573
+ { horizontal: "center", vertical: "middle" },
574
+ { horizontal: "right", vertical: "middle" },
575
+ { horizontal: "left", vertical: "bottom" },
576
+ { horizontal: "center", vertical: "bottom" },
577
+ { horizontal: "right", vertical: "bottom" },
578
+ ];
579
+
554
580
  // ─────────────────────────────────────────────────
555
581
  // Sub-components
556
582
  // ─────────────────────────────────────────────────
557
583
 
584
+ /**
585
+ * Compact 3×3 alignment grid (no border box). Inactive cells show a faint dot;
586
+ * the active cell shows accent bars oriented by flow — horizontal bars for a
587
+ * vertical flow, vertical bars for a horizontal flow (Figma convention).
588
+ * When `onDistribute` is provided, two distribute buttons (H + V) are rendered
589
+ * below the grid, matching Figma's inspector layout.
590
+ */
591
+ function CompactAlignmentMatrix({
592
+ value,
593
+ onChange,
594
+ direction,
595
+ disabled,
596
+ onDistribute,
597
+ }: {
598
+ value: AlignmentMatrixValue;
599
+ onChange: (value: AlignmentMatrixValue) => void;
600
+ direction: AutoLayoutDirection;
601
+ disabled: boolean;
602
+ onDistribute?: (axis: DistributionAxis) => void;
603
+ }) {
604
+ return (
605
+ <div
606
+ className={cn("space-y-1", disabled && "pointer-events-none opacity-40")}
607
+ >
608
+ <div className={cn("grid w-fit grid-cols-3 rounded-md")}>
609
+ {ALIGNMENT_CELLS.map((cell) => {
610
+ const active =
611
+ cell.horizontal === value.horizontal &&
612
+ cell.vertical === value.vertical;
613
+ return (
614
+ <button
615
+ key={`${cell.horizontal}-${cell.vertical}`}
616
+ type="button"
617
+ aria-label={`${cell.vertical} ${cell.horizontal}`}
618
+ aria-pressed={active}
619
+ disabled={disabled}
620
+ onClick={() =>
621
+ onChange({
622
+ horizontal: cell.horizontal,
623
+ vertical: cell.vertical,
624
+ })
625
+ }
626
+ className={cn(
627
+ "flex size-[22px] items-center justify-center rounded-[3px] transition-colors",
628
+ "hover:bg-[var(--design-editor-control-bg)]",
629
+ )}
630
+ >
631
+ {active ? (
632
+ <AlignmentBars
633
+ horizontal={cell.horizontal}
634
+ vertical={cell.vertical}
635
+ direction={direction}
636
+ />
637
+ ) : (
638
+ <span
639
+ className="block size-[3px] rounded-full bg-current opacity-25"
640
+ aria-hidden="true"
641
+ />
642
+ )}
643
+ </button>
644
+ );
645
+ })}
646
+ </div>
647
+ {onDistribute != null ? (
648
+ <div className="flex gap-0.5">
649
+ <Tooltip>
650
+ <TooltipTrigger asChild>
651
+ <button
652
+ type="button"
653
+ aria-label={
654
+ "Distribute horizontal spacing" /* i18n-ignore Figma inspector tooltip */
655
+ }
656
+ disabled={disabled}
657
+ onClick={() => onDistribute("horizontal")}
658
+ className={cn(
659
+ "flex size-[22px] items-center justify-center rounded-[3px] transition-colors",
660
+ "text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground",
661
+ "disabled:pointer-events-none disabled:opacity-40",
662
+ )}
663
+ >
664
+ <IconDistributeH />
665
+ </button>
666
+ </TooltipTrigger>
667
+ <TooltipContent>
668
+ {
669
+ "Distribute horizontal spacing" /* i18n-ignore Figma inspector tooltip */
670
+ }
671
+ </TooltipContent>
672
+ </Tooltip>
673
+ <Tooltip>
674
+ <TooltipTrigger asChild>
675
+ <button
676
+ type="button"
677
+ aria-label={
678
+ "Distribute vertical spacing" /* i18n-ignore Figma inspector tooltip */
679
+ }
680
+ disabled={disabled}
681
+ onClick={() => onDistribute("vertical")}
682
+ className={cn(
683
+ "flex size-[22px] items-center justify-center rounded-[3px] transition-colors",
684
+ "text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground",
685
+ "disabled:pointer-events-none disabled:opacity-40",
686
+ )}
687
+ >
688
+ <IconDistributeV />
689
+ </button>
690
+ </TooltipTrigger>
691
+ <TooltipContent>
692
+ {
693
+ "Distribute vertical spacing" /* i18n-ignore Figma inspector tooltip */
694
+ }
695
+ </TooltipContent>
696
+ </Tooltip>
697
+ </div>
698
+ ) : null}
699
+ </div>
700
+ );
701
+ }
702
+
703
+ /** Accent bars showing items packed toward the active edge, oriented by flow. */
704
+ function AlignmentBars({
705
+ horizontal,
706
+ vertical,
707
+ direction,
708
+ }: {
709
+ horizontal: AlignmentHorizontal;
710
+ vertical: AlignmentVertical;
711
+ direction: AutoLayoutDirection;
712
+ }) {
713
+ const accent = "var(--design-editor-accent-color, #18a0fb)";
714
+ // Vertical flow → children stack vertically → render horizontal bars.
715
+ // Horizontal flow → children sit in a row → render vertical bars.
716
+ const stack = direction === "vertical";
717
+ const box = 14;
718
+ const pad = 2.5;
719
+ const thickness = 2;
720
+ const length = 5;
721
+ const shortLength = 3.5;
722
+ const gap = 1.5;
723
+
724
+ // Position the group of two bars toward the active edge.
725
+ const total = stack
726
+ ? thickness * 2 + gap // height when bars are horizontal
727
+ : thickness * 2 + gap; // width when bars are vertical
728
+
729
+ const along = (h: AlignmentHorizontal | AlignmentVertical, size: number) => {
730
+ if (h === "left" || h === "top") return pad;
731
+ if (h === "right" || h === "bottom") return box - pad - size;
732
+ return (box - size) / 2;
733
+ };
734
+
735
+ const bars = [length, shortLength];
736
+
737
+ return (
738
+ <svg viewBox="0 0 14 14" width={14} height={14} aria-hidden="true">
739
+ {bars.map((len, i) => {
740
+ if (stack) {
741
+ // Horizontal bars: vary X by horizontal align, stack along Y.
742
+ const y = along(vertical, total) + i * (thickness + gap);
743
+ const x = along(horizontal, len);
744
+ return (
745
+ <rect
746
+ key={i}
747
+ x={x}
748
+ y={y}
749
+ width={len}
750
+ height={thickness}
751
+ rx={1}
752
+ fill={accent}
753
+ />
754
+ );
755
+ }
756
+ // Vertical bars: vary Y by vertical align, distribute along X.
757
+ const x = along(horizontal, total) + i * (thickness + gap);
758
+ const y = along(vertical, len);
759
+ return (
760
+ <rect
761
+ key={i}
762
+ x={x}
763
+ y={y}
764
+ width={thickness}
765
+ height={len}
766
+ rx={1}
767
+ fill={accent}
768
+ />
769
+ );
770
+ })}
771
+ </svg>
772
+ );
773
+ }
774
+
775
+ /** Small uppercase muted section label, 11px. */
776
+ function ControlLabel({ children }: { children: ReactNode }) {
777
+ return (
778
+ <span className="block text-[11px] font-medium text-muted-foreground">
779
+ {children}
780
+ </span>
781
+ );
782
+ }
783
+
558
784
  /** A single segment in the flow segmented control. */
559
785
  function FlowButton({
560
786
  label,
@@ -562,14 +788,12 @@ function FlowButton({
562
788
  disabled,
563
789
  onClick,
564
790
  children,
565
- position,
566
791
  }: {
567
792
  label: string;
568
793
  active: boolean;
569
794
  disabled: boolean;
570
795
  onClick: () => void;
571
796
  children: ReactNode;
572
- position?: "first" | "last";
573
797
  }) {
574
798
  return (
575
799
  <Tooltip>
@@ -581,16 +805,15 @@ function FlowButton({
581
805
  disabled={disabled}
582
806
  onClick={onClick}
583
807
  className={cn(
584
- "flex flex-1 items-center justify-center transition-colors",
585
- "border-r border-[var(--design-editor-control-border,hsl(var(--border)/0.5))]",
586
- "text-muted-foreground hover:bg-[var(--design-editor-panel-raised-bg)] hover:text-foreground",
808
+ "flex h-6 flex-1 items-center justify-center rounded-[5px] transition-colors",
809
+ "text-muted-foreground hover:text-foreground",
587
810
  "disabled:pointer-events-none disabled:opacity-40",
588
- position === "last" && "border-r-0",
589
- active && [
590
- "bg-[var(--design-editor-panel-bg)]",
591
- "text-foreground",
592
- "shadow-[inset_0_0_0_1px_var(--design-editor-control-border,hsl(var(--border)))]",
593
- ],
811
+ active
812
+ ? [
813
+ "bg-[var(--design-editor-panel-bg)] text-foreground",
814
+ "shadow-[0_0_0_1px_var(--design-editor-control-border,hsl(var(--border))),0_1px_2px_rgba(0,0,0,0.25)]",
815
+ ]
816
+ : "hover:bg-[var(--design-editor-panel-raised-bg)]",
594
817
  )}
595
818
  >
596
819
  {children}
@@ -601,99 +824,766 @@ function FlowButton({
601
824
  );
602
825
  }
603
826
 
604
- /** Figma-style sizing field: [axis | resolved-size | mode ▾] as a dropdown trigger. */
605
- function SizingField({
606
- axis,
827
+ /**
828
+ * Gap field: [icon] value [▾] with a sliders icon to the right.
829
+ * The chevron dropdown offers Fixed (numeric gap) vs Auto (space-between).
830
+ */
831
+ function GapField({
607
832
  value,
608
- resolvedSize,
609
- options = SIZING_OPTIONS,
610
- labels,
833
+ onGapChange,
834
+ onDistribute,
835
+ label,
611
836
  disabled,
837
+ }: {
838
+ value: number;
839
+ onGapChange: (gap: number) => void;
840
+ onDistribute?: (axis: DistributionAxis) => void;
841
+ label: string;
842
+ disabled: boolean;
843
+ }) {
844
+ return (
845
+ <div className="flex items-center gap-1.5">
846
+ {/* [gap-icon] value [▾] in one control surface */}
847
+ <div
848
+ className={cn(
849
+ "flex h-7 min-w-0 flex-1 items-center rounded-md bg-[var(--design-editor-control-bg)] pl-1.5",
850
+ disabled && "opacity-40",
851
+ )}
852
+ >
853
+ <span className="flex shrink-0 items-center text-muted-foreground">
854
+ <IconGap className="size-3.5" />
855
+ </span>
856
+ <ScrubInput
857
+ label={label}
858
+ ariaLabel={label}
859
+ value={value}
860
+ onChange={(next) => onGapChange(next)}
861
+ unit="px"
862
+ min={0}
863
+ step={1}
864
+ precision={1}
865
+ disabled={disabled}
866
+ className="min-w-0 flex-1 gap-0"
867
+ labelClassName="hidden"
868
+ inputClassName="h-6 border-0 bg-transparent px-1 text-[11px] shadow-none focus-visible:ring-0"
869
+ />
870
+ {onDistribute != null ? (
871
+ <DropdownMenu>
872
+ <Tooltip>
873
+ <TooltipTrigger asChild>
874
+ <DropdownMenuTrigger asChild>
875
+ <button
876
+ type="button"
877
+ aria-label={"Gap mode" /* i18n-ignore inspector tooltip */}
878
+ disabled={disabled}
879
+ className={cn(
880
+ "flex h-7 w-6 shrink-0 items-center justify-center rounded-r-md",
881
+ "text-muted-foreground hover:text-foreground",
882
+ "disabled:pointer-events-none disabled:opacity-40",
883
+ "focus:outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color,hsl(var(--primary)))]",
884
+ )}
885
+ >
886
+ <ChevronDownMini />
887
+ </button>
888
+ </DropdownMenuTrigger>
889
+ </TooltipTrigger>
890
+ <TooltipContent>
891
+ {"Gap mode" /* i18n-ignore inspector tooltip */}
892
+ </TooltipContent>
893
+ </Tooltip>
894
+ <DropdownMenuContent
895
+ align="end"
896
+ className="min-w-[110px] text-[12px]"
897
+ sideOffset={4}
898
+ >
899
+ <DropdownMenuCheckboxItem
900
+ checked
901
+ className="text-[12px]"
902
+ onSelect={() => {
903
+ /* Fixed gap: keep current numeric value (already numeric). */
904
+ }}
905
+ >
906
+ {"Fixed" /* i18n-ignore Figma gap mode label */}
907
+ </DropdownMenuCheckboxItem>
908
+ <DropdownMenuCheckboxItem
909
+ checked={false}
910
+ className="text-[12px]"
911
+ onSelect={() => onDistribute("horizontal")}
912
+ >
913
+ {"Auto" /* i18n-ignore Figma gap mode label */}
914
+ </DropdownMenuCheckboxItem>
915
+ </DropdownMenuContent>
916
+ </DropdownMenu>
917
+ ) : null}
918
+ </div>
919
+ {/* Sliders / advanced spacing icon (Figma's tune control) */}
920
+ <Tooltip>
921
+ <TooltipTrigger asChild>
922
+ <Button
923
+ type="button"
924
+ variant="ghost"
925
+ size="icon"
926
+ disabled={disabled}
927
+ aria-label={
928
+ "Advanced gap settings" /* i18n-ignore inspector tooltip */
929
+ }
930
+ onClick={() => onDistribute?.("horizontal")}
931
+ className="size-7 shrink-0 rounded-md text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground"
932
+ >
933
+ <IconSlidersMini />
934
+ </Button>
935
+ </TooltipTrigger>
936
+ <TooltipContent>
937
+ {"Advanced gap settings" /* i18n-ignore inspector tooltip */}
938
+ </TooltipContent>
939
+ </Tooltip>
940
+ </div>
941
+ );
942
+ }
943
+
944
+ /** A compact padding field: [icon] value. */
945
+ function PaddingField({
946
+ icon: Icon,
947
+ ariaLabel,
948
+ value,
612
949
  onChange,
950
+ disabled,
951
+ }: {
952
+ icon: (props: { className?: string }) => ReactNode;
953
+ ariaLabel: string;
954
+ value: number;
955
+ onChange: (value: number) => void;
956
+ disabled: boolean;
957
+ }) {
958
+ return (
959
+ <div
960
+ className={cn(
961
+ "flex h-7 min-w-0 items-center rounded-md bg-[var(--design-editor-control-bg)] pl-1.5",
962
+ disabled && "opacity-40",
963
+ )}
964
+ >
965
+ <span className="flex shrink-0 items-center text-muted-foreground">
966
+ <Icon className="size-3.5" />
967
+ </span>
968
+ <ScrubInput
969
+ label={ariaLabel}
970
+ ariaLabel={ariaLabel}
971
+ value={value}
972
+ onChange={(next) => onChange(next)}
973
+ unit="px"
974
+ min={0}
975
+ step={1}
976
+ precision={1}
977
+ disabled={disabled}
978
+ className="min-w-0 flex-1 gap-0"
979
+ labelClassName="hidden"
980
+ inputClassName="h-6 border-0 bg-transparent px-1 text-[11px] shadow-none focus-visible:ring-0"
981
+ />
982
+ </div>
983
+ );
984
+ }
985
+
986
+ /** Link / unlink padding toggle button. */
987
+ function PaddingLinkButton({
988
+ linked,
989
+ disabled,
990
+ linkLabel,
991
+ unlinkLabel,
992
+ onToggle,
613
993
  }: {
994
+ linked: boolean;
995
+ disabled: boolean;
996
+ linkLabel: string;
997
+ unlinkLabel: string;
998
+ onToggle: () => void;
999
+ }) {
1000
+ return (
1001
+ <Tooltip>
1002
+ <TooltipTrigger asChild>
1003
+ <Button
1004
+ type="button"
1005
+ variant="ghost"
1006
+ size="icon"
1007
+ disabled={disabled}
1008
+ aria-label={linked ? unlinkLabel : linkLabel}
1009
+ onClick={onToggle}
1010
+ className={cn(
1011
+ "size-7 shrink-0 rounded-md hover:bg-[var(--design-editor-control-bg)]",
1012
+ linked
1013
+ ? "text-[var(--design-editor-accent-color,hsl(var(--primary)))] hover:text-[var(--design-editor-accent-color,hsl(var(--primary)))]"
1014
+ : "text-muted-foreground hover:text-foreground",
1015
+ )}
1016
+ >
1017
+ {linked ? <IconPaddingLinked /> : <IconPaddingUnlinked />}
1018
+ </Button>
1019
+ </TooltipTrigger>
1020
+ <TooltipContent>{linked ? unlinkLabel : linkLabel}</TooltipContent>
1021
+ </Tooltip>
1022
+ );
1023
+ }
1024
+
1025
+ interface SizingFieldMinMax {
1026
+ min?: number | null;
1027
+ max?: number | null;
1028
+ }
1029
+
1030
+ export interface SizingFieldProps {
1031
+ /** Display letter ("W" / "H"). */
614
1032
  axis: string;
1033
+ /** Logical axis used by min/max + variable callbacks. */
1034
+ sizingAxis: AutoLayoutSizingAxis;
615
1035
  value: AutoLayoutSizing;
616
1036
  resolvedSize?: number;
1037
+ /** Currently-set min/max constraints (px). */
1038
+ minMax?: SizingFieldMinMax;
617
1039
  options?: AutoLayoutSizing[];
618
- labels: AutoLayoutMatrixLabels;
1040
+ /** Optional label overrides; English defaults are used for any omitted key. */
1041
+ labels?: Partial<AutoLayoutMatrixLabels>;
619
1042
  disabled: boolean;
620
1043
  onChange: (value: AutoLayoutSizing) => void;
1044
+ onMinMaxChange?: (
1045
+ axis: AutoLayoutSizingAxis,
1046
+ kind: "min" | "max",
1047
+ value: number | null,
1048
+ ) => void;
1049
+ onApplyVariable?: (axis: AutoLayoutSizingAxis) => void;
1050
+ }
1051
+
1052
+ /**
1053
+ * Figma-style sizing field. Trigger renders `[axis | value | mode ▾]`; the menu
1054
+ * is the full Figma resizing dropdown:
1055
+ * Fixed · Hug contents · Fill container
1056
+ * ──────────────────────
1057
+ * Add min … · Add max …
1058
+ * ──────────────────────
1059
+ * Apply variable …
1060
+ * "Add min/max" reveals an inline number input; set constraints render as a
1061
+ * small sub-row below the trigger with a remove (×). Hug/Fill rows are gated by
1062
+ * the `options` list (per-axis availability); Fixed is always present.
1063
+ */
1064
+ export function SizingField({
1065
+ axis,
1066
+ sizingAxis,
1067
+ value,
1068
+ resolvedSize,
1069
+ minMax,
1070
+ options = SIZING_OPTIONS,
1071
+ labels: labelOverrides,
1072
+ disabled,
1073
+ onChange,
1074
+ onMinMaxChange,
1075
+ onApplyVariable,
1076
+ }: SizingFieldProps) {
1077
+ const labels = { ...DEFAULT_AUTO_LAYOUT_LABELS, ...labelOverrides };
1078
+ const isWidth = sizingAxis === "horizontal";
1079
+ // Which inline "add" editor is currently open (none by default).
1080
+ const [editing, setEditing] = useState<null | "min" | "max">(null);
1081
+
1082
+ const minValue = minMax?.min ?? null;
1083
+ const maxValue = minMax?.max ?? null;
1084
+ const hasMin = minValue != null;
1085
+ const hasMax = maxValue != null;
1086
+
1087
+ const canHug = options.includes("hug");
1088
+ const canFill = options.includes("fill");
1089
+
1090
+ // Figma rule: when Fixed, show ONLY the numeric value + chevron (no word).
1091
+ // When Hug / Fill, show value + the mode word.
1092
+ const showWord = value !== "fixed";
1093
+
1094
+ const addMinLabel = isWidth ? labels.addMinWidth : labels.addMinHeight;
1095
+ const addMaxLabel = isWidth ? labels.addMaxWidth : labels.addMaxHeight;
1096
+ const minLabel = isWidth ? labels.minWidth : labels.minHeight;
1097
+ const maxLabel = isWidth ? labels.maxWidth : labels.maxHeight;
1098
+
1099
+ const openEditor = (kind: "min" | "max") => {
1100
+ // Seed a sensible default when first adding the constraint.
1101
+ const seed = Math.max(0, Math.round(resolvedSize ?? 0));
1102
+ onMinMaxChange?.(sizingAxis, kind, kind === "min" ? seed : seed || 1);
1103
+ setEditing(kind);
1104
+ };
1105
+
1106
+ return (
1107
+ <div className="flex min-w-0 flex-col gap-1">
1108
+ <DropdownMenu>
1109
+ <Tooltip>
1110
+ <TooltipTrigger asChild>
1111
+ <DropdownMenuTrigger asChild>
1112
+ <button
1113
+ type="button"
1114
+ aria-label={`${axis} ${Math.round(resolvedSize ?? 0)} ${labels[value]}`}
1115
+ disabled={disabled}
1116
+ className={cn(
1117
+ "flex h-7 w-full items-center gap-1 overflow-hidden rounded-md px-1.5",
1118
+ "bg-[var(--design-editor-control-bg)] text-[11px]",
1119
+ "hover:bg-[var(--design-editor-panel-raised-bg)]",
1120
+ "disabled:pointer-events-none disabled:opacity-40",
1121
+ "focus:outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color,hsl(var(--primary)))]",
1122
+ )}
1123
+ >
1124
+ {/* Axis letter */}
1125
+ <span className="shrink-0 text-muted-foreground">{axis}</span>
1126
+ {/* Resolved size */}
1127
+ <span className="min-w-0 flex-1 truncate text-left tabular-nums text-foreground">
1128
+ {Math.round(resolvedSize ?? 0)}
1129
+ </span>
1130
+ {/* Mode word (Hug/Fill only) */}
1131
+ {showWord ? (
1132
+ <span className="shrink-0 truncate text-muted-foreground">
1133
+ {labels[value]}
1134
+ </span>
1135
+ ) : null}
1136
+ {/* Caret */}
1137
+ <span className="flex shrink-0 items-center text-muted-foreground">
1138
+ <ChevronDownMini />
1139
+ </span>
1140
+ </button>
1141
+ </DropdownMenuTrigger>
1142
+ </TooltipTrigger>
1143
+ <TooltipContent>{`${axis} · ${labels[value]}`}</TooltipContent>
1144
+ </Tooltip>
1145
+ <DropdownMenuContent
1146
+ align="start"
1147
+ className="min-w-[180px] text-[12px]"
1148
+ sideOffset={4}
1149
+ >
1150
+ {/* ── Modes ── */}
1151
+ <SizingMenuItem
1152
+ icon={<IconSizingFixed />}
1153
+ label={labels.fixed}
1154
+ active={value === "fixed"}
1155
+ onSelect={() => onChange("fixed")}
1156
+ />
1157
+ {canHug ? (
1158
+ <SizingMenuItem
1159
+ icon={<IconSizingHug />}
1160
+ label={labels.hugContents}
1161
+ active={value === "hug"}
1162
+ onSelect={() => onChange("hug")}
1163
+ />
1164
+ ) : null}
1165
+ {canFill ? (
1166
+ <SizingMenuItem
1167
+ icon={<IconSizingFill />}
1168
+ label={labels.fillContainer}
1169
+ active={value === "fill"}
1170
+ onSelect={() => onChange("fill")}
1171
+ />
1172
+ ) : null}
1173
+
1174
+ {/* ── Min / Max ── */}
1175
+ {onMinMaxChange ? (
1176
+ <>
1177
+ <DropdownMenuSeparator />
1178
+ <SizingMenuItem
1179
+ icon={<IconSizingMin />}
1180
+ label={addMinLabel}
1181
+ active={hasMin}
1182
+ disabled={hasMin}
1183
+ onSelect={() => openEditor("min")}
1184
+ />
1185
+ <SizingMenuItem
1186
+ icon={<IconSizingMax />}
1187
+ label={addMaxLabel}
1188
+ active={hasMax}
1189
+ disabled={hasMax}
1190
+ onSelect={() => openEditor("max")}
1191
+ />
1192
+ </>
1193
+ ) : null}
1194
+
1195
+ {/* ── Variable ── */}
1196
+ <DropdownMenuSeparator />
1197
+ <SizingMenuItem
1198
+ icon={<IconSizingVariable />}
1199
+ label={labels.applyVariable}
1200
+ disabled={!onApplyVariable}
1201
+ onSelect={() => onApplyVariable?.(sizingAxis)}
1202
+ />
1203
+ </DropdownMenuContent>
1204
+ </DropdownMenu>
1205
+
1206
+ {/* ── Constraint sub-rows ── */}
1207
+ {hasMin ? (
1208
+ <ConstraintSubRow
1209
+ label={minLabel}
1210
+ value={minValue ?? 0}
1211
+ disabled={disabled}
1212
+ removeLabel={labels.removeConstraint}
1213
+ onChange={(next) => onMinMaxChange?.(sizingAxis, "min", next)}
1214
+ onRemove={() => {
1215
+ onMinMaxChange?.(sizingAxis, "min", null);
1216
+ setEditing((cur) => (cur === "min" ? null : cur));
1217
+ }}
1218
+ />
1219
+ ) : null}
1220
+ {hasMax ? (
1221
+ <ConstraintSubRow
1222
+ label={maxLabel}
1223
+ value={maxValue ?? 0}
1224
+ disabled={disabled}
1225
+ removeLabel={labels.removeConstraint}
1226
+ onChange={(next) => onMinMaxChange?.(sizingAxis, "max", next)}
1227
+ onRemove={() => {
1228
+ onMinMaxChange?.(sizingAxis, "max", null);
1229
+ setEditing((cur) => (cur === "max" ? null : cur));
1230
+ }}
1231
+ />
1232
+ ) : null}
1233
+ </div>
1234
+ );
1235
+ }
1236
+
1237
+ /** A single icon + label row in the sizing menu, with an active checkmark. */
1238
+ function SizingMenuItem({
1239
+ icon,
1240
+ label,
1241
+ active = false,
1242
+ disabled = false,
1243
+ onSelect,
1244
+ }: {
1245
+ icon: ReactNode;
1246
+ label: string;
1247
+ active?: boolean;
1248
+ disabled?: boolean;
1249
+ onSelect: () => void;
621
1250
  }) {
622
- // Ensure current value is in list (guard against stale state)
623
- const validOptions: AutoLayoutSizing[] = options.includes(value)
624
- ? options
625
- : [...options, value];
1251
+ return (
1252
+ <DropdownMenuItem
1253
+ disabled={disabled}
1254
+ onSelect={(event) => {
1255
+ // Keep the menu's selection semantics but route through our handler.
1256
+ event.preventDefault();
1257
+ onSelect();
1258
+ }}
1259
+ className="gap-2 pl-2 pr-2 text-[12px]"
1260
+ >
1261
+ <span className="flex size-4 shrink-0 items-center justify-center text-muted-foreground">
1262
+ {icon}
1263
+ </span>
1264
+ <span className="min-w-0 flex-1 truncate">{label}</span>
1265
+ <span className="flex size-3.5 shrink-0 items-center justify-center text-[var(--design-editor-accent-color,hsl(var(--primary)))]">
1266
+ {active ? <CheckMini /> : null}
1267
+ </span>
1268
+ </DropdownMenuItem>
1269
+ );
1270
+ }
626
1271
 
1272
+ /** Inline min/max constraint editor row with a remove (×) affordance. */
1273
+ function ConstraintSubRow({
1274
+ label,
1275
+ value,
1276
+ disabled,
1277
+ removeLabel,
1278
+ onChange,
1279
+ onRemove,
1280
+ }: {
1281
+ label: string;
1282
+ value: number;
1283
+ disabled: boolean;
1284
+ removeLabel: string;
1285
+ onChange: (value: number) => void;
1286
+ onRemove: () => void;
1287
+ }) {
627
1288
  return (
628
- <DropdownMenu>
1289
+ <div
1290
+ className={cn(
1291
+ "flex h-6 min-w-0 items-center rounded-md bg-[var(--design-editor-control-bg)] pl-1.5",
1292
+ disabled && "opacity-40",
1293
+ )}
1294
+ >
1295
+ <ScrubInput
1296
+ label={label}
1297
+ ariaLabel={label}
1298
+ value={value}
1299
+ onChange={(next) => onChange(Math.max(0, Math.round(next)))}
1300
+ unit="px"
1301
+ min={0}
1302
+ step={1}
1303
+ precision={1}
1304
+ disabled={disabled}
1305
+ className="min-w-0 flex-1 gap-0"
1306
+ labelClassName="hidden"
1307
+ inputClassName="h-5 border-0 bg-transparent px-1 text-[11px] shadow-none focus-visible:ring-0"
1308
+ />
629
1309
  <Tooltip>
630
1310
  <TooltipTrigger asChild>
631
- <DropdownMenuTrigger asChild>
632
- <button
633
- type="button"
634
- aria-label={`${axis} ${labels[value]}`}
635
- disabled={disabled}
636
- className={cn(
637
- "flex h-6 w-full items-center overflow-hidden rounded-md",
638
- "bg-[var(--design-editor-control-bg)] text-[11px]",
639
- "hover:bg-[var(--design-editor-panel-raised-bg)]",
640
- "disabled:pointer-events-none disabled:opacity-40",
641
- "focus:outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]",
642
- )}
643
- >
644
- {/* Axis letter */}
645
- <span className="flex h-full w-4 shrink-0 items-center justify-center border-r border-[var(--design-editor-control-border,hsl(var(--border)/0.6))] text-muted-foreground">
646
- {axis}
647
- </span>
648
- {/* Resolved size */}
649
- <span className="min-w-0 flex-1 truncate px-0.5 text-center tabular-nums text-foreground">
650
- {Math.round(resolvedSize ?? 0)}
651
- </span>
652
- {/* Mode word + caret */}
653
- <span className="flex h-full shrink-0 items-center gap-0 border-l border-[var(--design-editor-control-border,hsl(var(--border)/0.6))] px-0.5 text-muted-foreground">
654
- {labels[value]}
655
- <svg
656
- viewBox="0 0 8 8"
657
- width={8}
658
- height={8}
659
- fill="currentColor"
660
- aria-hidden="true"
661
- className="opacity-60"
662
- >
663
- <path
664
- d="M1.5 3 L4 5.5 L6.5 3"
665
- fill="none"
666
- stroke="currentColor"
667
- strokeWidth="1.2"
668
- strokeLinecap="round"
669
- strokeLinejoin="round"
670
- />
671
- </svg>
672
- </span>
673
- </button>
674
- </DropdownMenuTrigger>
675
- </TooltipTrigger>
676
- <TooltipContent>{`${axis} ${labels[value]}`}</TooltipContent>
677
- </Tooltip>
678
- <DropdownMenuContent
679
- align="start"
680
- className="min-w-[100px] text-[12px]"
681
- sideOffset={4}
682
- >
683
- {validOptions.map((opt) => (
684
- <DropdownMenuItem
685
- key={opt}
686
- onSelect={() => onChange(opt)}
1311
+ <button
1312
+ type="button"
1313
+ aria-label={removeLabel}
1314
+ disabled={disabled}
1315
+ onClick={onRemove}
687
1316
  className={cn(
688
- "text-[12px]",
689
- opt === value &&
690
- "font-medium text-[var(--design-editor-accent-color)]",
1317
+ "flex h-6 w-5 shrink-0 items-center justify-center rounded-r-md",
1318
+ "text-muted-foreground hover:text-foreground",
1319
+ "disabled:pointer-events-none disabled:opacity-40",
691
1320
  )}
692
1321
  >
693
- {labels[opt]}
694
- </DropdownMenuItem>
695
- ))}
696
- </DropdownMenuContent>
697
- </DropdownMenu>
1322
+ <IconSizingRemove />
1323
+ </button>
1324
+ </TooltipTrigger>
1325
+ <TooltipContent>{removeLabel}</TooltipContent>
1326
+ </Tooltip>
1327
+ </div>
1328
+ );
1329
+ }
1330
+
1331
+ /** Small check glyph for the active menu row. */
1332
+ function CheckMini() {
1333
+ return (
1334
+ <svg
1335
+ viewBox="0 0 14 14"
1336
+ width={12}
1337
+ height={12}
1338
+ fill="none"
1339
+ aria-hidden="true"
1340
+ >
1341
+ <path
1342
+ d="M2.5 7.5 L5.5 10.5 L11.5 3.5"
1343
+ stroke="currentColor"
1344
+ strokeWidth="1.6"
1345
+ strokeLinecap="round"
1346
+ strokeLinejoin="round"
1347
+ />
1348
+ </svg>
1349
+ );
1350
+ }
1351
+
1352
+ // ─────────────────────────────────────────────────
1353
+ // Inline SVG glyphs (Tabler-weight, 24px viewBox)
1354
+ // ─────────────────────────────────────────────────
1355
+
1356
+ /** Minimal downward chevron — matches the Figma caret weight. */
1357
+ function ChevronDownMini() {
1358
+ return (
1359
+ <svg
1360
+ viewBox="0 0 8 8"
1361
+ width={8}
1362
+ height={8}
1363
+ fill="none"
1364
+ aria-hidden="true"
1365
+ className="opacity-70"
1366
+ >
1367
+ <path
1368
+ d="M1.5 3 L4 5.5 L6.5 3"
1369
+ stroke="currentColor"
1370
+ strokeWidth="1.2"
1371
+ strokeLinecap="round"
1372
+ strokeLinejoin="round"
1373
+ />
1374
+ </svg>
1375
+ );
1376
+ }
1377
+
1378
+ /** Normal / free-flow layout glyph — loosely placed small rects. */
1379
+ function IconFlowNormal() {
1380
+ return (
1381
+ <svg
1382
+ viewBox="0 0 24 24"
1383
+ width={14}
1384
+ height={14}
1385
+ fill="currentColor"
1386
+ aria-hidden="true"
1387
+ >
1388
+ <rect x="4" y="4" width="6" height="6" rx="1.5" />
1389
+ <rect x="14" y="6" width="6" height="6" rx="1.5" />
1390
+ <rect x="6" y="14" width="6" height="6" rx="1.5" />
1391
+ </svg>
1392
+ );
1393
+ }
1394
+
1395
+ /** Resize-to-fit diagonal arrows. */
1396
+ function IconResizeToFitMini() {
1397
+ return (
1398
+ <svg
1399
+ viewBox="0 0 24 24"
1400
+ width={16}
1401
+ height={16}
1402
+ fill="none"
1403
+ stroke="currentColor"
1404
+ strokeWidth="1.6"
1405
+ strokeLinecap="round"
1406
+ strokeLinejoin="round"
1407
+ aria-hidden="true"
1408
+ >
1409
+ <path d="M9 4 H5 a1 1 0 0 0 -1 1 V9" />
1410
+ <path d="M15 4 H19 a1 1 0 0 1 1 1 V9" />
1411
+ <path d="M9 20 H5 a1 1 0 0 1 -1 -1 V15" />
1412
+ <path d="M15 20 H19 a1 1 0 0 0 1 -1 V15" />
1413
+ </svg>
1414
+ );
1415
+ }
1416
+
1417
+ /** Sliders / tune icon for advanced gap settings. */
1418
+ function IconSlidersMini() {
1419
+ return (
1420
+ <svg
1421
+ viewBox="0 0 24 24"
1422
+ width={16}
1423
+ height={16}
1424
+ fill="none"
1425
+ stroke="currentColor"
1426
+ strokeWidth="1.6"
1427
+ strokeLinecap="round"
1428
+ strokeLinejoin="round"
1429
+ aria-hidden="true"
1430
+ >
1431
+ <line x1="8" y1="4" x2="8" y2="20" />
1432
+ <line x1="16" y1="4" x2="16" y2="20" />
1433
+ <circle cx="8" cy="9" r="2.2" fill="var(--design-editor-panel-bg)" />
1434
+ <circle cx="16" cy="15" r="2.2" fill="var(--design-editor-panel-bg)" />
1435
+ </svg>
1436
+ );
1437
+ }
1438
+
1439
+ /** Padding linked (all four sides) glyph. */
1440
+ function IconPaddingLinked() {
1441
+ return (
1442
+ <svg
1443
+ viewBox="0 0 24 24"
1444
+ width={16}
1445
+ height={16}
1446
+ fill="none"
1447
+ stroke="currentColor"
1448
+ strokeWidth="1.6"
1449
+ strokeLinecap="round"
1450
+ strokeLinejoin="round"
1451
+ aria-hidden="true"
1452
+ >
1453
+ <rect x="4" y="4" width="16" height="16" rx="2" />
1454
+ <rect x="9" y="9" width="6" height="6" rx="1" />
1455
+ </svg>
1456
+ );
1457
+ }
1458
+
1459
+ /** Padding unlinked glyph (dashed inner frame). */
1460
+ function IconPaddingUnlinked() {
1461
+ return (
1462
+ <svg
1463
+ viewBox="0 0 24 24"
1464
+ width={16}
1465
+ height={16}
1466
+ fill="none"
1467
+ stroke="currentColor"
1468
+ strokeWidth="1.6"
1469
+ strokeLinecap="round"
1470
+ strokeLinejoin="round"
1471
+ aria-hidden="true"
1472
+ >
1473
+ <rect x="4" y="4" width="16" height="16" rx="2" strokeDasharray="3 2.5" />
1474
+ <rect x="9" y="9" width="6" height="6" rx="1" />
1475
+ </svg>
1476
+ );
1477
+ }
1478
+
1479
+ /** Per-side padding glyphs — frame with one thick edge. */
1480
+ function IconPaddingTopMini({ className }: { className?: string }) {
1481
+ return <PaddingSideGlyph className={className} side="top" />;
1482
+ }
1483
+ function IconPaddingRightMini({ className }: { className?: string }) {
1484
+ return <PaddingSideGlyph className={className} side="right" />;
1485
+ }
1486
+ function IconPaddingBottomMini({ className }: { className?: string }) {
1487
+ return <PaddingSideGlyph className={className} side="bottom" />;
1488
+ }
1489
+ function IconPaddingLeftMini({ className }: { className?: string }) {
1490
+ return <PaddingSideGlyph className={className} side="left" />;
1491
+ }
1492
+
1493
+ type PaddingSide = "top" | "right" | "bottom" | "left";
1494
+
1495
+ const PADDING_EDGE: Record<PaddingSide, [number, number, number, number]> = {
1496
+ top: [4, 4, 20, 4],
1497
+ right: [20, 4, 20, 20],
1498
+ bottom: [4, 20, 20, 20],
1499
+ left: [4, 4, 4, 20],
1500
+ };
1501
+
1502
+ function PaddingSideGlyph({
1503
+ side,
1504
+ className,
1505
+ }: {
1506
+ side: PaddingSide;
1507
+ className?: string;
1508
+ }) {
1509
+ const [x1, y1, x2, y2] = PADDING_EDGE[side];
1510
+ return (
1511
+ <svg
1512
+ viewBox="0 0 24 24"
1513
+ className={cn("size-3.5", className)}
1514
+ fill="none"
1515
+ stroke="currentColor"
1516
+ strokeWidth="1.5"
1517
+ strokeLinecap="round"
1518
+ strokeLinejoin="round"
1519
+ aria-hidden="true"
1520
+ >
1521
+ <rect x="4" y="4" width="16" height="16" rx="2" opacity="0.4" />
1522
+ <line x1={x1} y1={y1} x2={x2} y2={y2} strokeWidth="3" />
1523
+ </svg>
1524
+ );
1525
+ }
1526
+
1527
+ /** Distribute horizontal spacing (space-between on main/cross axis). */
1528
+ function IconDistributeH() {
1529
+ return (
1530
+ <svg
1531
+ viewBox="0 0 14 14"
1532
+ width={14}
1533
+ height={14}
1534
+ fill="none"
1535
+ stroke="currentColor"
1536
+ strokeWidth="1.2"
1537
+ strokeLinecap="round"
1538
+ aria-hidden="true"
1539
+ >
1540
+ {/* left edge line */}
1541
+ <line x1="1.5" y1="2" x2="1.5" y2="12" />
1542
+ {/* right edge line */}
1543
+ <line x1="12.5" y1="2" x2="12.5" y2="12" />
1544
+ {/* center block */}
1545
+ <rect
1546
+ x="4.5"
1547
+ y="4"
1548
+ width="5"
1549
+ height="6"
1550
+ rx="1"
1551
+ fill="currentColor"
1552
+ stroke="none"
1553
+ opacity="0.5"
1554
+ />
1555
+ </svg>
1556
+ );
1557
+ }
1558
+
1559
+ /** Distribute vertical spacing (space-between on cross/main axis). */
1560
+ function IconDistributeV() {
1561
+ return (
1562
+ <svg
1563
+ viewBox="0 0 14 14"
1564
+ width={14}
1565
+ height={14}
1566
+ fill="none"
1567
+ stroke="currentColor"
1568
+ strokeWidth="1.2"
1569
+ strokeLinecap="round"
1570
+ aria-hidden="true"
1571
+ >
1572
+ {/* top edge line */}
1573
+ <line x1="2" y1="1.5" x2="12" y2="1.5" />
1574
+ {/* bottom edge line */}
1575
+ <line x1="2" y1="12.5" x2="12" y2="12.5" />
1576
+ {/* center block */}
1577
+ <rect
1578
+ x="4"
1579
+ y="4.5"
1580
+ width="6"
1581
+ height="5"
1582
+ rx="1"
1583
+ fill="currentColor"
1584
+ stroke="none"
1585
+ opacity="0.5"
1586
+ />
1587
+ </svg>
698
1588
  );
699
1589
  }