Wire editor actions, move kiwi-serialize, clean dead code
- VariablesDialog: replace 7 manual undo.push + graph mutation + requestRender patterns with editor.renameCollection/addVariable/removeVariable etc. - PositionSection: replace inline alignment/flip/rotate geometry with editor.alignNodes/flipNodes/rotateNodes - Move kiwi-serialize.ts into kiwi/ where it belongs - Remove dead createPropertyChange export from undo.ts - Un-export internal queryFonts/FontInfo from fonts.ts - Deduplicate font weight maps: single FONT_WEIGHT_NAMES in fonts.ts, figma-api-proxy.ts imports from there
This commit is contained in:
parent
4b49d69601
commit
30f8a3fda6
|
|
@ -7,7 +7,7 @@ import {
|
|||
buildFigKiwi,
|
||||
parseFigKiwiChunks,
|
||||
decompressFigKiwiDataAsync
|
||||
} from './kiwi-serialize'
|
||||
} from './kiwi/kiwi-serialize'
|
||||
import { initCodec, getCompiledSchema, getSchemaBytes } from './kiwi/codec'
|
||||
import { decodeBinarySchema, compileSchema, ByteBuffer } from './kiwi/kiwi-schema'
|
||||
import { nodeChangeToProps, sortChildren } from './kiwi/kiwi-convert'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { zipSync, type Zippable } from 'fflate'
|
||||
import { buildFigKiwi } from './kiwi-serialize'
|
||||
import { buildFigKiwi } from './kiwi/kiwi-serialize'
|
||||
|
||||
interface CompressMessage {
|
||||
schemaDeflated: Uint8Array
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { zipSync, deflateSync, type Zippable } from 'fflate'
|
||||
|
||||
import { CANVAS_BG_COLOR, IS_BROWSER, IS_TAURI } from './constants'
|
||||
import { sceneNodeToKiwi, fractionalPosition, buildFigKiwi, buildFontDigestMap, safeColor } from './kiwi-serialize'
|
||||
import { sceneNodeToKiwi, fractionalPosition, buildFigKiwi, buildFontDigestMap, safeColor } from './kiwi/kiwi-serialize'
|
||||
import { initCodec, getCompiledSchema, getSchemaBytes } from './kiwi/codec'
|
||||
import { stringToGuid } from './kiwi/kiwi-convert'
|
||||
import { renderThumbnail } from './render-image'
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import type {
|
|||
LayoutMode,
|
||||
} from './scene-graph'
|
||||
import { normalizeColor } from './color'
|
||||
import { FONT_WEIGHT_NAMES } from './fonts'
|
||||
import type { Rect } from './types'
|
||||
import { copyFills, copyStrokes, copyEffects } from './copy'
|
||||
|
||||
|
|
@ -17,42 +18,24 @@ const MIXED = Symbol('mixed')
|
|||
export type FigmaFontName = { family: string; style: string }
|
||||
|
||||
export function weightToStyleName(weight: number, italic: boolean): string {
|
||||
const names: Record<number, string> = {
|
||||
100: 'Thin',
|
||||
200: 'Extra Light',
|
||||
300: 'Light',
|
||||
400: 'Regular',
|
||||
500: 'Medium',
|
||||
600: 'Semi Bold',
|
||||
700: 'Bold',
|
||||
800: 'Extra Bold',
|
||||
900: 'Black'
|
||||
}
|
||||
const base = names[weight] ?? 'Regular'
|
||||
const base = FONT_WEIGHT_NAMES[weight] ?? 'Regular'
|
||||
return italic ? `${base} Italic` : base
|
||||
}
|
||||
|
||||
const STYLE_NAME_TO_WEIGHT: Record<string, number> = Object.fromEntries([
|
||||
...Object.entries(FONT_WEIGHT_NAMES).map(([w, name]) => [name.toLowerCase(), Number(w)]),
|
||||
['ultra light', 200],
|
||||
['', 400],
|
||||
['demi bold', 600],
|
||||
['ultra bold', 800],
|
||||
['heavy', 900]
|
||||
])
|
||||
|
||||
export function styleNameToWeight(style: string): { weight: number; italic: boolean } {
|
||||
const lower = style.toLowerCase()
|
||||
const italic = lower.includes('italic')
|
||||
const clean = lower.replace(/italic/i, '').trim()
|
||||
const map: Record<string, number> = {
|
||||
thin: 100,
|
||||
'extra light': 200,
|
||||
'ultra light': 200,
|
||||
light: 300,
|
||||
regular: 400,
|
||||
'': 400,
|
||||
medium: 500,
|
||||
'semi bold': 600,
|
||||
'demi bold': 600,
|
||||
bold: 700,
|
||||
'extra bold': 800,
|
||||
'ultra bold': 800,
|
||||
black: 900,
|
||||
heavy: 900
|
||||
}
|
||||
return { weight: map[clean] ?? 400, italic }
|
||||
return { weight: STYLE_NAME_TO_WEIGHT[clean] ?? 400, italic }
|
||||
}
|
||||
|
||||
export const INTERNAL_ID = Symbol('id')
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export function getFontProvider(): TypefaceFontProvider | null {
|
|||
return fontProvider
|
||||
}
|
||||
|
||||
export async function queryFonts(): Promise<FontInfo[]> {
|
||||
async function queryFonts(): Promise<FontInfo[]> {
|
||||
if (!IS_BROWSER || !window.queryLocalFonts) return []
|
||||
try {
|
||||
const fonts = await window.queryLocalFonts()
|
||||
|
|
@ -353,17 +353,20 @@ export function setCJKFallbackFamily(family: string): void {
|
|||
cjkFallbackFamily = family
|
||||
}
|
||||
|
||||
export function weightToStyle(weight: number, italic = false): string {
|
||||
let label = 'Regular'
|
||||
if (weight <= 100) label = 'Thin'
|
||||
else if (weight <= 200) label = 'ExtraLight'
|
||||
else if (weight <= 300) label = 'Light'
|
||||
else if (weight <= 400) label = 'Regular'
|
||||
else if (weight <= 500) label = 'Medium'
|
||||
else if (weight <= 600) label = 'SemiBold'
|
||||
else if (weight <= 700) label = 'Bold'
|
||||
else if (weight <= 800) label = 'ExtraBold'
|
||||
else if (weight >= 900) label = 'Black'
|
||||
if (italic) label += ' Italic'
|
||||
return label
|
||||
export const FONT_WEIGHT_NAMES: Record<number, string> = {
|
||||
100: 'Thin',
|
||||
200: 'Extra Light',
|
||||
300: 'Light',
|
||||
400: 'Regular',
|
||||
500: 'Medium',
|
||||
600: 'Semi Bold',
|
||||
700: 'Bold',
|
||||
800: 'Extra Bold',
|
||||
900: 'Black'
|
||||
}
|
||||
|
||||
export function weightToStyle(weight: number, italic = false): string {
|
||||
const rounded = Math.round(weight / 100) * 100
|
||||
const label = (FONT_WEIGHT_NAMES[rounded] ?? 'Regular').replace(/ /g, '')
|
||||
return italic ? `${label} Italic` : label
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ export { computeLayout, computeAllLayouts, setTextMeasurer } from './layout'
|
|||
export type { TextMeasurer } from './layout'
|
||||
export { getCanvasKit, getGpuBackend, type CanvasKitOptions, type GpuBackend } from './canvaskit'
|
||||
export {
|
||||
FONT_WEIGHT_NAMES,
|
||||
collectFontKeys,
|
||||
loadFont,
|
||||
listFamilies,
|
||||
|
|
@ -171,7 +172,7 @@ export {
|
|||
sceneNodeToKiwi,
|
||||
fractionalPosition,
|
||||
mapToFigmaType
|
||||
} from './kiwi-serialize'
|
||||
} from './kiwi/kiwi-serialize'
|
||||
|
||||
export {
|
||||
createElement,
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ export const FIG_KIWI_VERSION = 106
|
|||
|
||||
import { deflateSync, inflateSync } from 'fflate'
|
||||
|
||||
import { weightToStyle, getLoadedFontData } from './fonts'
|
||||
import { encodeVectorNetworkBlob } from './vector'
|
||||
import { stringToGuid, VARIABLE_BINDING_FIELDS } from './kiwi/kiwi-convert'
|
||||
import { weightToStyle, getLoadedFontData } from '../fonts'
|
||||
import { encodeVectorNetworkBlob } from '../vector'
|
||||
import { stringToGuid, VARIABLE_BINDING_FIELDS } from './kiwi-convert'
|
||||
|
||||
import type { NodeChange, Paint, VariableConsumptionEntry } from './kiwi/codec'
|
||||
import type { SceneGraph, SceneNode, CharacterStyleOverride } from './scene-graph'
|
||||
import type { Color, GUID } from './types'
|
||||
import type { NodeChange, Paint, VariableConsumptionEntry } from './codec'
|
||||
import type { SceneGraph, SceneNode, CharacterStyleOverride } from '../scene-graph'
|
||||
import type { Color, GUID } from '../types'
|
||||
|
||||
const fontDigestCache = new Map<string, Uint8Array>()
|
||||
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import type { SceneNode } from './scene-graph'
|
||||
|
||||
export interface UndoEntry {
|
||||
label: string
|
||||
forward: () => void
|
||||
|
|
@ -92,18 +90,4 @@ export class UndoManager {
|
|||
}
|
||||
}
|
||||
|
||||
export function createPropertyChange(
|
||||
node: SceneNode,
|
||||
changes: Partial<SceneNode>,
|
||||
label: string
|
||||
): UndoEntry {
|
||||
const previous = Object.fromEntries(
|
||||
(Object.keys(changes) as (keyof SceneNode)[]).map((key) => [key, node[key]])
|
||||
) as Partial<SceneNode>
|
||||
|
||||
return {
|
||||
label,
|
||||
forward: () => Object.assign(node, changes),
|
||||
inverse: () => Object.assign(node, previous)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,20 +69,7 @@ function commitRenameCollection(id: string, input: HTMLInputElement) {
|
|||
const value = input.value.trim()
|
||||
const collection = store.graph.variableCollections.get(id)
|
||||
if (collection && value && value !== collection.name) {
|
||||
const oldName = collection.name
|
||||
store.undo.push({
|
||||
label: 'Rename collection',
|
||||
forward: () => {
|
||||
store.graph.variableCollections.set(id, { ...collection, name: value })
|
||||
store.requestRender()
|
||||
},
|
||||
inverse: () => {
|
||||
store.graph.variableCollections.set(id, { ...collection, name: oldName })
|
||||
store.requestRender()
|
||||
}
|
||||
})
|
||||
store.graph.variableCollections.set(id, { ...collection, name: value })
|
||||
store.requestRender()
|
||||
store.renameCollection(id, value)
|
||||
}
|
||||
editingCollectionId.value = null
|
||||
}
|
||||
|
|
@ -119,42 +106,15 @@ function shortName(variable: Variable): string {
|
|||
|
||||
function commitNameEdit(variable: Variable, newName: string) {
|
||||
if (newName && newName !== variable.name) {
|
||||
const oldName = variable.name
|
||||
store.undo.push({
|
||||
label: 'Rename variable',
|
||||
forward: () => {
|
||||
variable.name = newName
|
||||
store.requestRender()
|
||||
},
|
||||
inverse: () => {
|
||||
variable.name = oldName
|
||||
store.requestRender()
|
||||
}
|
||||
})
|
||||
variable.name = newName
|
||||
store.requestRender()
|
||||
store.renameVariable(variable.id, newName)
|
||||
}
|
||||
}
|
||||
|
||||
function updateColorValue(variable: Variable, modeId: string, color: Color) {
|
||||
const oldValue = structuredClone(variable.valuesByMode[modeId])
|
||||
store.undo.push({
|
||||
label: 'Change variable value',
|
||||
forward: () => {
|
||||
variable.valuesByMode[modeId] = color
|
||||
store.requestRender()
|
||||
},
|
||||
inverse: () => {
|
||||
variable.valuesByMode[modeId] = oldValue
|
||||
store.requestRender()
|
||||
}
|
||||
})
|
||||
variable.valuesByMode[modeId] = color
|
||||
store.requestRender()
|
||||
store.updateVariableValue(variable.id, modeId, color)
|
||||
}
|
||||
|
||||
function commitValueEdit(variable: Variable, modeId: string, newValue: string) {
|
||||
const oldValue = structuredClone(variable.valuesByMode[modeId])
|
||||
let parsed: VariableValue
|
||||
if (variable.type === 'COLOR') {
|
||||
parsed = parseColor(newValue.startsWith('#') ? newValue : `#${newValue}`)
|
||||
|
|
@ -167,19 +127,7 @@ function commitValueEdit(variable: Variable, modeId: string, newValue: string) {
|
|||
} else {
|
||||
parsed = newValue
|
||||
}
|
||||
store.undo.push({
|
||||
label: 'Change variable value',
|
||||
forward: () => {
|
||||
variable.valuesByMode[modeId] = parsed
|
||||
store.requestRender()
|
||||
},
|
||||
inverse: () => {
|
||||
variable.valuesByMode[modeId] = oldValue
|
||||
store.requestRender()
|
||||
}
|
||||
})
|
||||
variable.valuesByMode[modeId] = parsed
|
||||
store.requestRender()
|
||||
store.updateVariableValue(variable.id, modeId, parsed)
|
||||
}
|
||||
|
||||
function addVariable() {
|
||||
|
|
@ -201,19 +149,7 @@ function addVariable() {
|
|||
description: '',
|
||||
hiddenFromPublishing: false
|
||||
}
|
||||
store.undo.push({
|
||||
label: 'Create variable',
|
||||
forward: () => {
|
||||
store.graph.addVariable(variable)
|
||||
store.requestRender()
|
||||
},
|
||||
inverse: () => {
|
||||
store.graph.removeVariable(id)
|
||||
store.requestRender()
|
||||
}
|
||||
})
|
||||
store.graph.addVariable(variable)
|
||||
store.requestRender()
|
||||
store.addVariable(variable)
|
||||
}
|
||||
|
||||
function addCollection() {
|
||||
|
|
@ -225,42 +161,12 @@ function addCollection() {
|
|||
defaultModeId: 'default',
|
||||
variableIds: []
|
||||
}
|
||||
const prevTab = activeTab.value
|
||||
store.undo.push({
|
||||
label: 'Create collection',
|
||||
forward: () => {
|
||||
store.graph.addCollection(collection)
|
||||
activeTab.value = id
|
||||
store.requestRender()
|
||||
},
|
||||
inverse: () => {
|
||||
store.graph.removeCollection(id)
|
||||
activeTab.value = prevTab
|
||||
store.requestRender()
|
||||
}
|
||||
})
|
||||
store.graph.addCollection(collection)
|
||||
store.addCollection(collection)
|
||||
activeTab.value = id
|
||||
store.requestRender()
|
||||
}
|
||||
|
||||
function removeVariable(id: string) {
|
||||
const variable = store.graph.variables.get(id)
|
||||
if (!variable) return
|
||||
const snapshot = structuredClone(variable)
|
||||
store.undo.push({
|
||||
label: 'Delete variable',
|
||||
forward: () => {
|
||||
store.graph.removeVariable(id)
|
||||
store.requestRender()
|
||||
},
|
||||
inverse: () => {
|
||||
store.graph.addVariable(snapshot)
|
||||
store.requestRender()
|
||||
}
|
||||
})
|
||||
store.graph.removeVariable(id)
|
||||
store.requestRender()
|
||||
store.removeVariable(id)
|
||||
}
|
||||
|
||||
const columns = computed<ColumnDef<Variable>[]>(() => {
|
||||
|
|
|
|||
|
|
@ -19,107 +19,7 @@ const hValue = multiProp('height')
|
|||
const rotationValue = computed(() =>
|
||||
isMulti.value ? multiProp('rotation').value : Math.round(node.value?.rotation ?? 0)
|
||||
)
|
||||
|
||||
type HAlign = 'left' | 'center' | 'right'
|
||||
type VAlign = 'top' | 'center' | 'bottom'
|
||||
|
||||
function alignHorizontal(align: HAlign) {
|
||||
const selected = nodes.value
|
||||
if (selected.length === 0) return
|
||||
|
||||
let minX: number, maxX: number
|
||||
|
||||
if (selected.length === 1) {
|
||||
const parent = store.graph.getNode(selected[0].parentId ?? '')
|
||||
if (!parent) return
|
||||
minX = 0
|
||||
maxX = parent.width
|
||||
} else {
|
||||
minX = Infinity
|
||||
maxX = -Infinity
|
||||
for (const n of selected) {
|
||||
const abs = store.graph.getAbsolutePosition(n.id)
|
||||
minX = Math.min(minX, abs.x)
|
||||
maxX = Math.max(maxX, abs.x + n.width)
|
||||
}
|
||||
}
|
||||
|
||||
for (const n of selected) {
|
||||
let targetX: number
|
||||
if (selected.length === 1) {
|
||||
if (align === 'left') targetX = minX
|
||||
else if (align === 'right') targetX = maxX - n.width
|
||||
else targetX = (minX + maxX) / 2 - n.width / 2
|
||||
store.updateNode(n.id, { x: targetX })
|
||||
} else {
|
||||
const abs = store.graph.getAbsolutePosition(n.id)
|
||||
if (align === 'left') targetX = minX
|
||||
else if (align === 'right') targetX = maxX - n.width
|
||||
else targetX = (minX + maxX) / 2 - n.width / 2
|
||||
store.updateNode(n.id, { x: n.x + (targetX - abs.x) })
|
||||
}
|
||||
}
|
||||
store.requestRender()
|
||||
}
|
||||
|
||||
function alignVertical(align: VAlign) {
|
||||
const selected = nodes.value
|
||||
if (selected.length === 0) return
|
||||
|
||||
let minY: number, maxY: number
|
||||
|
||||
if (selected.length === 1) {
|
||||
const parent = store.graph.getNode(selected[0].parentId ?? '')
|
||||
if (!parent) return
|
||||
minY = 0
|
||||
maxY = parent.height
|
||||
} else {
|
||||
minY = Infinity
|
||||
maxY = -Infinity
|
||||
for (const n of selected) {
|
||||
const abs = store.graph.getAbsolutePosition(n.id)
|
||||
minY = Math.min(minY, abs.y)
|
||||
maxY = Math.max(maxY, abs.y + n.height)
|
||||
}
|
||||
}
|
||||
|
||||
for (const n of selected) {
|
||||
let targetY: number
|
||||
if (selected.length === 1) {
|
||||
if (align === 'top') targetY = minY
|
||||
else if (align === 'bottom') targetY = maxY - n.height
|
||||
else targetY = (minY + maxY) / 2 - n.height / 2
|
||||
store.updateNode(n.id, { y: targetY })
|
||||
} else {
|
||||
const abs = store.graph.getAbsolutePosition(n.id)
|
||||
if (align === 'top') targetY = minY
|
||||
else if (align === 'bottom') targetY = maxY - n.height
|
||||
else targetY = (minY + maxY) / 2 - n.height / 2
|
||||
store.updateNode(n.id, { y: n.y + (targetY - abs.y) })
|
||||
}
|
||||
}
|
||||
store.requestRender()
|
||||
}
|
||||
|
||||
function flipHorizontal() {
|
||||
for (const n of nodes.value) {
|
||||
store.updateNodeWithUndo(n.id, { flipX: !n.flipX }, 'Flip horizontal')
|
||||
}
|
||||
store.requestRender()
|
||||
}
|
||||
|
||||
function flipVertical() {
|
||||
for (const n of nodes.value) {
|
||||
store.updateNodeWithUndo(n.id, { flipY: !n.flipY }, 'Flip vertical')
|
||||
}
|
||||
}
|
||||
|
||||
function rotate90() {
|
||||
for (const n of nodes.value) {
|
||||
store.updateNodeWithUndo(n.id, { rotation: (n.rotation + 90) % 360 }, 'Rotate 90°')
|
||||
}
|
||||
store.requestRender()
|
||||
}
|
||||
const ids = computed(() => nodes.value.map((n) => n.id))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -133,7 +33,7 @@ function rotate90() {
|
|||
class="flex size-7 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-align-left"
|
||||
title="Align left"
|
||||
@click="alignHorizontal('left')"
|
||||
@click="store.alignNodes(ids, 'horizontal', 'min')"
|
||||
>
|
||||
<icon-lucide-align-horizontal-justify-start class="size-3.5" />
|
||||
</button>
|
||||
|
|
@ -141,7 +41,7 @@ function rotate90() {
|
|||
class="flex size-7 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-align-center-h"
|
||||
title="Align center horizontally"
|
||||
@click="alignHorizontal('center')"
|
||||
@click="store.alignNodes(ids, 'horizontal', 'center')"
|
||||
>
|
||||
<icon-lucide-align-horizontal-justify-center class="size-3.5" />
|
||||
</button>
|
||||
|
|
@ -149,7 +49,7 @@ function rotate90() {
|
|||
class="flex size-7 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-align-right"
|
||||
title="Align right"
|
||||
@click="alignHorizontal('right')"
|
||||
@click="store.alignNodes(ids, 'horizontal', 'max')"
|
||||
>
|
||||
<icon-lucide-align-horizontal-justify-end class="size-3.5" />
|
||||
</button>
|
||||
|
|
@ -159,7 +59,7 @@ function rotate90() {
|
|||
class="flex size-7 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-align-top"
|
||||
title="Align top"
|
||||
@click="alignVertical('top')"
|
||||
@click="store.alignNodes(ids, 'vertical', 'min')"
|
||||
>
|
||||
<icon-lucide-align-vertical-justify-start class="size-3.5" />
|
||||
</button>
|
||||
|
|
@ -167,7 +67,7 @@ function rotate90() {
|
|||
class="flex size-7 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-align-center-v"
|
||||
title="Align center vertically"
|
||||
@click="alignVertical('center')"
|
||||
@click="store.alignNodes(ids, 'vertical', 'center')"
|
||||
>
|
||||
<icon-lucide-align-vertical-justify-center class="size-3.5" />
|
||||
</button>
|
||||
|
|
@ -175,7 +75,7 @@ function rotate90() {
|
|||
class="flex size-7 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-align-bottom"
|
||||
title="Align bottom"
|
||||
@click="alignVertical('bottom')"
|
||||
@click="store.alignNodes(ids, 'vertical', 'max')"
|
||||
>
|
||||
<icon-lucide-align-vertical-justify-end class="size-3.5" />
|
||||
</button>
|
||||
|
|
@ -235,7 +135,7 @@ function rotate90() {
|
|||
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-flip-horizontal"
|
||||
title="Flip horizontal"
|
||||
@click="flipHorizontal"
|
||||
@click="store.flipNodes(ids, 'horizontal')"
|
||||
>
|
||||
<icon-lucide-flip-horizontal class="size-3.5" />
|
||||
</button>
|
||||
|
|
@ -243,7 +143,7 @@ function rotate90() {
|
|||
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-flip-vertical"
|
||||
title="Flip vertical"
|
||||
@click="flipVertical"
|
||||
@click="store.flipNodes(ids, 'vertical')"
|
||||
>
|
||||
<icon-lucide-flip-vertical class="size-3.5" />
|
||||
</button>
|
||||
|
|
@ -251,7 +151,7 @@ function rotate90() {
|
|||
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
|
||||
data-test-id="position-rotate-90"
|
||||
title="Rotate 90°"
|
||||
@click="rotate90"
|
||||
@click="store.rotateNodes(ids, 90)"
|
||||
>
|
||||
<icon-lucide-rotate-cw class="size-3.5" />
|
||||
</button>
|
||||
|
|
|
|||
Loading…
Reference in a new issue