From 18899ff13c08dc3b8c40ee58fd5e584e1ec90b2e Mon Sep 17 00:00:00 2001 From: Luca Candela <73209+CaliLuke@users.noreply.github.com> Date: Sat, 25 Jul 2026 14:05:57 -0700 Subject: [PATCH] feat(editor): add Figma-style frame presets (#418) Add frame preset creation and resizing with constraint-aware layout, undo/redo, toolbar flyouts, localization, documentation, and regression coverage. --- CHANGELOG.md | 1 + packages/core/src/editor/shapes.ts | 9 +- .../core/src/editor/shapes/frame-presets.ts | 154 +++++++++ packages/core/src/layout/grid.ts | 3 +- packages/docs/guide/figma-comparison.md | 2 +- packages/docs/user-guide/drawing-shapes.md | 2 + packages/scene-graph/package.json | 6 + packages/scene-graph/src/resize.ts | 184 +++++++++++ packages/scene-graph/tsdown.config.ts | 1 + packages/vue/src/i18n/locales/de/panels.json | 14 +- packages/vue/src/i18n/locales/es/panels.json | 14 +- packages/vue/src/i18n/locales/fr/panels.json | 14 +- packages/vue/src/i18n/locales/it/panels.json | 14 +- packages/vue/src/i18n/locales/ja/panels.json | 14 +- packages/vue/src/i18n/locales/pl/panels.json | 14 +- packages/vue/src/i18n/locales/ru/panels.json | 14 +- .../vue/src/i18n/locales/zh-cn/panels.json | 14 +- packages/vue/src/i18n/messages/panels.ts | 12 + packages/vue/src/index.ts | 6 +- .../src/primitives/Toolbar/ToolbarRoot.vue | 17 +- .../vue/src/primitives/Toolbar/context.ts | 1 + .../src/primitives/Toolbar/useToolbarState.ts | 27 +- packages/vue/src/shared/input/resize.ts | 65 ++-- .../src/shared/input/resize/constraints.ts | 53 --- packages/vue/src/shared/input/resize/start.ts | 41 +-- .../vue/src/shared/input/resize/vector.ts | 37 --- packages/vue/src/shared/input/types.ts | 11 +- src/app/editor/frame-presets.ts | 223 +++++++++++++ src/components/DesignPanel.vue | 17 +- src/components/Toolbar/DesktopToolbar.vue | 38 +-- src/components/Toolbar/MobileToolbar.vue | 14 +- src/components/Toolbar/ToolFlyout.vue | 70 ++-- src/components/Toolbar/Toolbar.vue | 4 +- .../frame-presets/FramePresetSelect.vue | 55 ++++ .../frame-presets/FramePresetsSection.vue | 60 ++++ src/theme/toolbar.ts | 25 +- tests/e2e/properties/frame-presets.spec.ts | 69 ++++ tests/e2e/toolbar/basic.spec.ts | 60 +++- tests/engine/editor/frame-presets.test.ts | 303 ++++++++++++++++++ tests/engine/vue/controls/constraints.test.ts | 54 +++- 40 files changed, 1439 insertions(+), 297 deletions(-) create mode 100644 packages/core/src/editor/shapes/frame-presets.ts create mode 100644 packages/scene-graph/src/resize.ts delete mode 100644 packages/vue/src/shared/input/resize/constraints.ts delete mode 100644 packages/vue/src/shared/input/resize/vector.ts create mode 100644 src/app/editor/frame-presets.ts create mode 100644 src/components/properties/frame-presets/FramePresetSelect.vue create mode 100644 src/components/properties/frame-presets/FramePresetsSection.vue create mode 100644 tests/e2e/properties/frame-presets.spec.ts create mode 100644 tests/engine/editor/frame-presets.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f312ce4f..6cdeff391 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Test OpenAI-compatible provider connections from AI settings with clearer setup errors. - Build custom property panels with new Vue SDK number fields, bindable values, property sections, segmented controls, property lists, color models, fill controls, and gradient primitives. - Connect local MCP clients through automatically discovered private Unix sockets on macOS and Linux, with localhost TCP fallback. (#338) +- Create centered frames from current Figma-style device and asset presets, or resize selected frames from the Design panel while preserving their names. ### Changed diff --git a/packages/core/src/editor/shapes.ts b/packages/core/src/editor/shapes.ts index 3951ed240..b3212f319 100644 --- a/packages/core/src/editor/shapes.ts +++ b/packages/core/src/editor/shapes.ts @@ -8,6 +8,7 @@ import { SECTION_DEFAULT_STROKE } from '#core/constants' +import { createFramePresetActions } from './shapes/frame-presets' import { createPenActions } from './shapes/pen' import { adoptNodesIntoSection as adoptNodesIntoSectionImpl } from './shapes/section-adopt' import type { EditorContext } from './types' @@ -38,7 +39,8 @@ export function createShapeActions(ctx: EditorContext) { y: number, w: number, h: number, - parentId?: string + parentId?: string, + name?: string ): string { const fill = DEFAULT_FILLS[type] ?? DEFAULT_FILLS.RECTANGLE const pid = parentId ?? ctx.state.currentPageId @@ -47,7 +49,8 @@ export function createShapeActions(ctx: EditorContext) { y, width: w, height: h, - fills: [{ ...fill }] + fills: [{ ...fill }], + ...(name ? { name } : {}) } if (type === 'SECTION') { overrides.strokes = [{ ...SECTION_DEFAULT_STROKE }] @@ -79,6 +82,7 @@ export function createShapeActions(ctx: EditorContext) { } const penActions = createPenActions(ctx, createShape) + const framePresetActions = createFramePresetActions(ctx, createShape) function setTool(tool: typeof ctx.state.activeTool) { ctx.setActiveTool(tool) @@ -87,6 +91,7 @@ export function createShapeActions(ctx: EditorContext) { return { createShape, ...penActions, + ...framePresetActions, adoptNodesIntoSection: (sectionId: string) => adoptNodesIntoSectionImpl(ctx, sectionId), setTool } diff --git a/packages/core/src/editor/shapes/frame-presets.ts b/packages/core/src/editor/shapes/frame-presets.ts new file mode 100644 index 000000000..814447a5d --- /dev/null +++ b/packages/core/src/editor/shapes/frame-presets.ts @@ -0,0 +1,154 @@ +import type { SceneNode } from '@open-pencil/scene-graph' +import { + collectResizeDescendants, + computeConstrainedResizeChanges, + type ResizeSnapshot +} from '@open-pencil/scene-graph/resize' + +import type { EditorContext } from '#core/editor/types' + +export interface FramePresetDimensions { + name: string + width: number + height: number +} + +type FrameResizePatch = Pick< + SceneNode, + 'width' | 'height' | 'primaryAxisSizing' | 'counterAxisSizing' | 'layoutGrow' | 'layoutAlignSelf' +> + +type CreateShape = ( + type: 'FRAME', + x: number, + y: number, + width: number, + height: number, + parentId: string | undefined, + name: string +) => string + +function fixedSizePatch( + ctx: EditorContext, + node: SceneNode, + preset: FramePresetDimensions +): FrameResizePatch { + const parent = node.parentId ? ctx.graph.getNode(node.parentId) : undefined + const inheritsStretch = + node.layoutPositioning !== 'ABSOLUTE' && + parent?.layoutMode !== 'NONE' && + (parent?.layoutMode === 'GRID' || parent?.counterAxisAlign === 'STRETCH') + + return { + width: preset.width, + height: preset.height, + primaryAxisSizing: 'FIXED', + counterAxisSizing: 'FIXED', + layoutGrow: 0, + layoutAlignSelf: + node.layoutAlignSelf === 'STRETCH' || (node.layoutAlignSelf === 'AUTO' && inheritsStretch) + ? 'MIN' + : node.layoutAlignSelf + } +} + +export function createFramePresetActions(ctx: EditorContext, createShape: CreateShape) { + function createFrameFromPreset(preset: FramePresetDimensions): string { + const { width: viewportWidth, height: viewportHeight } = ctx.getViewportSize() + const centerX = (viewportWidth / 2 - ctx.state.panX) / ctx.state.zoom + const centerY = (viewportHeight / 2 - ctx.state.panY) / ctx.state.zoom + const previousSelection = new Set(ctx.state.selectedIds) + const id = ctx.undo.runBatch('Create frame', () => { + const createdId = createShape( + 'FRAME', + centerX - preset.width / 2, + centerY - preset.height / 2, + preset.width, + preset.height, + undefined, + preset.name + ) + const createdSelection = new Set([createdId]) + ctx.setSelectedIds(createdSelection) + ctx.undo.push({ + label: 'Select created frame', + forward: () => ctx.setSelectedIds(new Set(createdSelection)), + inverse: () => ctx.setSelectedIds(new Set(previousSelection)) + }) + return createdId + }) + + ctx.setActiveTool('SELECT') + ctx.requestRender() + return id + } + + function applyResize( + id: string, + root: Partial, + descendants: ReadonlyMap | ResizeSnapshot> + ) { + ctx.graph.updateNode(id, root) + for (const [childId, changes] of descendants) { + ctx.graph.updateNode(childId, changes) + if ('vectorNetwork' in changes) ctx.getRenderer()?.invalidateVectorPath(childId) + } + ctx.runLayoutForNode(id) + } + + function applyLayoutAwareResize( + id: string, + previous: FrameResizePatch, + next: FrameResizePatch, + originals: ReadonlyMap + ) { + ctx.graph.updateNode(id, next) + const provisional = computeConstrainedResizeChanges(ctx.graph, id, previous, next, originals) + for (const [childId, changes] of provisional) ctx.graph.updateNode(childId, changes) + ctx.runLayoutForNode(id) + + const final = computeConstrainedResizeChanges(ctx.graph, id, previous, next, originals) + applyResize(id, next, final) + } + + function resizeFrameToPreset(id: string, preset: FramePresetDimensions) { + const node = ctx.graph.getNode(id) + if (node?.type !== 'FRAME') return + + const previous = { + width: node.width, + height: node.height, + primaryAxisSizing: node.primaryAxisSizing, + counterAxisSizing: node.counterAxisSizing, + layoutGrow: node.layoutGrow, + layoutAlignSelf: node.layoutAlignSelf + } + const next = fixedSizePatch(ctx, node, preset) + if ( + previous.width === next.width && + previous.height === next.height && + previous.primaryAxisSizing === next.primaryAxisSizing && + previous.counterAxisSizing === next.counterAxisSizing && + previous.layoutGrow === next.layoutGrow && + previous.layoutAlignSelf === next.layoutAlignSelf + ) { + return + } + + const originalDescendants = collectResizeDescendants(ctx.graph, id) ?? new Map() + applyLayoutAwareResize(id, previous, next, originalDescendants) + const resizedDescendants = collectResizeDescendants(ctx.graph, id) ?? new Map() + ctx.undo.push({ + label: 'Resize frame to preset', + forward: () => applyLayoutAwareResize(id, previous, next, originalDescendants), + inverse: () => { + applyLayoutAwareResize(id, next, previous, resizedDescendants) + // Constraint math rounds and clamps, so only the captured snapshot can restore exactly. + applyResize(id, previous, originalDescendants) + } + }) + ctx.requestRender() + } + + return { createFrameFromPreset, resizeFrameToPreset } +} diff --git a/packages/core/src/layout/grid.ts b/packages/core/src/layout/grid.ts index 7bc53d1f2..d5b30050a 100644 --- a/packages/core/src/layout/grid.ts +++ b/packages/core/src/layout/grid.ts @@ -48,8 +48,9 @@ export function createGridChildNode(child: SceneNode): YogaNode { } const hasLayout = child.layoutMode !== 'NONE' const explicitStretch = child.layoutGrow > 0 || child.layoutAlignSelf === 'STRETCH' + const inheritsContainerStretch = hasLayout && child.layoutAlignSelf === 'AUTO' - if (explicitStretch || hasLayout) { + if (explicitStretch || inheritsContainerStretch) { yogaChild.setWidthStretch() } else { yogaChild.setWidth(child.width) diff --git a/packages/docs/guide/figma-comparison.md b/packages/docs/guide/figma-comparison.md index 4499ad817..5b7290d7d 100644 --- a/packages/docs/guide/figma-comparison.md +++ b/packages/docs/guide/figma-comparison.md @@ -35,7 +35,7 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu | Feature | Status | Notes | |---------|--------|-------| | Shape tools (Rectangle, Ellipse, Line, Polygon, Star) | ✅ | All basic shape types; polygon side count and star inner radius configurable | -| Frames | ✅ | Clip content, independent coordinate system | +| Frames | ✅ | Clip content, independent coordinate system, and Figma-style creation and resize presets | | Groups | ✅ | G to group, G to ungroup | | Sections | ✅ | Title pills, auto-adopt overlapping nodes, luminance-adaptive text | | Arc tool (arcs, semi-circles, rings) | ✅ | arcData with start/end angle and inner radius | diff --git a/packages/docs/user-guide/drawing-shapes.md b/packages/docs/user-guide/drawing-shapes.md index 4abf1663d..96c5e27e1 100644 --- a/packages/docs/user-guide/drawing-shapes.md +++ b/packages/docs/user-guide/drawing-shapes.md @@ -76,6 +76,8 @@ Click **+** to add an effect. Each effect row is collapsible with inline control **Frames** are containers. Drag shapes into a frame to make them children. Frames can clip their content (off by default) and support [auto layout](./auto-layout). +Select the Frame tool to browse collapsible presets for phones, tablets, desktops, presentations, watches, paper, social media, Figma Community assets, and archived devices in the Design panel. Choosing a preset creates a named frame centered in the viewport and returns to the Select tool. With an existing frame selected, use its Frame preset dropdown to resize it without changing its name. + **Sections** are top-level containers that automatically adopt overlapping sibling nodes when drawn. They're useful for organizing large canvases into logical areas. Sections display a title pill that you can drag. ## Keyboard Shortcuts diff --git a/packages/scene-graph/package.json b/packages/scene-graph/package.json index c08ffc9ce..72a8e8b94 100644 --- a/packages/scene-graph/package.json +++ b/packages/scene-graph/package.json @@ -124,6 +124,12 @@ "import": "./dist/geometry.js", "default": "./dist/geometry.js" }, + "./resize": { + "types": "./dist/resize.d.ts", + "bun": "./src/resize.ts", + "import": "./dist/resize.js", + "default": "./dist/resize.js" + }, "./parse-path": { "types": "./dist/parse-path.d.ts", "bun": "./src/parse-path.ts", diff --git a/packages/scene-graph/src/resize.ts b/packages/scene-graph/src/resize.ts new file mode 100644 index 000000000..1763a4eec --- /dev/null +++ b/packages/scene-graph/src/resize.ts @@ -0,0 +1,184 @@ +import type { Rect } from './primitives' +import type { ConstraintType, SceneNode, VectorNetwork } from './types' +import { cloneVectorNetwork } from './vector-network' + +export type ResizeSnapshot = Pick + +interface ResizeGraph { + getNode(id: string): SceneNode | undefined +} + +const CONSTRAINT_CONTAINER_TYPES = new Set([ + 'FRAME', + 'COMPONENT', + 'COMPONENT_SET', + 'INSTANCE', + 'GROUP', + 'BOOLEAN_OPERATION' +]) + +function constrainedAxis( + position: number, + size: number, + parentBefore: number, + parentAfter: number, + constraint: ConstraintType +): { position: number; size: number } { + const delta = parentAfter - parentBefore + if (constraint === 'MAX') return { position: position + delta, size } + if (constraint === 'CENTER') return { position: position + delta / 2, size } + if (constraint === 'STRETCH') return { position, size: Math.max(1, size + delta) } + if (constraint === 'SCALE' && parentBefore > 0) { + const scale = parentAfter / parentBefore + return { position: position * scale, size: Math.max(1, size * scale) } + } + return { position, size } +} + +export function constrainedChildRect( + child: Rect, + parentBefore: Pick, + parentAfter: Pick, + horizontal: ConstraintType, + vertical: ConstraintType +): Rect { + const x = constrainedAxis(child.x, child.width, parentBefore.width, parentAfter.width, horizontal) + const y = constrainedAxis( + child.y, + child.height, + parentBefore.height, + parentAfter.height, + vertical + ) + return { + x: Math.round(x.position), + y: Math.round(y.position), + width: Math.round(x.size), + height: Math.round(y.size) + } +} + +export function scaledChildRect( + child: Rect, + parentBefore: Pick, + parentAfter: Pick +): Rect { + return constrainedChildRect(child, parentBefore, parentAfter, 'SCALE', 'SCALE') +} + +export function scaleVectorNetworkForResize( + vectorNetwork: VectorNetwork | null, + originalWidth: number, + originalHeight: number, + width: number, + height: number +): VectorNetwork | null { + if (!vectorNetwork || originalWidth <= 0 || originalHeight <= 0) return null + + const scaleX = width / originalWidth + const scaleY = height / originalHeight + if (scaleX === 1 && scaleY === 1) return null + + return { + vertices: vectorNetwork.vertices.map((vertex) => ({ + ...vertex, + x: vertex.x * scaleX, + y: vertex.y * scaleY + })), + segments: vectorNetwork.segments.map((segment) => ({ + ...segment, + tangentStart: { + x: segment.tangentStart.x * scaleX, + y: segment.tangentStart.y * scaleY + }, + tangentEnd: { + x: segment.tangentEnd.x * scaleX, + y: segment.tangentEnd.y * scaleY + } + })), + regions: vectorNetwork.regions + } +} + +export function collectResizeDescendants( + graph: ResizeGraph, + rootId: string +): Map | null { + const root = graph.getNode(rootId) + if (!root || !CONSTRAINT_CONTAINER_TYPES.has(root.type)) return null + const snapshots = new Map() + + const collect = (parentId: string) => { + const parent = graph.getNode(parentId) + if (!parent) return + for (const childId of parent.childIds) { + const child = graph.getNode(childId) + if (!child) continue + snapshots.set(childId, { + x: child.x, + y: child.y, + width: child.width, + height: child.height, + vectorNetwork: child.vectorNetwork ? cloneVectorNetwork(child.vectorNetwork) : null + }) + collect(childId) + } + } + + collect(rootId) + return snapshots.size > 0 ? snapshots : null +} + +export function computeConstrainedResizeChanges( + graph: ResizeGraph, + rootId: string, + rootBefore: Pick, + rootAfter: Pick, + originals: ReadonlyMap +): Map> { + const changes = new Map>() + + const compute = ( + parentId: string, + parentBefore: Pick, + parentAfter: Pick + ) => { + const parent = graph.getNode(parentId) + if (!parent) return + const scalesChildren = parent.type === 'GROUP' || parent.type === 'BOOLEAN_OPERATION' + for (const childId of parent.childIds) { + const original = originals.get(childId) + const child = graph.getNode(childId) + if (!original || !child) continue + const isInFlow = parent.layoutMode !== 'NONE' && child.layoutPositioning !== 'ABSOLUTE' + if (isInFlow) { + compute(childId, original, child) + continue + } + const rect = scalesChildren + ? scaledChildRect(original, parentBefore, parentAfter) + : constrainedChildRect( + original, + parentBefore, + parentAfter, + child.horizontalConstraint, + child.verticalConstraint + ) + const childChanges: Partial = { ...rect } + const vectorNetwork = scaleVectorNetworkForResize( + original.vectorNetwork, + original.width, + original.height, + rect.width, + rect.height + ) + if (vectorNetwork) childChanges.vectorNetwork = vectorNetwork + changes.set(childId, childChanges) + // The final pass sees layout containers after Yoga has resolved HUG/FILL sizing. + compute(childId, original, child.layoutMode === 'NONE' ? rect : child) + } + } + + compute(rootId, rootBefore, rootAfter) + return changes +} diff --git a/packages/scene-graph/tsdown.config.ts b/packages/scene-graph/tsdown.config.ts index 28f1cd102..c0776071e 100644 --- a/packages/scene-graph/tsdown.config.ts +++ b/packages/scene-graph/tsdown.config.ts @@ -22,6 +22,7 @@ export default defineConfig({ coordinate: './src/coordinate.ts', matrix: './src/matrix.ts', geometry: './src/geometry.ts', + resize: './src/resize.ts', 'parse-path': './src/parse-path.ts' }, platform: 'neutral', diff --git a/packages/vue/src/i18n/locales/de/panels.json b/packages/vue/src/i18n/locales/de/panels.json index f388d4e40..284a8e750 100644 --- a/packages/vue/src/i18n/locales/de/panels.json +++ b/packages/vue/src/i18n/locales/de/panels.json @@ -6,6 +6,18 @@ "ai": "KI", "assets": "Elemente", "page": "Seite", + "frame": "Frame", + "framePreset": "Frame-Voreinstellung", + "framePresetCustom": "Benutzerdefiniert", + "framePresetCategoryPhone": "Telefon", + "framePresetCategoryTablet": "Tablet", + "framePresetCategoryDesktop": "Desktop", + "framePresetCategoryPresentation": "Präsentation", + "framePresetCategoryWatch": "Uhr", + "framePresetCategoryPaper": "Papier", + "framePresetCategorySocialMedia": "Soziale Medien", + "framePresetCategoryFigmaCommunity": "Figma Community", + "framePresetCategoryArchive": "Archiv", "position": "Position", "layout": "Layout", "autoLayout": "Auto-Layout", @@ -246,4 +258,4 @@ "blendModeSaturation": "Sättigung", "blendModeColor": "Farbe", "blendModeLuminosity": "Luminanz" -} \ No newline at end of file +} diff --git a/packages/vue/src/i18n/locales/es/panels.json b/packages/vue/src/i18n/locales/es/panels.json index 4402cd33f..1ae4caee0 100644 --- a/packages/vue/src/i18n/locales/es/panels.json +++ b/packages/vue/src/i18n/locales/es/panels.json @@ -8,6 +8,18 @@ "ai": "IA", "assets": "Recursos", "page": "Página", + "frame": "Marco", + "framePreset": "Preajuste de marco", + "framePresetCustom": "Personalizado", + "framePresetCategoryPhone": "Teléfono", + "framePresetCategoryTablet": "Tableta", + "framePresetCategoryDesktop": "Escritorio", + "framePresetCategoryPresentation": "Presentación", + "framePresetCategoryWatch": "Reloj", + "framePresetCategoryPaper": "Papel", + "framePresetCategorySocialMedia": "Redes sociales", + "framePresetCategoryFigmaCommunity": "Figma Community", + "framePresetCategoryArchive": "Archivo", "position": "Posición", "layout": "Diseño", "autoLayout": "Auto-layout", @@ -246,4 +258,4 @@ "blendModeSaturation": "Saturación", "blendModeColor": "Color", "blendModeLuminosity": "Luminosidad" -} \ No newline at end of file +} diff --git a/packages/vue/src/i18n/locales/fr/panels.json b/packages/vue/src/i18n/locales/fr/panels.json index 13687e052..d8c42fd85 100644 --- a/packages/vue/src/i18n/locales/fr/panels.json +++ b/packages/vue/src/i18n/locales/fr/panels.json @@ -6,6 +6,18 @@ "ai": "IA", "assets": "Ressources", "page": "Page", + "frame": "Cadre", + "framePreset": "Préréglage de cadre", + "framePresetCustom": "Personnalisé", + "framePresetCategoryPhone": "Téléphone", + "framePresetCategoryTablet": "Tablette", + "framePresetCategoryDesktop": "Ordinateur", + "framePresetCategoryPresentation": "Présentation", + "framePresetCategoryWatch": "Montre", + "framePresetCategoryPaper": "Papier", + "framePresetCategorySocialMedia": "Réseaux sociaux", + "framePresetCategoryFigmaCommunity": "Figma Community", + "framePresetCategoryArchive": "Archives", "position": "Position", "layout": "Disposition", "autoLayout": "Auto-layout", @@ -246,4 +258,4 @@ "blendModeSaturation": "Saturation", "blendModeColor": "Couleur", "blendModeLuminosity": "Luminosité" -} \ No newline at end of file +} diff --git a/packages/vue/src/i18n/locales/it/panels.json b/packages/vue/src/i18n/locales/it/panels.json index 84aa4727e..e33217d20 100644 --- a/packages/vue/src/i18n/locales/it/panels.json +++ b/packages/vue/src/i18n/locales/it/panels.json @@ -6,6 +6,18 @@ "ai": "AI", "assets": "Risorse", "page": "Pagina", + "frame": "Cornice", + "framePreset": "Preimpostazione cornice", + "framePresetCustom": "Personalizzata", + "framePresetCategoryPhone": "Telefono", + "framePresetCategoryTablet": "Tablet", + "framePresetCategoryDesktop": "Desktop", + "framePresetCategoryPresentation": "Presentazione", + "framePresetCategoryWatch": "Orologio", + "framePresetCategoryPaper": "Carta", + "framePresetCategorySocialMedia": "Social media", + "framePresetCategoryFigmaCommunity": "Figma Community", + "framePresetCategoryArchive": "Archivio", "position": "Posizione", "layout": "Layout", "autoLayout": "Auto-layout", @@ -246,4 +258,4 @@ "blendModeSaturation": "Saturazione", "blendModeColor": "Colore", "blendModeLuminosity": "Luminosità" -} \ No newline at end of file +} diff --git a/packages/vue/src/i18n/locales/ja/panels.json b/packages/vue/src/i18n/locales/ja/panels.json index 5fb144da5..1da9f76d0 100644 --- a/packages/vue/src/i18n/locales/ja/panels.json +++ b/packages/vue/src/i18n/locales/ja/panels.json @@ -8,6 +8,18 @@ "ai": "AI", "assets": "アセット", "page": "ページ", + "frame": "フレーム", + "framePreset": "フレームプリセット", + "framePresetCustom": "カスタム", + "framePresetCategoryPhone": "スマートフォン", + "framePresetCategoryTablet": "タブレット", + "framePresetCategoryDesktop": "デスクトップ", + "framePresetCategoryPresentation": "プレゼンテーション", + "framePresetCategoryWatch": "ウォッチ", + "framePresetCategoryPaper": "用紙", + "framePresetCategorySocialMedia": "ソーシャルメディア", + "framePresetCategoryFigmaCommunity": "Figma Community", + "framePresetCategoryArchive": "アーカイブ", "position": "位置", "layout": "レイアウト", "autoLayout": "オートレイアウト", @@ -246,4 +258,4 @@ "blendModeSaturation": "彩度", "blendModeColor": "カラー", "blendModeLuminosity": "輝度" -} \ No newline at end of file +} diff --git a/packages/vue/src/i18n/locales/pl/panels.json b/packages/vue/src/i18n/locales/pl/panels.json index 28f522d70..7ed067e02 100644 --- a/packages/vue/src/i18n/locales/pl/panels.json +++ b/packages/vue/src/i18n/locales/pl/panels.json @@ -6,6 +6,18 @@ "ai": "AI", "assets": "Zasoby", "page": "Strona", + "frame": "Ramka", + "framePreset": "Ustawienie ramki", + "framePresetCustom": "Niestandardowe", + "framePresetCategoryPhone": "Telefon", + "framePresetCategoryTablet": "Tablet", + "framePresetCategoryDesktop": "Komputer", + "framePresetCategoryPresentation": "Prezentacja", + "framePresetCategoryWatch": "Zegarek", + "framePresetCategoryPaper": "Papier", + "framePresetCategorySocialMedia": "Media społecznościowe", + "framePresetCategoryFigmaCommunity": "Figma Community", + "framePresetCategoryArchive": "Archiwum", "position": "Pozycja", "layout": "Układ", "autoLayout": "Auto-layout", @@ -246,4 +258,4 @@ "blendModeSaturation": "Nasycenie", "blendModeColor": "Kolor", "blendModeLuminosity": "Jasność" -} \ No newline at end of file +} diff --git a/packages/vue/src/i18n/locales/ru/panels.json b/packages/vue/src/i18n/locales/ru/panels.json index 40738bee7..f1b0e8608 100644 --- a/packages/vue/src/i18n/locales/ru/panels.json +++ b/packages/vue/src/i18n/locales/ru/panels.json @@ -6,6 +6,18 @@ "ai": "AI", "assets": "Ассеты", "page": "Страница", + "frame": "Фрейм", + "framePreset": "Размер фрейма", + "framePresetCustom": "Пользовательский", + "framePresetCategoryPhone": "Телефон", + "framePresetCategoryTablet": "Планшет", + "framePresetCategoryDesktop": "Компьютер", + "framePresetCategoryPresentation": "Презентация", + "framePresetCategoryWatch": "Часы", + "framePresetCategoryPaper": "Бумага", + "framePresetCategorySocialMedia": "Социальные сети", + "framePresetCategoryFigmaCommunity": "Figma Community", + "framePresetCategoryArchive": "Архив", "position": "Позиция", "layout": "Раскладка", "autoLayout": "Автораскладка", @@ -246,4 +258,4 @@ "blendModeSaturation": "Насыщенность", "blendModeColor": "Цвет", "blendModeLuminosity": "Яркость" -} \ No newline at end of file +} diff --git a/packages/vue/src/i18n/locales/zh-cn/panels.json b/packages/vue/src/i18n/locales/zh-cn/panels.json index 9902ab99c..3bf36a895 100644 --- a/packages/vue/src/i18n/locales/zh-cn/panels.json +++ b/packages/vue/src/i18n/locales/zh-cn/panels.json @@ -6,6 +6,18 @@ "ai": "AI", "assets": "资源", "page": "页面", + "frame": "画框", + "framePreset": "画框预设", + "framePresetCustom": "自定义", + "framePresetCategoryPhone": "手机", + "framePresetCategoryTablet": "平板电脑", + "framePresetCategoryDesktop": "桌面设备", + "framePresetCategoryPresentation": "演示文稿", + "framePresetCategoryWatch": "手表", + "framePresetCategoryPaper": "纸张", + "framePresetCategorySocialMedia": "社交媒体", + "framePresetCategoryFigmaCommunity": "Figma Community", + "framePresetCategoryArchive": "归档", "position": "位置", "layout": "布局", "autoLayout": "自动布局", @@ -246,4 +258,4 @@ "blendModeSaturation": "饱和度", "blendModeColor": "颜色", "blendModeLuminosity": "明度" -} \ No newline at end of file +} diff --git a/packages/vue/src/i18n/messages/panels.ts b/packages/vue/src/i18n/messages/panels.ts index d5649f640..b148ab2e1 100644 --- a/packages/vue/src/i18n/messages/panels.ts +++ b/packages/vue/src/i18n/messages/panels.ts @@ -37,6 +37,18 @@ export const panelMessageDefaults = { spread: 'Spread', page: 'Page', + frame: 'Frame', + framePreset: 'Frame preset', + framePresetCustom: 'Custom', + framePresetCategoryPhone: 'Phone', + framePresetCategoryTablet: 'Tablet', + framePresetCategoryDesktop: 'Desktop', + framePresetCategoryPresentation: 'Presentation', + framePresetCategoryWatch: 'Watch', + framePresetCategoryPaper: 'Paper', + framePresetCategorySocialMedia: 'Social media', + framePresetCategoryFigmaCommunity: 'Figma Community', + framePresetCategoryArchive: 'Archive', position: 'Position', layout: 'Layout', autoLayout: 'Auto layout', diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index cacbfee60..2c9187e35 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -54,7 +54,11 @@ export type { UseFlatReorderDragOptions } from '#vue/shared/drag/useFlatReorderDrag' export { useInlineRename } from '#vue/editor/inline-rename/use' -export { useToolbarState } from '#vue/primitives/Toolbar/useToolbarState' +export { + getToolbarToolSelection, + isToolbarToolActive, + useToolbarState +} from '#vue/primitives/Toolbar/useToolbarState' export { useNodeFontStatus } from '#vue/shared/font-status/use' export { usePropScrub } from '#vue/controls/prop-scrub/use' export { toolCursor } from '#vue/editor/tool-cursor' diff --git a/packages/vue/src/primitives/Toolbar/ToolbarRoot.vue b/packages/vue/src/primitives/Toolbar/ToolbarRoot.vue index 691e80462..0bebf0d47 100644 --- a/packages/vue/src/primitives/Toolbar/ToolbarRoot.vue +++ b/packages/vue/src/primitives/Toolbar/ToolbarRoot.vue @@ -1,5 +1,5 @@