@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +28 -0
- package/corpus/core/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/de-DE/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/de-DE/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/es-ES/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/es-ES/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +16 -1
- package/corpus/core/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +14 -1
- package/corpus/core/docs/content/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/template-design.mdx +14 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/application-state/handlers.ts +1 -1
- package/corpus/core/src/brand-kit/index.ts +1 -1
- package/corpus/core/src/cli/skills.ts +18 -0
- package/corpus/core/src/client/AgentPanel.tsx +1 -1
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
- package/corpus/core/src/client/extensions/portable-extension.ts +1 -1
- package/corpus/core/src/code-agents/transcript-normalizer.ts +1 -1
- package/corpus/core/src/extensions/path.ts +1 -4
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +1 -1
- package/corpus/core/src/mcp/build-server.ts +2 -2
- package/corpus/core/src/mcp/embed-app.ts +35 -1
- package/corpus/core/src/org/handlers.ts +5 -5
- package/corpus/core/src/org/plugin.ts +2 -2
- package/corpus/core/src/scripts/call-agent.ts +1 -1
- package/corpus/core/src/scripts/db/safety.ts +1 -1
- package/corpus/core/src/server/auth.ts +3 -0
- package/corpus/core/src/server/builder-design-systems.ts +308 -0
- package/corpus/core/src/server/core-routes-plugin.ts +29 -3
- package/corpus/core/src/server/index.ts +8 -0
- package/corpus/core/src/styles/agent-native.css +14 -7
- package/corpus/core/src/usage/store.ts +2 -2
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +298 -15
- package/corpus/templates/analytics/actions/get-sql-dashboard.ts +9 -4
- package/corpus/templates/analytics/actions/mutate-dashboard.ts +18 -2
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +12 -2
- package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +66 -0
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +37 -0
- package/corpus/templates/analytics/app/i18n-data.ts +404 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +333 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +121 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +49 -26
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +137 -36
- package/corpus/templates/analytics/changelog/2026-06-29-analytics-agents-can-now-place-new-charts-into-requested-das.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-analytics-queries-now-report-failed-source-query-errors-inst.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-charts-can-now-be-dropped-between-full-dashboard-rows-to-cre.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-dashboard-email-reports-include-the-full-dashboard-image-wit.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-main-surface-border-reaches-the-top.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-session-replay-now-records-playable-sessions-in-production-a.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-set-up-session-replay-storage-from-settings-connect-builder-.md +6 -0
- package/corpus/templates/analytics/scripts/cleanup-empty-session-recordings.ts +102 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +49 -26
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +81 -20
- package/corpus/templates/analytics/server/lib/replay-storage.ts +79 -0
- package/corpus/templates/analytics/server/lib/s3-upload-provider.ts +255 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +29 -8
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -2
- package/corpus/templates/analytics/server/plugins/onboarding.ts +97 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +24 -0
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +105 -5
- package/corpus/templates/content/actions/set-image-alt-text.ts +1 -1
- package/corpus/templates/content/shared/document-export.ts +1 -1
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -18
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +17 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +32 -5
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +259 -0
- package/corpus/templates/design/actions/update-file.ts +9 -2
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +33 -75
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +72 -9
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +41 -39
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1029 -249
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +450 -101
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +978 -154
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +7 -9
- package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +133 -66
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +1234 -344
- package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +144 -94
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +301 -72
- package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +876 -286
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +601 -0
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +242 -0
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +30 -7
- package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +212 -155
- package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +423 -0
- package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +487 -107
- package/corpus/templates/design/app/components/design/inspector/index.ts +27 -0
- package/corpus/templates/design/app/global.css +29 -3
- package/corpus/templates/design/app/i18n/zh-TW.ts +13 -0
- package/corpus/templates/design/app/i18n-data.ts +239 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +1023 -353
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +57 -221
- package/corpus/templates/design/changelog/2026-06-29-canvas-selection-no-longer-flashes-broken-prototype-html-whe.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-command-menu-selections-are-clearer-in-dark-mode.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-element-selection-now-stays-on-the-intended-layer-after-sele.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-full-view-controls-now-sit-above-frames-inst.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-tools-can-draw-shapes-create-screens-and-kee.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-now-creates-anchor-based-bezier-vectors-with-ha.md +6 -0
- package/corpus/templates/design/server/handlers/import-figma-system.ts +38 -85
- package/corpus/templates/design/server/lib/design-snapshot.ts +9 -1
- package/corpus/templates/design/shared/code-layer.ts +209 -9
- package/corpus/templates/design/shared/html-content.ts +21 -0
- package/corpus/templates/design/shared/pen-path.ts +207 -0
- package/corpus/templates/mail/app/lib/utils.ts +2 -2
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +9 -10
- package/corpus/templates/slides/AGENTS.md +3 -2
- package/corpus/templates/slides/actions/import-file.ts +26 -17
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +259 -0
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +35 -167
- package/corpus/templates/slides/app/components/design-system/fig-import-response.ts +9 -13
- package/corpus/templates/slides/app/context/DeckContext.tsx +1 -1
- package/corpus/templates/slides/app/i18n/ar-SA.ts +9 -37
- package/corpus/templates/slides/app/i18n/de-DE.ts +10 -25
- package/corpus/templates/slides/app/i18n/en-US.ts +9 -26
- package/corpus/templates/slides/app/i18n/es-ES.ts +10 -28
- package/corpus/templates/slides/app/i18n/fr-FR.ts +10 -28
- package/corpus/templates/slides/app/i18n/hi-IN.ts +9 -26
- package/corpus/templates/slides/app/i18n/ja-JP.ts +10 -22
- package/corpus/templates/slides/app/i18n/ko-KR.ts +9 -23
- package/corpus/templates/slides/app/i18n/pt-BR.ts +10 -28
- package/corpus/templates/slides/app/i18n/zh-CN.ts +9 -22
- package/corpus/templates/slides/app/i18n/zh-TW.ts +9 -22
- package/corpus/templates/slides/server/handlers/import-figma-system.ts +44 -12
- package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
- package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +1 -1
- package/dist/application-state/handlers.js +1 -1
- package/dist/application-state/handlers.js.map +1 -1
- package/dist/brand-kit/index.d.ts +1 -1
- package/dist/brand-kit/index.js +1 -1
- package/dist/brand-kit/index.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +18 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.js +1 -1
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +1 -1
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/extensions/portable-extension.d.ts +1 -1
- package/dist/client/extensions/portable-extension.d.ts.map +1 -1
- package/dist/client/extensions/portable-extension.js.map +1 -1
- package/dist/code-agents/transcript-normalizer.js +1 -1
- package/dist/code-agents/transcript-normalizer.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/extensions/path.d.ts +1 -1
- package/dist/extensions/path.d.ts.map +1 -1
- package/dist/extensions/path.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/integrations/a2a-continuation-processor.js +1 -1
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/mcp/build-server.d.ts +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +1 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +35 -1
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +8 -8
- package/dist/org/handlers.js +5 -5
- package/dist/org/handlers.js.map +1 -1
- package/dist/org/plugin.js +2 -2
- package/dist/org/plugin.js.map +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/call-agent.js +1 -1
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/scripts/db/safety.js +1 -1
- package/dist/scripts/db/safety.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +3 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-design-systems.d.ts +28 -0
- package/dist/server/builder-design-systems.d.ts.map +1 -0
- package/dist/server/builder-design-systems.js +217 -0
- package/dist/server/builder-design-systems.js.map +1 -0
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +20 -3
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/styles/agent-native.css +14 -7
- package/dist/usage/store.js +2 -2
- package/dist/usage/store.js.map +1 -1
- package/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
- package/docs/content/locales/ar-SA/template-design.mdx +14 -1
- package/docs/content/locales/de-DE/skills-guide.mdx +5 -0
- package/docs/content/locales/de-DE/template-design.mdx +15 -1
- package/docs/content/locales/es-ES/skills-guide.mdx +5 -0
- package/docs/content/locales/es-ES/template-design.mdx +15 -1
- package/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
- package/docs/content/locales/fr-FR/template-design.mdx +15 -1
- package/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
- package/docs/content/locales/hi-IN/template-design.mdx +15 -1
- package/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
- package/docs/content/locales/ja-JP/template-design.mdx +16 -1
- package/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
- package/docs/content/locales/ko-KR/template-design.mdx +14 -1
- package/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
- package/docs/content/locales/pt-BR/template-design.mdx +14 -1
- package/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
- package/docs/content/locales/zh-CN/template-design.mdx +14 -1
- package/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
- package/docs/content/locales/zh-TW/template-design.mdx +14 -1
- package/docs/content/skills-guide.mdx +5 -0
- package/docs/content/template-design.mdx +14 -1
- package/package.json +1 -1
- package/corpus/templates/design/actions/import-figma-file.ts +0 -166
- 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={
|
|
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(
|
|
880
|
+
const [collapsed, setCollapsed] = useState(defaultCollapsed);
|
|
821
881
|
|
|
822
882
|
return (
|
|
823
|
-
<section className="shrink-0 border-
|
|
824
|
-
<div className="flex min-h-
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
1280
|
-
|
|
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
|
|
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 === "
|
|
1536
|
+
} else if (sizing === "hug") {
|
|
1537
|
+
// Hug contents → shrink to fit children/content.
|
|
1538
|
+
patch[sizeProperty] = "fit-content";
|
|
1334
1539
|
if (isMainFlexAxis) {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
patch.
|
|
1338
|
-
patch.
|
|
1339
|
-
|
|
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
|
-
|
|
1547
|
+
// Fill container.
|
|
1346
1548
|
if (isMainFlexAxis) {
|
|
1347
|
-
|
|
1348
|
-
patch.
|
|
1349
|
-
patch.
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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=
|
|
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
|
-
|
|
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
|
-
"
|
|
1719
|
+
"size-6 cursor-pointer rounded-md text-muted-foreground hover:text-foreground",
|
|
1472
1720
|
active &&
|
|
1473
|
-
"bg-[var(--design-editor-
|
|
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
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
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
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
2171
|
-
<
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
2437
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
2852
|
+
<SizingField
|
|
2453
2853
|
axis="H"
|
|
2854
|
+
sizingAxis="vertical"
|
|
2454
2855
|
value={inferElementSizing(element, "vertical")}
|
|
2455
2856
|
resolvedSize={element.boundingRect.height}
|
|
2456
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
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
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
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
|
-
) :
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
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
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
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
|
-
"
|
|
2762
|
-
|
|
3356
|
+
"backgroundImage",
|
|
3357
|
+
current ? `${nextLayer}, ${current}` : nextLayer,
|
|
2763
3358
|
);
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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 ??
|
|
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
|
|
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
|
-
<
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
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
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
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="
|
|
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
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
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
|
-
|
|
3341
|
-
|
|
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
|
|
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>
|