@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
@@ -4,6 +4,7 @@ import {
4
4
  alphaToOpacity,
5
5
  parseCssColor,
6
6
  rgbaToCss,
7
+ rgbaToHex,
7
8
  withColorOpacity,
8
9
  } from "@shared/color-utils";
9
10
  import {
@@ -11,18 +12,24 @@ import {
11
12
  IconAlignJustified,
12
13
  IconAlignLeft,
13
14
  IconAlignRight,
15
+ IconArrowAutofitHeight,
16
+ IconArrowAutofitWidth,
17
+ IconBackground,
18
+ IconBlur,
14
19
  IconBorderStyle,
15
20
  IconBrush,
16
21
  IconChevronDown,
17
- IconChevronRight,
18
22
  IconCode,
19
23
  IconComponents,
24
+ IconDroplet,
20
25
  IconEye,
21
26
  IconEyeOff,
22
27
  IconFlipHorizontal,
23
28
  IconFlipVertical,
24
29
  IconFrame,
30
+ IconLayoutDistributeHorizontal,
25
31
  IconLayoutGrid,
32
+ IconSlice,
26
33
  IconLayoutAlignBottom,
27
34
  IconLayoutAlignCenter,
28
35
  IconLayoutAlignLeft,
@@ -38,6 +45,8 @@ import {
38
45
  IconPalette,
39
46
  IconPhoto,
40
47
  IconPlus,
48
+ IconShadow,
49
+ IconSquare,
41
50
  IconTypography,
42
51
  IconUnlink,
43
52
  IconVector,
@@ -45,6 +54,12 @@ import {
45
54
  import { useCallback, useEffect, useState, type ReactNode } from "react";
46
55
 
47
56
  import { Button } from "@/components/ui/button";
57
+ import {
58
+ DropdownMenu,
59
+ DropdownMenuContent,
60
+ DropdownMenuItem,
61
+ DropdownMenuTrigger,
62
+ } from "@/components/ui/dropdown-menu";
48
63
  import { Input } from "@/components/ui/input";
49
64
  import { Label } from "@/components/ui/label";
50
65
  import {
@@ -78,6 +93,7 @@ import {
78
93
  ExportSettingsPanel,
79
94
  FigmaColorPicker,
80
95
  ScrubInput,
96
+ SizingField,
81
97
  type AlignmentMatrixValue,
82
98
  type AutoLayoutMatrixValue,
83
99
  type AutoLayoutSizing,
@@ -100,6 +116,7 @@ interface EditPanelProps {
100
116
  selectedElement: ElementInfo | null;
101
117
  pageStyles?: Record<string, string>;
102
118
  zoom?: number;
119
+ headerTrailing?: ReactNode;
103
120
  width?: number;
104
121
  activeTab?: InspectorTab;
105
122
  onActiveTabChange?: (tab: InspectorTab) => void;
@@ -203,6 +220,7 @@ function ColorInput({
203
220
  blendMode,
204
221
  onBlendModeChange,
205
222
  supportsLayeredFills = false,
223
+ documentColors,
206
224
  }: {
207
225
  label: string;
208
226
  value: string;
@@ -212,6 +230,8 @@ function ColorInput({
212
230
  blendMode?: string;
213
231
  onBlendModeChange?: (value: string) => void;
214
232
  supportsLayeredFills?: boolean;
233
+ /** Hex strings already in use on the page — forwarded to the color picker swatch grid. */
234
+ documentColors?: string[];
215
235
  }) {
216
236
  const [draft, setDraft] = useState(value);
217
237
  const [selectedFillId, setSelectedFillId] = useState(SOLID_FILL_ID);
@@ -285,6 +305,26 @@ function ColorInput({
285
305
  setSelectedFillId(SOLID_FILL_ID);
286
306
  };
287
307
 
308
+ const handlePaintValueChange = (nextValue: string) => {
309
+ if (!supportsLayeredFills || !onBackgroundImageChange) {
310
+ setNext(nextValue);
311
+ return;
312
+ }
313
+
314
+ const selectedLayer = fillLayerIndex(selectedFillId);
315
+ if (selectedLayer !== null) {
316
+ replaceBackgroundLayer(selectedLayer, nextValue);
317
+ const gradient = parseGradientLayer(nextValue);
318
+ if (gradient) setSelectedStopId(gradient.stops[0]?.id);
319
+ return;
320
+ }
321
+
322
+ onBackgroundImageChange(joinCssLayers([nextValue, ...backgroundLayers]));
323
+ setSelectedFillId(fillLayerId(0));
324
+ const gradient = parseGradientLayer(nextValue);
325
+ setSelectedStopId(gradient?.stops[0]?.id);
326
+ };
327
+
288
328
  const fillRows = supportsLayeredFills
289
329
  ? buildFillRows(
290
330
  draft || value || "#000000",
@@ -422,6 +462,10 @@ function ColorInput({
422
462
  : colorHasVisibleAlpha(draft || value)
423
463
  ? "solid"
424
464
  : "none";
465
+ const pickerValue =
466
+ selectedLayerIndex !== null
467
+ ? (backgroundLayers[selectedLayerIndex] ?? draft ?? value ?? "#000000")
468
+ : draft || "#000000";
425
469
 
426
470
  const handlePaintTypeChange = (type: FigmaPaintType) => {
427
471
  const selectedLayer = fillLayerIndex(selectedFillId);
@@ -476,8 +520,11 @@ function ColorInput({
476
520
  return (
477
521
  <FigmaColorPicker
478
522
  label={label}
479
- value={draft || "#000000"}
523
+ value={pickerValue}
480
524
  onChange={setNext}
525
+ onPaintValueChange={
526
+ supportsLayeredFills ? handlePaintValueChange : undefined
527
+ }
481
528
  blendMode={blendMode}
482
529
  onBlendModeChange={onBlendModeChange}
483
530
  showBlendMode={supportsLayeredFills}
@@ -499,6 +546,7 @@ function ColorInput({
499
546
  supportsLayeredFills ? handleAddGradientStop : undefined
500
547
  }
501
548
  onRemoveGradientStop={handleRemoveGradientStop}
549
+ documentColors={documentColors}
502
550
  />
503
551
  );
504
552
  }
@@ -808,41 +856,48 @@ function PropSlider({
808
856
 
809
857
  const EmptyScrubIcon = () => null;
810
858
 
859
+ /**
860
+ * Figma-style inspector section. Matches Figma's real "Design" panel chrome:
861
+ * - NO left collapse chevron (Figma has none).
862
+ * - A thin divider line above each section.
863
+ * - A bold left-aligned title.
864
+ * - Right-aligned action icons (add layer, toggles, styles, etc.).
865
+ *
866
+ * The title is still clickable to collapse the body (Figma collapses sections
867
+ * on title click) but renders no chevron glyph, just like Figma.
868
+ */
811
869
  function PanelSection({
812
870
  title,
813
871
  actions,
814
872
  children,
873
+ defaultCollapsed = false,
815
874
  }: {
816
875
  title: string;
817
876
  actions?: ReactNode;
818
877
  children?: ReactNode;
878
+ defaultCollapsed?: boolean;
819
879
  }) {
820
- const [collapsed, setCollapsed] = useState(false);
880
+ const [collapsed, setCollapsed] = useState(defaultCollapsed);
821
881
 
822
882
  return (
823
- <section className="shrink-0 border-b border-border/90">
824
- <div className="flex min-h-8 items-center gap-2 px-3 pt-1.5">
883
+ <section className="shrink-0 border-t border-[var(--design-editor-control-border)] first:border-t-0">
884
+ <div className="flex min-h-9 items-center gap-2 px-3">
825
885
  <button
826
886
  type="button"
827
- className="flex min-w-0 flex-1 cursor-pointer items-center gap-1 truncate bg-transparent text-left"
887
+ className="flex min-w-0 flex-1 cursor-pointer items-center bg-transparent text-left"
828
888
  onClick={() => setCollapsed((c) => !c)}
829
889
  aria-expanded={!collapsed}
830
890
  >
831
- {collapsed ? (
832
- <IconChevronRight className="size-3 shrink-0 text-muted-foreground" />
833
- ) : (
834
- <IconChevronDown className="size-3 shrink-0 text-muted-foreground" />
835
- )}
836
- <h3 className="min-w-0 flex-1 truncate text-xs font-semibold text-foreground">
891
+ <h3 className="min-w-0 flex-1 truncate text-[11px] font-semibold text-foreground">
837
892
  {title}
838
893
  </h3>
839
894
  </button>
840
895
  {actions ? (
841
- <div className="flex items-center gap-0.5">{actions}</div>
896
+ <div className="flex shrink-0 items-center gap-0.5">{actions}</div>
842
897
  ) : null}
843
898
  </div>
844
899
  {!collapsed && children ? (
845
- <div className="space-y-1.5 px-3 pb-2 pt-1.5 text-[11px]">
900
+ <div className="space-y-1.5 px-3 pb-3 pt-0.5 text-[11px]">
846
901
  {children}
847
902
  </div>
848
903
  ) : null}
@@ -1251,6 +1306,64 @@ function autoLayoutAlignmentFromStyles(
1251
1306
  };
1252
1307
  }
1253
1308
 
1309
+ /**
1310
+ * Block-level container tags that act like Figma frames. Selecting any of
1311
+ * these shows the Auto layout section (in an "add" state when not yet flex),
1312
+ * mirroring Figma where any frame/container exposes auto-layout controls.
1313
+ */
1314
+ const CONTAINER_TAGS = new Set([
1315
+ "div",
1316
+ "section",
1317
+ "main",
1318
+ "header",
1319
+ "footer",
1320
+ "nav",
1321
+ "article",
1322
+ "aside",
1323
+ "form",
1324
+ "ul",
1325
+ "ol",
1326
+ "figure",
1327
+ "fieldset",
1328
+ "details",
1329
+ "dialog",
1330
+ "blockquote",
1331
+ "table",
1332
+ "tbody",
1333
+ "thead",
1334
+ "tr",
1335
+ ]);
1336
+
1337
+ /** Leaf tags that never get auto-layout (text, media, vectors, controls). */
1338
+ const LEAF_TAGS = new Set([
1339
+ "img",
1340
+ "video",
1341
+ "picture",
1342
+ "audio",
1343
+ "canvas",
1344
+ "svg",
1345
+ "path",
1346
+ "input",
1347
+ "textarea",
1348
+ "select",
1349
+ "br",
1350
+ "hr",
1351
+ "iframe",
1352
+ ]);
1353
+
1354
+ /**
1355
+ * Whether the element should expose the Auto layout section. True for anything
1356
+ * already laid out with flexbox, or any block-level container tag that isn't a
1357
+ * known leaf/text element. This is what makes a plain frame/container with
1358
+ * children show the full Auto layout section like Figma does.
1359
+ */
1360
+ function isContainerElement(element: ElementInfo): boolean {
1361
+ if (element.isFlexContainer) return true;
1362
+ const tag = (element.tagName || "").toLowerCase();
1363
+ if (TEXT_TAGS.has(tag) || LEAF_TAGS.has(tag)) return false;
1364
+ return CONTAINER_TAGS.has(tag);
1365
+ }
1366
+
1254
1367
  function isParentFlex(element: ElementInfo): boolean {
1255
1368
  return (
1256
1369
  element.isFlexChild ||
@@ -1268,19 +1381,103 @@ function parentFlexDirection(element: ElementInfo): AutoLayoutSizingAxis {
1268
1381
  : "horizontal";
1269
1382
  }
1270
1383
 
1384
+ function isTextElement(element: ElementInfo): boolean {
1385
+ return TEXT_TAGS.has((element.tagName || "").toLowerCase());
1386
+ }
1387
+
1388
+ /**
1389
+ * Per-axis sizing availability following Figma's contextual rules:
1390
+ * - Fixed: always.
1391
+ * - Hug contents: only CONTAINERS (flex/container frames) and TEXT can hug
1392
+ * their content. Leaves like img/svg/input cannot.
1393
+ * - Fill container: only when the element is a CHILD of a flex/grid (auto
1394
+ * layout) parent, OR a block-flow child (which fills via width:100%).
1395
+ * Hug applies to width and height independently; the same set is offered on
1396
+ * both axes here and the per-axis CSS in `commitElementSizing` resolves the
1397
+ * exact behavior (main-axis grow vs cross-axis stretch).
1398
+ */
1271
1399
  function availableSizingForElement(
1272
1400
  element: ElementInfo,
1273
1401
  ): Partial<Record<AutoLayoutSizingAxis, AutoLayoutSizing[]>> {
1274
- const options: AutoLayoutSizing[] =
1275
- isParentFlex(element) || isParentGrid(element)
1276
- ? ["hug", "fill", "fixed"]
1277
- : ["hug", "fixed"];
1402
+ const canHug = isContainerElement(element) || isTextElement(element);
1403
+ const isFlexChildEl = isParentFlex(element) || isParentGrid(element);
1404
+ // Block-flow children can still "fill" via width:100% on the horizontal axis.
1405
+ const isBlockChild = Boolean(element.parentDisplay) && !isFlexChildEl;
1406
+
1407
+ const buildAxis = (axis: AutoLayoutSizingAxis): AutoLayoutSizing[] => {
1408
+ const options: AutoLayoutSizing[] = ["fixed"];
1409
+ if (canHug) options.push("hug");
1410
+ // Fill: flex/grid child on either axis; block child only fills width.
1411
+ if (isFlexChildEl || (isBlockChild && axis === "horizontal")) {
1412
+ options.push("fill");
1413
+ }
1414
+ return options;
1415
+ };
1416
+
1417
+ return {
1418
+ horizontal: buildAxis("horizontal"),
1419
+ vertical: buildAxis("vertical"),
1420
+ };
1421
+ }
1422
+
1423
+ /** Read the currently-set min/max constraints (px) for a sizing axis. */
1424
+ function readElementMinMax(
1425
+ element: ElementInfo,
1426
+ axis: AutoLayoutSizingAxis,
1427
+ ): { min: number | null; max: number | null } {
1428
+ const styles = element.computedStyles;
1429
+ const minRaw = axis === "horizontal" ? styles.minWidth : styles.minHeight;
1430
+ const maxRaw = axis === "horizontal" ? styles.maxWidth : styles.maxHeight;
1278
1431
  return {
1279
- horizontal: options,
1280
- vertical: options,
1432
+ min: parseConstraintLength(minRaw),
1433
+ max: parseConstraintLength(maxRaw),
1281
1434
  };
1282
1435
  }
1283
1436
 
1437
+ /**
1438
+ * Parse a min/max CSS length into a px number, or null when unset. Browser
1439
+ * computed values are "0px"/"none" for the defaults — both read as "not set"
1440
+ * so we don't surface a constraint sub-row the user never added.
1441
+ */
1442
+ function parseConstraintLength(value: string | undefined): number | null {
1443
+ const normalized = value?.trim().toLowerCase();
1444
+ if (
1445
+ !normalized ||
1446
+ normalized === "none" ||
1447
+ normalized === "auto" ||
1448
+ normalized === "0px" ||
1449
+ normalized === "0"
1450
+ ) {
1451
+ return null;
1452
+ }
1453
+ const parsed = Number.parseFloat(normalized);
1454
+ return Number.isFinite(parsed) ? parsed : null;
1455
+ }
1456
+
1457
+ /** Commit a single min/max constraint (px) or clear it when value is null. */
1458
+ function commitElementMinMax(
1459
+ axis: AutoLayoutSizingAxis,
1460
+ kind: "min" | "max",
1461
+ value: number | null,
1462
+ onStyleChange: (property: string, value: string) => void,
1463
+ ) {
1464
+ const isHorizontal = axis === "horizontal";
1465
+ const property =
1466
+ kind === "min"
1467
+ ? isHorizontal
1468
+ ? "minWidth"
1469
+ : "minHeight"
1470
+ : isHorizontal
1471
+ ? "maxWidth"
1472
+ : "maxHeight";
1473
+ if (value == null) {
1474
+ // Clearing: min → 0 (CSS initial), max → none (CSS initial).
1475
+ onStyleChange(property, kind === "min" ? "0px" : "none");
1476
+ return;
1477
+ }
1478
+ onStyleChange(property, `${Math.max(0, Math.round(value))}px`);
1479
+ }
1480
+
1284
1481
  function inferElementSizing(
1285
1482
  element: ElementInfo,
1286
1483
  axis: AutoLayoutSizingAxis,
@@ -1288,11 +1485,15 @@ function inferElementSizing(
1288
1485
  const styles = element.computedStyles;
1289
1486
  const size = axis === "horizontal" ? styles.width : styles.height;
1290
1487
  const parentDirection = parentFlexDirection(element);
1291
- const isMainFlexAxis = isParentFlex(element) && parentDirection === axis;
1488
+ const isFlex = isParentFlex(element);
1489
+ const isMainFlexAxis = isFlex && parentDirection === axis;
1490
+ const isCrossFlexAxis = isFlex && parentDirection !== axis;
1491
+ const alignSelf = (styles.alignSelf || "").toLowerCase();
1292
1492
 
1293
1493
  if (
1294
1494
  size === "100%" ||
1295
- (isMainFlexAxis && Number.parseFloat(styles.flexGrow || "0") > 0)
1495
+ (isMainFlexAxis && Number.parseFloat(styles.flexGrow || "0") > 0) ||
1496
+ (isCrossFlexAxis && alignSelf === "stretch")
1296
1497
  ) {
1297
1498
  return "fill";
1298
1499
  }
@@ -1324,29 +1525,43 @@ function commitElementSizing(
1324
1525
  const patch: Record<string, string> = {};
1325
1526
 
1326
1527
  if (sizing === "fixed") {
1528
+ // Fixed → explicit px dimension. Reset any grow/stretch on the flex
1529
+ // main-axis so the pixel value sticks.
1327
1530
  patch[sizeProperty] = `${resolvedSize}px`;
1328
1531
  if (isMainFlexAxis) {
1329
1532
  patch.flexGrow = "0";
1330
1533
  patch.flexShrink = "0";
1331
1534
  patch.flexBasis = "auto";
1332
1535
  }
1333
- } else if (sizing === "fill") {
1536
+ } else if (sizing === "hug") {
1537
+ // Hug contents → shrink to fit children/content.
1538
+ patch[sizeProperty] = "fit-content";
1334
1539
  if (isMainFlexAxis) {
1335
- patch[sizeProperty] = "auto";
1336
- patch.flexGrow = "1";
1337
- patch.flexShrink = "1";
1338
- patch.flexBasis = "0px";
1339
- } else {
1340
- patch[sizeProperty] = "100%";
1341
- if (isFlex) patch.alignSelf = "stretch";
1342
- if (isGrid) patch[isHorizontal ? "justifySelf" : "alignSelf"] = "stretch";
1540
+ // A flex container hugging on its main axis uses flex-basis:auto + no
1541
+ // stretch (spec: "flex-basis: auto + no stretch").
1542
+ patch.flexGrow = "0";
1543
+ patch.flexShrink = "0";
1544
+ patch.flexBasis = "auto";
1343
1545
  }
1344
1546
  } else {
1345
- patch[sizeProperty] = "auto";
1547
+ // Fill container.
1346
1548
  if (isMainFlexAxis) {
1347
- patch.flexGrow = "0";
1348
- patch.flexShrink = "1";
1349
- patch.flexBasis = "auto";
1549
+ // Parent main axis → grow into available space: flex: 1 0 0.
1550
+ patch.flexGrow = "1";
1551
+ patch.flexShrink = "0";
1552
+ patch.flexBasis = "0";
1553
+ // Clear any explicit dimension so flex-basis governs.
1554
+ patch[sizeProperty] = "auto";
1555
+ } else if (isFlex) {
1556
+ // Parent cross axis → stretch to the parent's cross size.
1557
+ patch.alignSelf = "stretch";
1558
+ patch[sizeProperty] = "auto";
1559
+ } else if (isGrid) {
1560
+ patch[isHorizontal ? "justifySelf" : "alignSelf"] = "stretch";
1561
+ patch[sizeProperty] = "auto";
1562
+ } else {
1563
+ // Child of a non-flex (block) parent → fill width with 100%.
1564
+ patch[sizeProperty] = "100%";
1350
1565
  }
1351
1566
  }
1352
1567
 
@@ -1370,9 +1585,27 @@ function SelectionHeader({ element }: { element: ElementInfo | null }) {
1370
1585
 
1371
1586
  return (
1372
1587
  <div className="flex min-h-8 shrink-0 items-center justify-between gap-2 border-b border-border/90 px-3">
1373
- <div className="flex min-w-0 items-center gap-1.5 text-left text-[13px] font-semibold text-foreground">
1588
+ {/* M3 · Node-type label + rename/type dropdown affordance (▾) */}
1589
+ <button
1590
+ type="button"
1591
+ className="flex min-w-0 items-center gap-1.5 bg-transparent text-left text-[13px] font-semibold text-foreground"
1592
+ >
1374
1593
  <TypeIcon className="size-3.5 shrink-0 text-muted-foreground" />
1375
1594
  <span className="truncate">{title}</span>
1595
+ <IconChevronDown className="size-2.5 shrink-0 text-muted-foreground" />
1596
+ </button>
1597
+ {/* M3 · Right-aligned quick actions: create-component + dev inspect (</>) */}
1598
+ <div className="flex shrink-0 items-center gap-0.5">
1599
+ <SectionIconButton
1600
+ label={"Create component" /* i18n-ignore Figma inspector action */}
1601
+ >
1602
+ <IconComponents className="size-3.5" />
1603
+ </SectionIconButton>
1604
+ <SectionIconButton
1605
+ label={"Inspect code" /* i18n-ignore Figma inspector action */}
1606
+ >
1607
+ <IconCode className="size-3.5" />
1608
+ </SectionIconButton>
1376
1609
  </div>
1377
1610
  </div>
1378
1611
  );
@@ -1381,39 +1614,42 @@ function SelectionHeader({ element }: { element: ElementInfo | null }) {
1381
1614
  function InspectorTabsHeader({
1382
1615
  activeTab,
1383
1616
  onActiveTabChange,
1617
+ trailing,
1384
1618
  }: {
1385
1619
  activeTab: InspectorTab;
1386
1620
  onActiveTabChange: (tab: InspectorTab) => void;
1621
+ trailing?: ReactNode;
1387
1622
  }) {
1388
1623
  const t = useT();
1389
1624
 
1390
1625
  return (
1391
- <div className="flex min-h-8 shrink-0 items-center justify-between border-b border-border/90 px-3">
1626
+ <div className="flex min-h-8 shrink-0 items-center justify-between gap-1 border-b border-border/90 px-2">
1392
1627
  <Tabs
1393
1628
  value={activeTab}
1394
1629
  onValueChange={(value) => onActiveTabChange(value as InspectorTab)}
1395
1630
  >
1396
- <TabsList className="h-7 justify-start gap-1 rounded-none bg-transparent p-0">
1631
+ <TabsList className="h-7 justify-start gap-0.5 rounded-none bg-transparent p-0">
1397
1632
  <TabsTrigger
1398
1633
  value="design"
1399
- className="h-6 rounded-md px-2.5 text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
1634
+ className="h-6 rounded-md px-1.5 text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
1400
1635
  >
1401
1636
  {"Design" /* i18n-ignore Figma inspector tab */}
1402
1637
  </TabsTrigger>
1403
1638
  <TabsTrigger
1404
1639
  value="tweaks"
1405
- className="h-6 rounded-md px-2.5 text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
1640
+ className="h-6 rounded-md px-1.5 text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
1406
1641
  >
1407
1642
  {t("designEditor.tweaks")}
1408
1643
  </TabsTrigger>
1409
1644
  <TabsTrigger
1410
1645
  value="extensions"
1411
- className="h-6 rounded-md px-2.5 text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
1646
+ className="h-6 rounded-md px-1.5 text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
1412
1647
  >
1413
1648
  {t("designEditor.extensions")}
1414
1649
  </TabsTrigger>
1415
1650
  </TabsList>
1416
1651
  </Tabs>
1652
+ {trailing ? <div className="shrink-0">{trailing}</div> : null}
1417
1653
  </div>
1418
1654
  );
1419
1655
  }
@@ -1423,11 +1659,13 @@ function SectionIconButton({
1423
1659
  onClick,
1424
1660
  children,
1425
1661
  disabled = false,
1662
+ className,
1426
1663
  }: {
1427
1664
  label: string;
1428
1665
  onClick?: () => void;
1429
1666
  children: ReactNode;
1430
1667
  disabled?: boolean;
1668
+ className?: string;
1431
1669
  }) {
1432
1670
  return (
1433
1671
  <Tooltip>
@@ -1436,7 +1674,10 @@ function SectionIconButton({
1436
1674
  type="button"
1437
1675
  variant="ghost"
1438
1676
  size="icon"
1439
- className="size-6 cursor-pointer rounded-md text-muted-foreground hover:text-foreground disabled:cursor-not-allowed"
1677
+ className={cn(
1678
+ "size-6 cursor-pointer rounded-md text-muted-foreground hover:text-foreground disabled:cursor-not-allowed",
1679
+ className,
1680
+ )}
1440
1681
  disabled={disabled}
1441
1682
  onClick={onClick}
1442
1683
  aria-label={label}
@@ -1449,9 +1690,13 @@ function SectionIconButton({
1449
1690
  );
1450
1691
  }
1451
1692
 
1452
- function InspectorIconButton({
1693
+ /**
1694
+ * Section-header toggle icon (Figma's right-aligned section actions, e.g. the
1695
+ * auto-layout ⊞ toggle). Highlights with the accent color when active.
1696
+ */
1697
+ function SectionIconToggle({
1453
1698
  label,
1454
- active,
1699
+ active = false,
1455
1700
  onClick,
1456
1701
  children,
1457
1702
  }: {
@@ -1467,13 +1712,14 @@ function InspectorIconButton({
1467
1712
  type="button"
1468
1713
  variant="ghost"
1469
1714
  size="icon"
1715
+ aria-label={label}
1716
+ aria-pressed={active}
1717
+ onClick={onClick}
1470
1718
  className={cn(
1471
- "h-6 min-w-6 cursor-pointer rounded-none border-r border-border/50 text-muted-foreground first:rounded-l-md last:rounded-r-md last:border-r-0 hover:bg-[var(--design-editor-panel-raised-bg)] hover:text-foreground disabled:cursor-not-allowed",
1719
+ "size-6 cursor-pointer rounded-md text-muted-foreground hover:text-foreground",
1472
1720
  active &&
1473
- "bg-[var(--design-editor-panel-bg)] text-[var(--design-editor-accent-color)] shadow-[inset_0_0_0_1px_var(--design-editor-control-border)]",
1721
+ "bg-[var(--design-editor-accent-color)]/15 text-[var(--design-editor-accent-color)] hover:text-[var(--design-editor-accent-color)]",
1474
1722
  )}
1475
- onClick={onClick}
1476
- aria-label={label}
1477
1723
  >
1478
1724
  {children}
1479
1725
  </Button>
@@ -1483,58 +1729,33 @@ function InspectorIconButton({
1483
1729
  );
1484
1730
  }
1485
1731
 
1486
- function InspectorSegment({ children }: { children: ReactNode }) {
1487
- return (
1488
- <div className="flex min-w-0 overflow-hidden rounded-md bg-[var(--design-editor-control-bg)]">
1489
- {children}
1490
- </div>
1491
- );
1492
- }
1493
-
1494
- function sizingModeLabel(mode: AutoLayoutSizing): string {
1495
- if (mode === "hug") return "Hug"; // i18n-ignore Figma inspector sizing mode
1496
- if (mode === "fill") return "Fill"; // i18n-ignore Figma inspector sizing mode
1497
- return "Fixed"; // i18n-ignore Figma inspector sizing mode
1498
- }
1499
-
1500
- function SizingModeButton({
1501
- axis,
1502
- value,
1503
- resolvedSize,
1504
- options,
1505
- onChange,
1732
+ function InspectorIconButton({
1733
+ label,
1734
+ active,
1735
+ onClick,
1736
+ children,
1506
1737
  }: {
1507
- axis: "W" | "H";
1508
- value: AutoLayoutSizing;
1509
- resolvedSize: number;
1510
- options: AutoLayoutSizing[];
1511
- onChange: (value: AutoLayoutSizing) => void;
1738
+ label: string;
1739
+ active?: boolean;
1740
+ onClick?: () => void;
1741
+ children: ReactNode;
1512
1742
  }) {
1513
- const validOptions = options.includes(value) ? options : [...options, value];
1514
- const currentIndex = validOptions.indexOf(value);
1515
- const nextValue =
1516
- validOptions[(currentIndex + 1) % validOptions.length] ?? "fixed";
1517
- const label = `${axis} ${sizingModeLabel(value)}`;
1518
-
1519
1743
  return (
1520
1744
  <Tooltip>
1521
1745
  <TooltipTrigger asChild>
1522
1746
  <Button
1523
1747
  type="button"
1524
1748
  variant="ghost"
1749
+ size="icon"
1750
+ className={cn(
1751
+ "h-6 min-w-6 cursor-pointer rounded-none border-r border-border/50 text-muted-foreground first:rounded-l-md last:rounded-r-md last:border-r-0 hover:bg-[var(--design-editor-panel-raised-bg)] hover:text-foreground disabled:cursor-not-allowed",
1752
+ active &&
1753
+ "bg-[var(--design-editor-panel-bg)] text-[var(--design-editor-accent-color)] shadow-[inset_0_0_0_1px_var(--design-editor-control-border)]",
1754
+ )}
1755
+ onClick={onClick}
1525
1756
  aria-label={label}
1526
- onClick={() => onChange(nextValue)}
1527
- className="h-6 justify-start rounded-md bg-[var(--design-editor-control-bg)] px-0 text-[11px] font-normal text-foreground hover:bg-[var(--design-editor-panel-raised-bg)]"
1528
1757
  >
1529
- <span className="flex h-full w-6 shrink-0 items-center justify-center rounded-l-md border-r border-border/60 text-muted-foreground">
1530
- {axis}
1531
- </span>
1532
- <span className="min-w-0 flex-1 truncate px-1 text-left tabular-nums text-muted-foreground">
1533
- {Math.round(resolvedSize)}
1534
- </span>
1535
- <span className="flex h-full min-w-8 items-center justify-center border-l border-border/60 px-1.5 font-medium text-foreground">
1536
- {sizingModeLabel(value)}
1537
- </span>
1758
+ {children}
1538
1759
  </Button>
1539
1760
  </TooltipTrigger>
1540
1761
  <TooltipContent>{label}</TooltipContent>
@@ -1542,6 +1763,14 @@ function SizingModeButton({
1542
1763
  );
1543
1764
  }
1544
1765
 
1766
+ function InspectorSegment({ children }: { children: ReactNode }) {
1767
+ return (
1768
+ <div className="flex min-w-0 overflow-hidden rounded-md bg-[var(--design-editor-control-bg)]">
1769
+ {children}
1770
+ </div>
1771
+ );
1772
+ }
1773
+
1545
1774
  function CornerRadiusControl({
1546
1775
  styles,
1547
1776
  onStyleChange,
@@ -1716,7 +1945,7 @@ function StrokeLayerControl({
1716
1945
  return (
1717
1946
  <div className="space-y-1.5">
1718
1947
  {/* Figma stroke row: [swatch+hex trigger (flex-1)] [eye] [remove] */}
1719
- <div className="flex items-center gap-1.5">
1948
+ <div className="group flex items-center gap-1.5">
1720
1949
  <div className="min-w-0 flex-1">
1721
1950
  <ColorInput
1722
1951
  label=""
@@ -1725,6 +1954,7 @@ function StrokeLayerControl({
1725
1954
  />
1726
1955
  </div>
1727
1956
  <SectionIconButton
1957
+ className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
1728
1958
  label={
1729
1959
  visible
1730
1960
  ? t("editPanel.labels.hideLayer")
@@ -1749,6 +1979,7 @@ function StrokeLayerControl({
1749
1979
  )}
1750
1980
  </SectionIconButton>
1751
1981
  <SectionIconButton
1982
+ className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
1752
1983
  label={t("editPanel.labels.removeLayer")}
1753
1984
  onClick={onRemove}
1754
1985
  >
@@ -1903,7 +2134,7 @@ function ShadowEffectRow({
1903
2134
  return (
1904
2135
  <Popover>
1905
2136
  {/* Figma effect row: [swatch+label+x,y,blur trigger (flex-1)] [remove] */}
1906
- <div className="flex items-center gap-1.5">
2137
+ <div className="group flex items-center gap-1.5">
1907
2138
  <PopoverTrigger asChild>
1908
2139
  <button
1909
2140
  type="button"
@@ -1925,6 +2156,7 @@ function ShadowEffectRow({
1925
2156
  </button>
1926
2157
  </PopoverTrigger>
1927
2158
  <SectionIconButton
2159
+ className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
1928
2160
  label={t("editPanel.labels.removeLayer")}
1929
2161
  onClick={onRemove}
1930
2162
  >
@@ -2072,8 +2304,97 @@ function TypographyProperties({
2072
2304
  }));
2073
2305
  const textAlign = styles.textAlign || "left";
2074
2306
 
2307
+ // M1 · Text resizing mode (auto-width / auto-height / fixed). Figma's text
2308
+ // nodes always expose this segment. Infer the current mode from the live CSS:
2309
+ // auto-width hugs both axes (width:auto + no wrapping), auto-height hugs the
2310
+ // height only (fixed width, content wraps), fixed pins both width and height.
2311
+ const widthIsAuto =
2312
+ !styles.width || styles.width === "auto" || styles.width === "max-content";
2313
+ const heightIsAuto = !styles.height || styles.height === "auto";
2314
+ const noWrap = styles.whiteSpace === "nowrap";
2315
+ const resizeMode: "auto-width" | "auto-height" | "fixed" =
2316
+ widthIsAuto && noWrap
2317
+ ? "auto-width"
2318
+ : !heightIsAuto && !widthIsAuto
2319
+ ? "fixed"
2320
+ : "auto-height";
2321
+ const currentWidth = styles.width && !widthIsAuto ? styles.width : "200px";
2322
+ const currentHeight = styles.height && !heightIsAuto ? styles.height : "48px";
2323
+ const setResizeMode = (mode: "auto-width" | "auto-height" | "fixed") => {
2324
+ if (mode === "auto-width") {
2325
+ onStyleChange("width", "auto");
2326
+ onStyleChange("height", "auto");
2327
+ onStyleChange("whiteSpace", "nowrap");
2328
+ } else if (mode === "auto-height") {
2329
+ onStyleChange("width", currentWidth);
2330
+ onStyleChange("height", "auto");
2331
+ onStyleChange("whiteSpace", "normal");
2332
+ } else {
2333
+ onStyleChange("width", currentWidth);
2334
+ onStyleChange("height", currentHeight);
2335
+ onStyleChange("whiteSpace", "normal");
2336
+ }
2337
+ };
2338
+
2339
+ // M2 · Vertical text alignment (top / middle / bottom). For auto-layout text
2340
+ // containers (display:flex) Figma maps this to `justifyContent`; for normal
2341
+ // text we fall back to `verticalAlign`, which is what an inline/grid text box
2342
+ // honors. Read whichever the element currently expresses.
2343
+ const display = (styles.display || "").toLowerCase();
2344
+ const isFlexText = display.includes("flex");
2345
+ const verticalAlign = isFlexText
2346
+ ? styles.justifyContent === "center"
2347
+ ? "middle"
2348
+ : styles.justifyContent === "flex-end"
2349
+ ? "bottom"
2350
+ : "top"
2351
+ : styles.verticalAlign === "middle"
2352
+ ? "middle"
2353
+ : styles.verticalAlign === "bottom"
2354
+ ? "bottom"
2355
+ : "top";
2356
+ const setVerticalAlign = (mode: "top" | "middle" | "bottom") => {
2357
+ if (isFlexText) {
2358
+ onStyleChange(
2359
+ "justifyContent",
2360
+ mode === "middle"
2361
+ ? "center"
2362
+ : mode === "bottom"
2363
+ ? "flex-end"
2364
+ : "flex-start",
2365
+ );
2366
+ } else {
2367
+ onStyleChange("verticalAlign", mode);
2368
+ }
2369
+ };
2370
+
2075
2371
  return (
2076
2372
  <PanelSection title={t("editPanel.sections.typography")}>
2373
+ {/* Row 0: text resizing (auto-width / auto-height / fixed) */}
2374
+ <InspectorSegment>
2375
+ <InspectorIconButton
2376
+ label={"Auto width" /* i18n-ignore Figma text-resize mode */}
2377
+ active={resizeMode === "auto-width"}
2378
+ onClick={() => setResizeMode("auto-width")}
2379
+ >
2380
+ <IconArrowAutofitWidth className="size-3.5" />
2381
+ </InspectorIconButton>
2382
+ <InspectorIconButton
2383
+ label={"Auto height" /* i18n-ignore Figma text-resize mode */}
2384
+ active={resizeMode === "auto-height"}
2385
+ onClick={() => setResizeMode("auto-height")}
2386
+ >
2387
+ <IconArrowAutofitHeight className="size-3.5" />
2388
+ </InspectorIconButton>
2389
+ <InspectorIconButton
2390
+ label={"Fixed size" /* i18n-ignore Figma text-resize mode */}
2391
+ active={resizeMode === "fixed"}
2392
+ onClick={() => setResizeMode("fixed")}
2393
+ >
2394
+ <IconSquare className="size-3.5" />
2395
+ </InspectorIconButton>
2396
+ </InspectorSegment>
2397
+
2077
2398
  {/* Row 1: font family full-width */}
2078
2399
  <Select
2079
2400
  value={styles.fontFamily || "sans-serif"}
@@ -2166,37 +2487,62 @@ function TypographyProperties({
2166
2487
  />
2167
2488
  </div>
2168
2489
 
2169
- {/* Row 4: text alignment */}
2170
- <InspectorSegment>
2171
- <InspectorIconButton
2172
- label={t("editPanel.textAligns.left")}
2173
- active={textAlign === "left" || textAlign === "start"}
2174
- onClick={() => onStyleChange("textAlign", "left")}
2175
- >
2176
- <IconAlignLeft className="size-3.5" />
2177
- </InspectorIconButton>
2178
- <InspectorIconButton
2179
- label={t("editPanel.textAligns.center")}
2180
- active={textAlign === "center"}
2181
- onClick={() => onStyleChange("textAlign", "center")}
2182
- >
2183
- <IconAlignCenter className="size-3.5" />
2184
- </InspectorIconButton>
2185
- <InspectorIconButton
2186
- label={t("editPanel.textAligns.right")}
2187
- active={textAlign === "right" || textAlign === "end"}
2188
- onClick={() => onStyleChange("textAlign", "right")}
2189
- >
2190
- <IconAlignRight className="size-3.5" />
2191
- </InspectorIconButton>
2192
- <InspectorIconButton
2193
- label={t("editPanel.textAligns.justify")}
2194
- active={textAlign === "justify"}
2195
- onClick={() => onStyleChange("textAlign", "justify")}
2196
- >
2197
- <IconAlignJustified className="size-3.5" />
2198
- </InspectorIconButton>
2199
- </InspectorSegment>
2490
+ {/* Row 4: horizontal + vertical text alignment */}
2491
+ <div className="grid grid-cols-2 gap-1.5">
2492
+ <InspectorSegment>
2493
+ <InspectorIconButton
2494
+ label={t("editPanel.textAligns.left")}
2495
+ active={textAlign === "left" || textAlign === "start"}
2496
+ onClick={() => onStyleChange("textAlign", "left")}
2497
+ >
2498
+ <IconAlignLeft className="size-3.5" />
2499
+ </InspectorIconButton>
2500
+ <InspectorIconButton
2501
+ label={t("editPanel.textAligns.center")}
2502
+ active={textAlign === "center"}
2503
+ onClick={() => onStyleChange("textAlign", "center")}
2504
+ >
2505
+ <IconAlignCenter className="size-3.5" />
2506
+ </InspectorIconButton>
2507
+ <InspectorIconButton
2508
+ label={t("editPanel.textAligns.right")}
2509
+ active={textAlign === "right" || textAlign === "end"}
2510
+ onClick={() => onStyleChange("textAlign", "right")}
2511
+ >
2512
+ <IconAlignRight className="size-3.5" />
2513
+ </InspectorIconButton>
2514
+ <InspectorIconButton
2515
+ label={t("editPanel.textAligns.justify")}
2516
+ active={textAlign === "justify"}
2517
+ onClick={() => onStyleChange("textAlign", "justify")}
2518
+ >
2519
+ <IconAlignJustified className="size-3.5" />
2520
+ </InspectorIconButton>
2521
+ </InspectorSegment>
2522
+ <InspectorSegment>
2523
+ <InspectorIconButton
2524
+ label={"Align top" /* i18n-ignore Figma vertical text align */}
2525
+ active={verticalAlign === "top"}
2526
+ onClick={() => setVerticalAlign("top")}
2527
+ >
2528
+ <IconLayoutAlignTop className="size-3.5" />
2529
+ </InspectorIconButton>
2530
+ <InspectorIconButton
2531
+ label={"Align middle" /* i18n-ignore Figma vertical text align */}
2532
+ active={verticalAlign === "middle"}
2533
+ onClick={() => setVerticalAlign("middle")}
2534
+ >
2535
+ <IconLayoutAlignMiddle className="size-3.5" />
2536
+ </InspectorIconButton>
2537
+ <InspectorIconButton
2538
+ label={"Align bottom" /* i18n-ignore Figma vertical text align */}
2539
+ active={verticalAlign === "bottom"}
2540
+ onClick={() => setVerticalAlign("bottom")}
2541
+ >
2542
+ <IconLayoutAlignBottom className="size-3.5" />
2543
+ </InspectorIconButton>
2544
+ </InspectorSegment>
2545
+ </div>
2200
2546
  </PanelSection>
2201
2547
  );
2202
2548
  }
@@ -2213,8 +2559,21 @@ function FlexContainerControls({
2213
2559
  }) {
2214
2560
  const t = useT();
2215
2561
  const styles = element.computedStyles;
2562
+ // The element's CURRENT layout flow as authored in code, read from its own
2563
+ // computed `display`: block/flow-root/grid/etc. = "normal flow",
2564
+ // flex/inline-flex = auto layout. We forward it so the AutoLayoutMatrix Flow
2565
+ // control can show the right state (normal vs horizontal/vertical/wrap)
2566
+ // instead of an empty "add" affordance.
2567
+ const display = (styles.display || "").toLowerCase();
2568
+ const isFlex = display.includes("flex");
2216
2569
  const flexDirection: AutoLayoutMatrixValue["direction"] =
2217
2570
  styles.flexDirection?.includes("column") ? "vertical" : "horizontal";
2571
+ // When the element is in normal flow (not flex yet), picking any flow option
2572
+ // must first turn it into a flex container; otherwise setting flex-direction
2573
+ // alone is a no-op against a block element.
2574
+ const ensureFlex = () => {
2575
+ if (!isFlex) onStyleChange("display", "flex");
2576
+ };
2218
2577
  const padding = {
2219
2578
  top: parseNumericValue(styles.paddingTop || "0"),
2220
2579
  right: parseNumericValue(styles.paddingRight || "0"),
@@ -2242,24 +2601,37 @@ function FlexContainerControls({
2242
2601
  horizontal: inferElementSizing(element, "horizontal"),
2243
2602
  vertical: inferElementSizing(element, "vertical"),
2244
2603
  },
2604
+ childMinMax: {
2605
+ horizontal: readElementMinMax(element, "horizontal"),
2606
+ vertical: readElementMinMax(element, "vertical"),
2607
+ },
2245
2608
  clipContent: styles.overflow === "hidden",
2246
2609
  resolvedSize: {
2247
2610
  horizontal: element.boundingRect.width,
2248
2611
  vertical: element.boundingRect.height,
2249
2612
  },
2613
+ // Forward the raw CSS display so the matrix can render the correct Flow
2614
+ // state (normal flow for block/grid, flex for flex/inline-flex). Added as an
2615
+ // optional contract field consumed by AutoLayoutMatrix; harmless when the
2616
+ // matrix ignores it.
2617
+ ...({ display } as Partial<AutoLayoutMatrixValue>),
2250
2618
  };
2251
2619
 
2252
2620
  return (
2253
2621
  <div className="space-y-2">
2254
2622
  <AutoLayoutMatrix
2255
2623
  value={autoLayoutValue}
2256
- onDirectionChange={(direction) =>
2624
+ onDirectionChange={(direction) => {
2625
+ ensureFlex();
2257
2626
  onStyleChange(
2258
2627
  "flexDirection",
2259
2628
  direction === "vertical" ? "column" : "row",
2260
- )
2261
- }
2262
- onWrapChange={(wrap) => onStyleChange("flexWrap", wrap)}
2629
+ );
2630
+ }}
2631
+ onWrapChange={(wrap) => {
2632
+ ensureFlex();
2633
+ onStyleChange("flexWrap", wrap);
2634
+ }}
2263
2635
  onAlignmentChange={(alignment) => {
2264
2636
  if (autoLayoutValue.direction === "vertical") {
2265
2637
  onStyleChange(
@@ -2317,6 +2689,9 @@ function FlexContainerControls({
2317
2689
  onStylesChange,
2318
2690
  );
2319
2691
  }}
2692
+ onChildMinMaxChange={(axis, kind, val) =>
2693
+ commitElementMinMax(axis, kind, val, onStyleChange)
2694
+ }
2320
2695
  />
2321
2696
  </div>
2322
2697
  );
@@ -2428,17 +2803,39 @@ function LayoutContextProperties({
2428
2803
  const flexChild = isParentFlex(element);
2429
2804
  const gridChild = isParentGrid(element);
2430
2805
  const availableSizing = availableSizingForElement(element);
2806
+ const isContainer = isContainerElement(element);
2807
+
2808
+ const childControls = (
2809
+ <>
2810
+ {flexChild ? (
2811
+ <div className="border-t border-border/70 pt-2">
2812
+ <FlexChildControls element={element} onStyleChange={onStyleChange} />
2813
+ </div>
2814
+ ) : null}
2815
+ {gridChild ? (
2816
+ <div className="border-t border-border/70 pt-2">
2817
+ <GridChildControls element={element} onStyleChange={onStyleChange} />
2818
+ </div>
2819
+ ) : null}
2820
+ </>
2821
+ );
2431
2822
 
2432
- if (!element.isFlexContainer) {
2823
+ // Leaf elements (text, img, svg, etc.) never get auto layout — show the plain
2824
+ // Figma W/H sizing block instead.
2825
+ if (!isContainer) {
2433
2826
  return (
2434
2827
  <PanelSection title={t("editPanel.sections.layout")}>
2435
- {/* Figma-style single-row-per-axis: [W | value | Fixed/Hug ▾] */}
2436
- <div className="grid grid-cols-2 gap-1.5">
2437
- <SizingModeButton
2828
+ {/* Figma-style single-row-per-axis: [W | value | Fixed/Hug/Fill ▾] with
2829
+ the full sizing menu (modes + min/max + variable) per axis. */}
2830
+ <div className="grid grid-cols-2 items-start gap-1.5">
2831
+ <SizingField
2438
2832
  axis="W"
2833
+ sizingAxis="horizontal"
2439
2834
  value={inferElementSizing(element, "horizontal")}
2440
2835
  resolvedSize={element.boundingRect.width}
2441
- options={availableSizing.horizontal ?? ["hug", "fixed"]}
2836
+ minMax={readElementMinMax(element, "horizontal")}
2837
+ options={availableSizing.horizontal ?? ["fixed"]}
2838
+ disabled={false}
2442
2839
  onChange={(mode) =>
2443
2840
  commitElementSizing(
2444
2841
  element,
@@ -2448,12 +2845,18 @@ function LayoutContextProperties({
2448
2845
  onStylesChange,
2449
2846
  )
2450
2847
  }
2848
+ onMinMaxChange={(axis, kind, val) =>
2849
+ commitElementMinMax(axis, kind, val, onStyleChange)
2850
+ }
2451
2851
  />
2452
- <SizingModeButton
2852
+ <SizingField
2453
2853
  axis="H"
2854
+ sizingAxis="vertical"
2454
2855
  value={inferElementSizing(element, "vertical")}
2455
2856
  resolvedSize={element.boundingRect.height}
2456
- options={availableSizing.vertical ?? ["hug", "fixed"]}
2857
+ minMax={readElementMinMax(element, "vertical")}
2858
+ options={availableSizing.vertical ?? ["fixed"]}
2859
+ disabled={false}
2457
2860
  onChange={(mode) =>
2458
2861
  commitElementSizing(
2459
2862
  element,
@@ -2463,48 +2866,154 @@ function LayoutContextProperties({
2463
2866
  onStylesChange,
2464
2867
  )
2465
2868
  }
2869
+ onMinMaxChange={(axis, kind, val) =>
2870
+ commitElementMinMax(axis, kind, val, onStyleChange)
2871
+ }
2466
2872
  />
2467
2873
  </div>
2468
- {flexChild ? (
2469
- <FlexChildControls element={element} onStyleChange={onStyleChange} />
2470
- ) : null}
2471
- {gridChild ? (
2472
- <GridChildControls element={element} onStyleChange={onStyleChange} />
2473
- ) : null}
2874
+ {childControls}
2474
2875
  </PanelSection>
2475
2876
  );
2476
2877
  }
2477
2878
 
2879
+ // Any container element ALREADY has a layout in code — normal flow (block) by
2880
+ // default, or flex when it uses flexbox. Figma never makes you "add" auto
2881
+ // layout for a frame, so we always render the full layout controls and let
2882
+ // the Flow control reflect/switch the element's current `display`. Choosing a
2883
+ // horizontal/vertical/wrap/grid flow applies `display:flex`; choosing the
2884
+ // normal-flow option resets to `display:block`.
2478
2885
  return (
2479
- <PanelSection
2480
- title={t("editPanel.sections.autoLayout")}
2481
- actions={
2482
- <span className="flex size-7 items-center justify-center rounded-md bg-[var(--design-editor-accent-color)]/15 text-[var(--design-editor-accent-color)]">
2483
- <IconLayoutGrid className="size-4" />
2484
- </span>
2485
- }
2486
- >
2886
+ <PanelSection title={t("editPanel.sections.autoLayout")}>
2487
2887
  <FlexContainerControls
2488
2888
  element={element}
2489
2889
  onStyleChange={onStyleChange}
2490
2890
  onStylesChange={onStylesChange}
2491
2891
  />
2892
+ {childControls}
2893
+ </PanelSection>
2894
+ );
2895
+ }
2492
2896
 
2493
- {flexChild ? (
2494
- <div className="border-t border-border/70 pt-2">
2495
- <FlexChildControls element={element} onStyleChange={onStyleChange} />
2496
- </div>
2497
- ) : null}
2897
+ /**
2898
+ * Figma "Layout grid" / layout-guide section. Shown for frame/container
2899
+ * elements. Renders an overlay column/row guide by applying a non-destructive
2900
+ * `backgroundImage` repeating gradient layer tagged so it can be toggled off
2901
+ * without disturbing real fills.
2902
+ */
2903
+ const LAYOUT_GUIDE_MARKER = "/* an-layout-guide */";
2498
2904
 
2499
- {gridChild ? (
2500
- <div className="border-t border-border/70 pt-2">
2501
- <GridChildControls element={element} onStyleChange={onStyleChange} />
2905
+ function hasLayoutGuide(styles: Record<string, string>): boolean {
2906
+ return Boolean(styles.backgroundImage?.includes("repeating-linear-gradient"));
2907
+ }
2908
+
2909
+ function LayoutGuideProperties({
2910
+ element,
2911
+ onStyleChange,
2912
+ }: {
2913
+ element: ElementInfo;
2914
+ onStyleChange: (property: string, value: string) => void;
2915
+ }) {
2916
+ const styles = element.computedStyles;
2917
+ const active = hasLayoutGuide(styles);
2918
+
2919
+ const addGuide = () => {
2920
+ // 12-column overlay guide — Figma's default columns layout grid.
2921
+ const guide =
2922
+ "repeating-linear-gradient(to right, color-mix(in srgb, var(--design-editor-accent-color) 22%, transparent) 0 1px, transparent 1px calc(100% / 12))";
2923
+ const existing = compactCssValue(styles.backgroundImage, "");
2924
+ onStyleChange(
2925
+ "backgroundImage",
2926
+ existing ? `${guide}, ${existing}` : guide,
2927
+ );
2928
+ };
2929
+
2930
+ const removeGuide = () => {
2931
+ const layers = splitCssLayers(styles.backgroundImage || "").filter(
2932
+ (layer) => !layer.includes("repeating-linear-gradient"),
2933
+ );
2934
+ onStyleChange(
2935
+ "backgroundImage",
2936
+ layers.length ? joinCssLayers(layers) : "none",
2937
+ );
2938
+ };
2939
+
2940
+ return (
2941
+ <PanelSection
2942
+ title={"Layout guide" /* i18n-ignore Figma inspector label */}
2943
+ defaultCollapsed
2944
+ actions={
2945
+ <SectionIconButton
2946
+ label={
2947
+ active
2948
+ ? "Remove layout guide" /* i18n-ignore Figma inspector action */
2949
+ : "Add layout guide" /* i18n-ignore Figma inspector action */
2950
+ }
2951
+ onClick={active ? removeGuide : addGuide}
2952
+ >
2953
+ {active ? (
2954
+ <IconMinus className="size-3.5" />
2955
+ ) : (
2956
+ <IconPlus className="size-3.5" />
2957
+ )}
2958
+ </SectionIconButton>
2959
+ }
2960
+ >
2961
+ {active ? (
2962
+ <div className="flex items-center gap-2 rounded-md bg-[var(--design-editor-control-bg)] px-2 py-1.5 text-[11px] text-muted-foreground">
2963
+ <IconLayoutGrid className="size-3.5 shrink-0" />
2964
+ <span className="min-w-0 flex-1 truncate text-foreground">
2965
+ {"Columns" /* i18n-ignore Figma inspector label */}
2966
+ </span>
2967
+ <span className="shrink-0 tabular-nums">12</span>
2502
2968
  </div>
2503
- ) : null}
2969
+ ) : (
2970
+ <p className="text-[11px] text-muted-foreground">
2971
+ {"No layout guides" /* i18n-ignore Figma inspector empty state */}
2972
+ </p>
2973
+ )}
2504
2974
  </PanelSection>
2505
2975
  );
2506
2976
  }
2507
2977
 
2978
+ /**
2979
+ * Togglable export preview thumbnail (Figma shows a small preview of the export
2980
+ * frame above the export rows). Renders a proportional placeholder reflecting
2981
+ * the selected element's aspect ratio, fill, radius and dimensions.
2982
+ */
2983
+ function ExportPreview({ element }: { element: ElementInfo | null }) {
2984
+ const rect = element?.boundingRect;
2985
+ const width = rect?.width ?? 0;
2986
+ const height = rect?.height ?? 0;
2987
+ const aspect = width > 0 && height > 0 ? width / height : 1;
2988
+ const styles = element?.computedStyles ?? {};
2989
+ const fill = cssColorOrFallback(
2990
+ styles.backgroundColor || styles.color,
2991
+ "var(--design-editor-control-bg)",
2992
+ );
2993
+ const radius = Math.min(8, cssLengthNumber(styles.borderRadius || "0"));
2994
+
2995
+ return (
2996
+ <div className="mt-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] p-3">
2997
+ <div
2998
+ className="mx-auto flex max-h-28 items-center justify-center"
2999
+ style={{
3000
+ aspectRatio: aspect,
3001
+ width: aspect >= 1 ? "100%" : "auto",
3002
+ height: aspect < 1 ? "7rem" : "auto",
3003
+ }}
3004
+ >
3005
+ <div
3006
+ className="size-full border border-[var(--design-editor-control-border)] shadow-sm"
3007
+ style={{ background: fill, borderRadius: radius }}
3008
+ />
3009
+ </div>
3010
+ <p className="mt-2 text-center text-[10px] tabular-nums text-muted-foreground">
3011
+ {Math.round(width)} × {Math.round(height)}
3012
+ </p>
3013
+ </div>
3014
+ );
3015
+ }
3016
+
2508
3017
  /** Position, size, and spacing properties */
2509
3018
  function PositionLayoutProperties({
2510
3019
  element,
@@ -2517,6 +3026,10 @@ function PositionLayoutProperties({
2517
3026
  const styles = element.computedStyles;
2518
3027
  const constrainedPosition =
2519
3028
  styles.position === "absolute" || styles.position === "fixed";
3029
+ // Reflect the active packing in the alignment segments (Figma highlights the
3030
+ // current alignment). For a flex container the main axis is justifyContent.
3031
+ const alignH = justifyToHorizontal(styles.justifyContent);
3032
+ const alignV = alignToVertical(styles.alignItems);
2520
3033
  const constraintsValue: ConstraintsValue = {
2521
3034
  horizontal:
2522
3035
  styles.left && styles.right
@@ -2603,7 +3116,23 @@ function PositionLayoutProperties({
2603
3116
  );
2604
3117
 
2605
3118
  return (
2606
- <PanelSection title={t("editPanel.sections.positionLayout")}>
3119
+ <PanelSection
3120
+ title={t("editPanel.sections.positionLayout")}
3121
+ actions={
3122
+ <SectionIconToggle
3123
+ label={"Absolute position" /* i18n-ignore Figma inspector action */}
3124
+ active={constrainedPosition}
3125
+ onClick={() =>
3126
+ onStyleChange(
3127
+ "position",
3128
+ constrainedPosition ? "relative" : "absolute",
3129
+ )
3130
+ }
3131
+ >
3132
+ <IconLayoutDistributeHorizontal className="size-3.5" />
3133
+ </SectionIconToggle>
3134
+ }
3135
+ >
2607
3136
  <div className="space-y-1.5">
2608
3137
  <SubsectionLabel>
2609
3138
  {"Alignment" /* i18n-ignore Figma inspector label */}
@@ -2612,41 +3141,47 @@ function PositionLayoutProperties({
2612
3141
  <InspectorSegment>
2613
3142
  <InspectorIconButton
2614
3143
  label={t("editPanel.textAligns.left")}
3144
+ active={alignH === "left"}
2615
3145
  onClick={() => onStyleChange("justifyContent", "flex-start")}
2616
3146
  >
2617
- <IconLayoutAlignLeft className="size-4" />
3147
+ <IconLayoutAlignLeft className="size-3.5" />
2618
3148
  </InspectorIconButton>
2619
3149
  <InspectorIconButton
2620
3150
  label={t("editPanel.textAligns.center")}
3151
+ active={alignH === "center"}
2621
3152
  onClick={() => onStyleChange("justifyContent", "center")}
2622
3153
  >
2623
- <IconLayoutAlignCenter className="size-4" />
3154
+ <IconLayoutAlignCenter className="size-3.5" />
2624
3155
  </InspectorIconButton>
2625
3156
  <InspectorIconButton
2626
3157
  label={t("editPanel.textAligns.right")}
3158
+ active={alignH === "right"}
2627
3159
  onClick={() => onStyleChange("justifyContent", "flex-end")}
2628
3160
  >
2629
- <IconLayoutAlignRight className="size-4" />
3161
+ <IconLayoutAlignRight className="size-3.5" />
2630
3162
  </InspectorIconButton>
2631
3163
  </InspectorSegment>
2632
3164
  <InspectorSegment>
2633
3165
  <InspectorIconButton
2634
3166
  label={t("editPanel.alignSelfOptions.start")}
3167
+ active={alignV === "top"}
2635
3168
  onClick={() => onStyleChange("alignItems", "flex-start")}
2636
3169
  >
2637
- <IconLayoutAlignTop className="size-4" />
3170
+ <IconLayoutAlignTop className="size-3.5" />
2638
3171
  </InspectorIconButton>
2639
3172
  <InspectorIconButton
2640
3173
  label={t("editPanel.alignSelfOptions.center")}
3174
+ active={alignV === "middle"}
2641
3175
  onClick={() => onStyleChange("alignItems", "center")}
2642
3176
  >
2643
- <IconLayoutAlignMiddle className="size-4" />
3177
+ <IconLayoutAlignMiddle className="size-3.5" />
2644
3178
  </InspectorIconButton>
2645
3179
  <InspectorIconButton
2646
3180
  label={t("editPanel.alignSelfOptions.end")}
3181
+ active={alignV === "bottom"}
2647
3182
  onClick={() => onStyleChange("alignItems", "flex-end")}
2648
3183
  >
2649
- <IconLayoutAlignBottom className="size-4" />
3184
+ <IconLayoutAlignBottom className="size-3.5" />
2650
3185
  </InspectorIconButton>
2651
3186
  </InspectorSegment>
2652
3187
  </div>
@@ -2738,51 +3273,101 @@ function FillProperties({
2738
3273
  const hasBackgroundLayer = !isTextElement && backgroundLayers.length > 0;
2739
3274
  const hasVisibleFill =
2740
3275
  isTextElement || colorHasVisibleAlpha(fillValue) || hasBackgroundLayer;
2741
- const fallbackValue = isTextElement
2742
- ? cssColorOrFallback(styles.color, "#000000")
2743
- : cssColorOrFallback(styles.backgroundColor, "#ffffff");
3276
+
3277
+ // Non-destructive fill hide: stash the color before hiding so toggling
3278
+ // visible again restores the exact original value (Figma never loses color).
3279
+ // Keyed by `${element.id ?? tagName}:${fillProperty}` so separate elements
3280
+ // don't share stash slots.
3281
+ const [hiddenFillStash, setHiddenFillStash] = useState<
3282
+ Record<string, string>
3283
+ >({});
3284
+ const stashKey = `${element.id ?? element.tagName}:${fillProperty}`;
3285
+ const isHidden = fillValue === "transparent" || fillValue === "";
3286
+ const handleFillVisibilityToggle = () => {
3287
+ if (isHidden) {
3288
+ // Restore the stashed color, or fall back to a sensible default.
3289
+ const restored =
3290
+ hiddenFillStash[stashKey] ?? (isTextElement ? "#000000" : "#ffffff");
3291
+ onStyleChange(fillProperty, restored);
3292
+ setHiddenFillStash((prev) => {
3293
+ const next = { ...prev };
3294
+ delete next[stashKey];
3295
+ return next;
3296
+ });
3297
+ } else {
3298
+ // Stash the current color before going transparent.
3299
+ setHiddenFillStash((prev) => ({ ...prev, [stashKey]: fillValue }));
3300
+ onStyleChange(fillProperty, "transparent");
3301
+ }
3302
+ };
3303
+
3304
+ // Document colors: unique hex strings from all CSS color properties on the
3305
+ // selected element, collected via the existing selectionColorValues helper.
3306
+ const docColorHexes = selectionColorValues(element)
3307
+ .map((c) => {
3308
+ const parsed = parseCssColor(c.value);
3309
+ return parsed ? rgbaToHex(parsed) : null;
3310
+ })
3311
+ .filter((h): h is string => Boolean(h));
3312
+ // Deduplicate (selectionColorValues already dedupes by raw CSS value, but
3313
+ // hex normalisation may collapse additional entries e.g. rgb vs #hex).
3314
+ const seenHex = new Set<string>();
3315
+ const documentColors = docColorHexes.filter((h) => {
3316
+ const key = h.toUpperCase();
3317
+ if (seenHex.has(key)) return false;
3318
+ seenHex.add(key);
3319
+ return true;
3320
+ });
2744
3321
 
2745
3322
  return (
2746
3323
  <PanelSection
2747
3324
  title={t("editPanel.sections.fill")}
2748
3325
  actions={
2749
- <SectionIconButton
2750
- label={t("editPanel.labels.addLayer")}
2751
- onClick={() => {
2752
- if (isTextElement) {
2753
- onStyleChange(
2754
- "color",
2755
- cssColorOrFallback(styles.color, "#000000"),
3326
+ <>
3327
+ {/* Figma color-styles affordance (grid icon) to the left of "+". */}
3328
+ <SectionIconButton
3329
+ label={"Styles" /* i18n-ignore Figma inspector action */}
3330
+ >
3331
+ <IconLayoutGrid className="size-3.5" />
3332
+ </SectionIconButton>
3333
+ <SectionIconButton
3334
+ label={t("editPanel.labels.addLayer")}
3335
+ onClick={() => {
3336
+ if (isTextElement) {
3337
+ onStyleChange(
3338
+ "color",
3339
+ cssColorOrFallback(styles.color, "#000000"),
3340
+ );
3341
+ return;
3342
+ }
3343
+ if (!colorHasVisibleAlpha(styles.backgroundColor)) {
3344
+ onStyleChange(
3345
+ "backgroundColor",
3346
+ cssColorOrFallback(styles.backgroundColor, "#ffffff"),
3347
+ );
3348
+ return;
3349
+ }
3350
+ const current = compactCssValue(styles.backgroundImage, "");
3351
+ const nextLayer = defaultGradientLayer(
3352
+ "linear",
3353
+ styles.backgroundColor || "#ffffff",
2756
3354
  );
2757
- return;
2758
- }
2759
- if (!colorHasVisibleAlpha(styles.backgroundColor)) {
2760
3355
  onStyleChange(
2761
- "backgroundColor",
2762
- cssColorOrFallback(styles.backgroundColor, "#ffffff"),
3356
+ "backgroundImage",
3357
+ current ? `${nextLayer}, ${current}` : nextLayer,
2763
3358
  );
2764
- return;
2765
- }
2766
- const current = compactCssValue(styles.backgroundImage, "");
2767
- const nextLayer = defaultGradientLayer(
2768
- "linear",
2769
- styles.backgroundColor || "#ffffff",
2770
- );
2771
- onStyleChange(
2772
- "backgroundImage",
2773
- current ? `${nextLayer}, ${current}` : nextLayer,
2774
- );
2775
- }}
2776
- >
2777
- <IconPlus className="size-3.5" />
2778
- </SectionIconButton>
3359
+ }}
3360
+ >
3361
+ <IconPlus className="size-3.5" />
3362
+ </SectionIconButton>
3363
+ </>
2779
3364
  }
2780
3365
  >
2781
3366
  {hasVisibleFill ? (
2782
3367
  <div className="space-y-1.5">
2783
3368
  {isTextElement || colorHasVisibleAlpha(fillValue) ? (
2784
3369
  /* Figma row: [swatch+hex trigger (flex-1)] [eye] [remove] */
2785
- <div className="flex items-center gap-1.5">
3370
+ <div className="group flex items-center gap-1.5">
2786
3371
  <div className="min-w-0 flex-1">
2787
3372
  <ColorInput
2788
3373
  label=""
@@ -2799,28 +3384,26 @@ function FillProperties({
2799
3384
  ? undefined
2800
3385
  : (v) => onStyleChange("backgroundBlendMode", v)
2801
3386
  }
3387
+ documentColors={documentColors}
2802
3388
  />
2803
3389
  </div>
2804
3390
  <SectionIconButton
3391
+ className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
2805
3392
  label={
2806
- fillValue === "transparent"
3393
+ isHidden
2807
3394
  ? t("editPanel.labels.showLayer")
2808
3395
  : t("editPanel.labels.hideLayer")
2809
3396
  }
2810
- onClick={() =>
2811
- onStyleChange(
2812
- fillProperty,
2813
- fillValue === "transparent" ? fallbackValue : "transparent",
2814
- )
2815
- }
3397
+ onClick={handleFillVisibilityToggle}
2816
3398
  >
2817
- {fillValue === "transparent" ? (
3399
+ {isHidden ? (
2818
3400
  <IconEyeOff className="size-3.5" />
2819
3401
  ) : (
2820
3402
  <IconEye className="size-3.5" />
2821
3403
  )}
2822
3404
  </SectionIconButton>
2823
3405
  <SectionIconButton
3406
+ className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
2824
3407
  label={t("editPanel.labels.removeLayer")}
2825
3408
  onClick={() => {
2826
3409
  if (isTextElement) {
@@ -2872,7 +3455,7 @@ function FillProperties({
2872
3455
  /* Figma row: [swatch+label+opacity% trigger (flex-1)] [eye] [remove] */
2873
3456
  <div
2874
3457
  key={`${layer}-${index}`}
2875
- className="flex items-center gap-1.5"
3458
+ className="group flex items-center gap-1.5"
2876
3459
  >
2877
3460
  <Popover>
2878
3461
  <PopoverTrigger asChild>
@@ -2899,7 +3482,8 @@ function FillProperties({
2899
3482
  className="w-80 p-0"
2900
3483
  >
2901
3484
  <FigmaColorPicker
2902
- value={gradient?.stops[0]?.color ?? "#000000"}
3485
+ value={gradient?.stops[0]?.color ?? layer}
3486
+ onPaintValueChange={replaceLayer}
2903
3487
  onChange={(nextColor) => {
2904
3488
  if (!gradient) return;
2905
3489
  const firstStop = gradient.stops[0];
@@ -2934,6 +3518,7 @@ function FillProperties({
2934
3518
  </PopoverContent>
2935
3519
  </Popover>
2936
3520
  <SectionIconButton
3521
+ className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
2937
3522
  label={
2938
3523
  opacity <= 0
2939
3524
  ? t("editPanel.labels.showLayer")
@@ -2959,6 +3544,7 @@ function FillProperties({
2959
3544
  )}
2960
3545
  </SectionIconButton>
2961
3546
  <SectionIconButton
3547
+ className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
2962
3548
  label={t("editPanel.labels.removeLayer")}
2963
3549
  onClick={removeLayer}
2964
3550
  >
@@ -3101,8 +3687,50 @@ function AppearanceProperties({
3101
3687
  }) {
3102
3688
  const t = useT();
3103
3689
  const styles = element.computedStyles;
3690
+ const hidden =
3691
+ styles.visibility === "hidden" ||
3692
+ styles.display === "none" ||
3693
+ parseNumericValue(styles.opacity || "1") === 0;
3104
3694
  return (
3105
- <PanelSection title={t("root.commandAppearance")}>
3695
+ <PanelSection
3696
+ title={t("root.commandAppearance")}
3697
+ actions={
3698
+ <>
3699
+ {/* Opacity / blend-mode affordance — matches Figma's pill icon */}
3700
+ <SectionIconButton
3701
+ label={
3702
+ "Opacity & blend mode" /* i18n-ignore Figma inspector action */
3703
+ }
3704
+ >
3705
+ <IconSlice className="size-3.5" />
3706
+ </SectionIconButton>
3707
+ {/* Visibility toggle */}
3708
+ <SectionIconToggle
3709
+ label={
3710
+ hidden
3711
+ ? "Show" /* i18n-ignore Figma inspector action */
3712
+ : "Hide" /* i18n-ignore Figma inspector action */
3713
+ }
3714
+ active={hidden}
3715
+ onClick={() =>
3716
+ onStyleChange("visibility", hidden ? "visible" : "hidden")
3717
+ }
3718
+ >
3719
+ {hidden ? (
3720
+ <IconEyeOff className="size-3.5" />
3721
+ ) : (
3722
+ <IconEye className="size-3.5" />
3723
+ )}
3724
+ </SectionIconToggle>
3725
+ {/* Styles / fill library affordance — matches Figma's droplet icon */}
3726
+ <SectionIconButton
3727
+ label={"Styles" /* i18n-ignore Figma inspector action */}
3728
+ >
3729
+ <IconDroplet className="size-3.5" />
3730
+ </SectionIconButton>
3731
+ </>
3732
+ }
3733
+ >
3106
3734
  <div className="grid grid-cols-2 gap-2">
3107
3735
  <ScrubInput
3108
3736
  label={t("editPanel.labels.opacity")}
@@ -3159,28 +3787,65 @@ function EffectsProperties({
3159
3787
  const t = useT();
3160
3788
  const styles = element.computedStyles;
3161
3789
  const blurValue = readBlurFilter(styles.filter);
3790
+ // M5 · Background (backdrop) blur is a distinct Figma effect type, backed by
3791
+ // CSS `backdrop-filter: blur()` (vs layer blur's `filter: blur()`).
3792
+ const backdropBlurValue = readBlurFilter(
3793
+ styles.backdropFilter || styles.webkitBackdropFilter,
3794
+ );
3162
3795
  const shadowLayers = parseShadowLayers(styles.boxShadow);
3163
3796
  const setShadowLayers = (layers: ShadowLayer[]) => {
3164
3797
  const boxShadow = serializeShadowLayers(layers);
3165
3798
  if (onStylesChange) onStylesChange({ boxShadow });
3166
3799
  else onStyleChange("boxShadow", boxShadow);
3167
3800
  };
3801
+ const addDropShadow = () =>
3802
+ setShadowLayers([
3803
+ ...shadowLayers,
3804
+ defaultDropShadowLayer(shadowLayers.length),
3805
+ ]);
3806
+ const addLayerBlur = () => onStyleChange("filter", "blur(4px)");
3807
+ const addBackgroundBlur = () => onStyleChange("backdropFilter", "blur(8px)");
3168
3808
 
3169
3809
  return (
3170
3810
  <PanelSection
3171
3811
  title={t("editPanel.sections.effects")}
3172
3812
  actions={
3173
- <SectionIconButton
3174
- label={t("editPanel.labels.addLayer")}
3175
- onClick={() =>
3176
- setShadowLayers([
3177
- ...shadowLayers,
3178
- defaultDropShadowLayer(shadowLayers.length),
3179
- ])
3180
- }
3181
- >
3182
- <IconPlus className="size-3.5" />
3183
- </SectionIconButton>
3813
+ <DropdownMenu>
3814
+ <DropdownMenuTrigger asChild>
3815
+ <Button
3816
+ type="button"
3817
+ variant="ghost"
3818
+ size="icon"
3819
+ className="size-6 cursor-pointer rounded-md text-muted-foreground hover:text-foreground"
3820
+ aria-label={t("editPanel.labels.addLayer")}
3821
+ >
3822
+ <IconPlus className="size-3.5" />
3823
+ </Button>
3824
+ </DropdownMenuTrigger>
3825
+ <DropdownMenuContent align="end" className="min-w-44">
3826
+ <DropdownMenuItem
3827
+ className="gap-2 text-[11px]"
3828
+ onSelect={addDropShadow}
3829
+ >
3830
+ <IconShadow className="size-3.5" />
3831
+ {t("editPanel.labels.dropShadow")}
3832
+ </DropdownMenuItem>
3833
+ <DropdownMenuItem
3834
+ className="gap-2 text-[11px]"
3835
+ onSelect={addLayerBlur}
3836
+ >
3837
+ <IconBlur className="size-3.5" />
3838
+ {t("editPanel.labels.layerBlur")}
3839
+ </DropdownMenuItem>
3840
+ <DropdownMenuItem
3841
+ className="gap-2 text-[11px]"
3842
+ onSelect={addBackgroundBlur}
3843
+ >
3844
+ <IconBackground className="size-3.5" />
3845
+ {"Background blur" /* i18n-ignore Figma effect type */}
3846
+ </DropdownMenuItem>
3847
+ </DropdownMenuContent>
3848
+ </DropdownMenu>
3184
3849
  }
3185
3850
  >
3186
3851
  {shadowLayers.length ? (
@@ -3210,7 +3875,7 @@ function EffectsProperties({
3210
3875
  {blurValue > 0 ? (
3211
3876
  /* Figma effect row for layer blur: flat row matching shadow rows */
3212
3877
  <Popover>
3213
- <div className="flex items-center gap-1.5">
3878
+ <div className="group flex items-center gap-1.5">
3214
3879
  <PopoverTrigger asChild>
3215
3880
  <button
3216
3881
  type="button"
@@ -3225,6 +3890,7 @@ function EffectsProperties({
3225
3890
  </button>
3226
3891
  </PopoverTrigger>
3227
3892
  <SectionIconButton
3893
+ className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
3228
3894
  label={t("editPanel.labels.removeLayer")}
3229
3895
  onClick={() => onStyleChange("filter", "none")}
3230
3896
  disabled={!styles.filter || styles.filter === "none"}
@@ -3256,6 +3922,58 @@ function EffectsProperties({
3256
3922
  </PopoverContent>
3257
3923
  </Popover>
3258
3924
  ) : null}
3925
+ {backdropBlurValue > 0 ? (
3926
+ /* M5 · Background (backdrop) blur effect row — mirrors the layer-blur row */
3927
+ <Popover>
3928
+ <div className="group flex items-center gap-1.5">
3929
+ <PopoverTrigger asChild>
3930
+ <button
3931
+ type="button"
3932
+ className="flex h-6 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-left text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)]"
3933
+ >
3934
+ <span className="min-w-0 flex-1 truncate font-medium text-foreground">
3935
+ {"Background blur" /* i18n-ignore Figma effect type */}
3936
+ </span>
3937
+ <span className="shrink-0 tabular-nums text-muted-foreground">
3938
+ {Math.round(backdropBlurValue)}px
3939
+ </span>
3940
+ </button>
3941
+ </PopoverTrigger>
3942
+ <SectionIconButton
3943
+ className="opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
3944
+ label={t("editPanel.labels.removeLayer")}
3945
+ onClick={() => onStyleChange("backdropFilter", "none")}
3946
+ disabled={
3947
+ !styles.backdropFilter || styles.backdropFilter === "none"
3948
+ }
3949
+ >
3950
+ <IconMinus className="size-3.5" />
3951
+ </SectionIconButton>
3952
+ </div>
3953
+ <PopoverContent
3954
+ side="left"
3955
+ align="start"
3956
+ sideOffset={8}
3957
+ className="w-56 p-3"
3958
+ >
3959
+ <ScrubInput
3960
+ label={t("editPanel.labels.blur")}
3961
+ value={backdropBlurValue}
3962
+ onChange={(value) =>
3963
+ onStyleChange(
3964
+ "backdropFilter",
3965
+ `blur(${Math.max(0, Math.round(value))}px)`,
3966
+ )
3967
+ }
3968
+ unit="px"
3969
+ min={0}
3970
+ precision={1}
3971
+ labelClassName="w-16"
3972
+ inputClassName="h-6"
3973
+ />
3974
+ </PopoverContent>
3975
+ </Popover>
3976
+ ) : null}
3259
3977
  </PanelSection>
3260
3978
  );
3261
3979
  }
@@ -3289,6 +4007,13 @@ function selectionColorValues(element: ElementInfo): SelectionColorValue[] {
3289
4007
  });
3290
4008
  }
3291
4009
 
4010
+ /** Uppercase 6-char hex (no #) for a CSS color, matching Figma's row readout. */
4011
+ function selectionDisplayHex(value: string): string {
4012
+ const parsed = parseCssColor(value);
4013
+ if (!parsed) return value.replace(/^#/, "").toUpperCase();
4014
+ return rgbaToHex(parsed).replace(/^#/, "").toUpperCase();
4015
+ }
4016
+
3292
4017
  function SelectionColorsProperties({
3293
4018
  element,
3294
4019
  onStyleChange,
@@ -3296,49 +4021,77 @@ function SelectionColorsProperties({
3296
4021
  element: ElementInfo;
3297
4022
  onStyleChange: (property: string, value: string) => void;
3298
4023
  }) {
4024
+ // M6 · Figma's Selection colors collapses to a single "Show selection colors"
4025
+ // affordance, expanding to one editable [swatch · hex · opacity] row per
4026
+ // unique color — matching the Fill row grammar instead of a swatch strip.
4027
+ const [expanded, setExpanded] = useState(false);
3299
4028
  const colors = selectionColorValues(element);
3300
- const overflowCount = Math.max(0, colors.length - 3);
3301
4029
  if (!colors.length) return null;
3302
4030
 
3303
4031
  return (
3304
4032
  <PanelSection
3305
4033
  title={"Selection colors" /* i18n-ignore Figma inspector label */}
3306
4034
  >
3307
- <div className="flex min-w-0 items-center gap-1.5">
3308
- {colors.slice(0, 3).map((color, index) => (
3309
- <Popover key={`${color.value}-${index}`}>
3310
- <Tooltip>
3311
- <TooltipTrigger asChild>
4035
+ {expanded ? (
4036
+ <div className="space-y-1.5">
4037
+ {colors.map((color, index) => {
4038
+ const parsed = parseCssColor(color.value);
4039
+ const opacity = parsed ? alphaToOpacity(parsed.a) : 100;
4040
+ return (
4041
+ <Popover key={`${color.value}-${index}`}>
3312
4042
  <PopoverTrigger asChild>
3313
4043
  <button
3314
4044
  type="button"
3315
- className="size-5 rounded-sm border border-[var(--design-editor-control-border)] transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
3316
- style={swatchStyle(color.value)}
4045
+ className="flex h-6 w-full items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
3317
4046
  aria-label={color.value}
3318
- />
4047
+ >
4048
+ <span
4049
+ className="size-4 shrink-0 rounded-[3px] border border-border/60"
4050
+ style={swatchStyle(color.value)}
4051
+ />
4052
+ <span className="min-w-0 flex-1 truncate text-left uppercase tabular-nums">
4053
+ {selectionDisplayHex(color.value)}
4054
+ </span>
4055
+ <span className="shrink-0 tabular-nums text-muted-foreground">
4056
+ {opacity}%
4057
+ </span>
4058
+ </button>
3319
4059
  </PopoverTrigger>
3320
- </TooltipTrigger>
3321
- <TooltipContent>{color.value}</TooltipContent>
3322
- </Tooltip>
3323
- <PopoverContent
3324
- side="left"
3325
- align="start"
3326
- sideOffset={8}
3327
- className="w-80 p-0"
3328
- >
3329
- <FigmaColorPicker
3330
- value={cssColorOrFallback(color.value, "#000000")}
3331
- onChange={(value) => onStyleChange(color.property, value)}
3332
- />
3333
- </PopoverContent>
3334
- </Popover>
3335
- ))}
3336
- {overflowCount > 0 ? (
3337
- <span className="pl-0.5 text-[11px] font-medium text-muted-foreground">
3338
- +{overflowCount}
4060
+ <PopoverContent
4061
+ side="left"
4062
+ align="start"
4063
+ sideOffset={8}
4064
+ className="w-80 p-0"
4065
+ >
4066
+ <FigmaColorPicker
4067
+ value={cssColorOrFallback(color.value, "#000000")}
4068
+ onChange={(value) => onStyleChange(color.property, value)}
4069
+ />
4070
+ </PopoverContent>
4071
+ </Popover>
4072
+ );
4073
+ })}
4074
+ </div>
4075
+ ) : (
4076
+ <button
4077
+ type="button"
4078
+ className="flex h-6 w-full items-center justify-between gap-2 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-left text-[11px] text-muted-foreground hover:bg-[var(--design-editor-panel-raised-bg)] hover:text-foreground"
4079
+ onClick={() => setExpanded(true)}
4080
+ >
4081
+ <span className="truncate">
4082
+ {"Show selection colors" /* i18n-ignore Figma inspector label */}
3339
4083
  </span>
3340
- ) : null}
3341
- </div>
4084
+ <div className="flex shrink-0 items-center -space-x-1">
4085
+ {colors.slice(0, 3).map((color, index) => (
4086
+ <span
4087
+ key={`${color.value}-${index}`}
4088
+ className="size-3.5 rounded-sm border border-[var(--design-editor-panel-bg)]"
4089
+ style={swatchStyle(color.value)}
4090
+ />
4091
+ ))}
4092
+ </div>
4093
+ </button>
4094
+ )}
3342
4095
  </PanelSection>
3343
4096
  );
3344
4097
  }
@@ -3362,6 +4115,7 @@ const TEXT_TAGS = new Set([
3362
4115
  export function EditPanel({
3363
4116
  selectedElement,
3364
4117
  pageStyles = {},
4118
+ headerTrailing,
3365
4119
  width = 256,
3366
4120
  activeTab = "design",
3367
4121
  onActiveTabChange,
@@ -3381,6 +4135,7 @@ export function EditPanel({
3381
4135
  format: "png",
3382
4136
  suffix: "",
3383
4137
  });
4138
+ const [showExportPreview, setShowExportPreview] = useState(false);
3384
4139
  const isTextElement = selectedElement
3385
4140
  ? TEXT_TAGS.has(selectedElement.tagName)
3386
4141
  : false;
@@ -3406,6 +4161,7 @@ export function EditPanel({
3406
4161
  <InspectorTabsHeader
3407
4162
  activeTab={activeTab}
3408
4163
  onActiveTabChange={handleActiveTabChange}
4164
+ trailing={headerTrailing}
3409
4165
  />
3410
4166
 
3411
4167
  {activeTab === "design" ? (
@@ -3460,10 +4216,31 @@ export function EditPanel({
3460
4216
  element={selectedElement}
3461
4217
  onStyleChange={onStyleChange}
3462
4218
  />
4219
+ {isContainerElement(selectedElement) ? (
4220
+ <LayoutGuideProperties
4221
+ element={selectedElement}
4222
+ onStyleChange={onStyleChange}
4223
+ />
4224
+ ) : null}
3463
4225
  </>
3464
4226
  )}
3465
4227
  {onExport ? (
3466
- <PanelSection title={t("editPanel.sections.export")}>
4228
+ <PanelSection
4229
+ title={t("editPanel.sections.export")}
4230
+ actions={
4231
+ <SectionIconToggle
4232
+ label={
4233
+ showExportPreview
4234
+ ? "Hide preview" /* i18n-ignore Figma inspector action */
4235
+ : "Show preview" /* i18n-ignore Figma inspector action */
4236
+ }
4237
+ active={showExportPreview}
4238
+ onClick={() => setShowExportPreview((shown) => !shown)}
4239
+ >
4240
+ <IconPhoto className="size-3.5" />
4241
+ </SectionIconToggle>
4242
+ }
4243
+ >
3467
4244
  <ExportSettingsPanel
3468
4245
  value={exportSettings}
3469
4246
  formats={["png", "svg"]}
@@ -3473,6 +4250,9 @@ export function EditPanel({
3473
4250
  }
3474
4251
  onExport={onExport}
3475
4252
  />
4253
+ {showExportPreview ? (
4254
+ <ExportPreview element={selectedElement} />
4255
+ ) : null}
3476
4256
  </PanelSection>
3477
4257
  ) : null}
3478
4258
  </div>