fix(ui): align layout sections with Figma
This commit is contained in:
parent
a23e58d785
commit
80750e65fc
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
### Changed
|
||||
|
||||
- Group sizing and auto-layout controls into one contextual Design panel section, and use Figma’s current Layout guide terminology.
|
||||
- Redesign the editor chrome and Design panel with denser aligned controls, clearer selection and section states, improved menus and overlays, consistent light/dark theming, and better keyboard and screen-reader behavior.
|
||||
- Scale the Layers panel to documents with thousands of nodes through virtualized rows, faster incremental updates, stable expansion, range selection, and scroll-to-selection.
|
||||
- Resolve fonts before text appears, with language-aware CJK and Arabic fallback, character-specific remote subsets, and more reliable rendering as fonts load.
|
||||
|
|
|
|||
|
|
@ -131,9 +131,9 @@
|
|||
"layoutHorizontal": "Horizontales Layout",
|
||||
"layoutVertical": "Vertikales Layout",
|
||||
"layoutGrid": "Rasterlayout",
|
||||
"layoutGrids": "Layout-Raster",
|
||||
"addLayoutGrid": "Layout-Raster hinzufügen",
|
||||
"removeLayoutGrid": "Layout-Raster entfernen",
|
||||
"layoutGrids": "Layout-Hilfslinien",
|
||||
"addLayoutGrid": "Layout-Hilfslinie hinzufügen",
|
||||
"removeLayoutGrid": "Layout-Hilfslinie entfernen",
|
||||
"gridGrid": "Raster",
|
||||
"gridColumns": "Spalten",
|
||||
"gridRows": "Zeilen",
|
||||
|
|
|
|||
|
|
@ -145,9 +145,9 @@
|
|||
"layoutHorizontal": "Diseño horizontal",
|
||||
"layoutVertical": "Diseño vertical",
|
||||
"layoutGrid": "Diseño de cuadrícula",
|
||||
"layoutGrids": "Cuadrículas de diseño",
|
||||
"addLayoutGrid": "Añadir cuadrícula de diseño",
|
||||
"removeLayoutGrid": "Eliminar cuadrícula de diseño",
|
||||
"layoutGrids": "Guías de diseño",
|
||||
"addLayoutGrid": "Añadir guía de diseño",
|
||||
"removeLayoutGrid": "Eliminar guía de diseño",
|
||||
"gridGrid": "Cuadrícula",
|
||||
"gridColumns": "Columnas",
|
||||
"gridRows": "Filas",
|
||||
|
|
|
|||
|
|
@ -131,9 +131,9 @@
|
|||
"layoutHorizontal": "Disposition horizontale",
|
||||
"layoutVertical": "Disposition verticale",
|
||||
"layoutGrid": "Disposition en grille",
|
||||
"layoutGrids": "Grilles de mise en page",
|
||||
"addLayoutGrid": "Ajouter une grille de mise en page",
|
||||
"removeLayoutGrid": "Supprimer la grille de mise en page",
|
||||
"layoutGrids": "Repères de mise en page",
|
||||
"addLayoutGrid": "Ajouter un repère de mise en page",
|
||||
"removeLayoutGrid": "Supprimer le repère de mise en page",
|
||||
"gridGrid": "Grille",
|
||||
"gridColumns": "Colonnes",
|
||||
"gridRows": "Rangées",
|
||||
|
|
|
|||
|
|
@ -131,9 +131,9 @@
|
|||
"layoutHorizontal": "Layout orizzontale",
|
||||
"layoutVertical": "Layout verticale",
|
||||
"layoutGrid": "Layout a griglia",
|
||||
"layoutGrids": "Griglie di layout",
|
||||
"addLayoutGrid": "Aggiungi griglia di layout",
|
||||
"removeLayoutGrid": "Rimuovi griglia di layout",
|
||||
"layoutGrids": "Guide di layout",
|
||||
"addLayoutGrid": "Aggiungi guida di layout",
|
||||
"removeLayoutGrid": "Rimuovi guida di layout",
|
||||
"gridGrid": "Griglia",
|
||||
"gridColumns": "Colonne",
|
||||
"gridRows": "Righe",
|
||||
|
|
|
|||
|
|
@ -145,9 +145,9 @@
|
|||
"layoutHorizontal": "横方向レイアウト",
|
||||
"layoutVertical": "縦方向レイアウト",
|
||||
"layoutGrid": "グリッドレイアウト",
|
||||
"layoutGrids": "レイアウトグリッド",
|
||||
"addLayoutGrid": "レイアウトグリッドを追加",
|
||||
"removeLayoutGrid": "レイアウトグリッドを削除",
|
||||
"layoutGrids": "レイアウトガイド",
|
||||
"addLayoutGrid": "レイアウトガイドを追加",
|
||||
"removeLayoutGrid": "レイアウトガイドを削除",
|
||||
"gridGrid": "グリッド",
|
||||
"gridColumns": "列",
|
||||
"gridRows": "行",
|
||||
|
|
|
|||
|
|
@ -131,9 +131,9 @@
|
|||
"layoutHorizontal": "Układ poziomy",
|
||||
"layoutVertical": "Układ pionowy",
|
||||
"layoutGrid": "Układ siatki",
|
||||
"layoutGrids": "Siatki układu",
|
||||
"addLayoutGrid": "Dodaj siatkę układu",
|
||||
"removeLayoutGrid": "Usuń siatkę układu",
|
||||
"layoutGrids": "Prowadnice układu",
|
||||
"addLayoutGrid": "Dodaj prowadnicę układu",
|
||||
"removeLayoutGrid": "Usuń prowadnicę układu",
|
||||
"gridGrid": "Siatka",
|
||||
"gridColumns": "Kolumny",
|
||||
"gridRows": "Wiersze",
|
||||
|
|
|
|||
|
|
@ -131,9 +131,9 @@
|
|||
"layoutHorizontal": "Горизонтальная раскладка",
|
||||
"layoutVertical": "Вертикальная раскладка",
|
||||
"layoutGrid": "Сетка",
|
||||
"layoutGrids": "Сетки макета",
|
||||
"addLayoutGrid": "Добавить сетку макета",
|
||||
"removeLayoutGrid": "Удалить сетку макета",
|
||||
"layoutGrids": "Направляющие макета",
|
||||
"addLayoutGrid": "Добавить направляющую макета",
|
||||
"removeLayoutGrid": "Удалить направляющую макета",
|
||||
"gridGrid": "Сетка",
|
||||
"gridColumns": "Столбцы",
|
||||
"gridRows": "Строки",
|
||||
|
|
|
|||
|
|
@ -131,9 +131,9 @@
|
|||
"layoutHorizontal": "水平布局",
|
||||
"layoutVertical": "垂直布局",
|
||||
"layoutGrid": "网格布局",
|
||||
"layoutGrids": "布局网格",
|
||||
"addLayoutGrid": "添加布局网格",
|
||||
"removeLayoutGrid": "移除布局网格",
|
||||
"layoutGrids": "布局参考线",
|
||||
"addLayoutGrid": "添加布局参考线",
|
||||
"removeLayoutGrid": "移除布局参考线",
|
||||
"gridGrid": "网格",
|
||||
"gridColumns": "列",
|
||||
"gridRows": "行",
|
||||
|
|
|
|||
|
|
@ -266,9 +266,9 @@ export const panelMessageDefaults = {
|
|||
resizeAutoWidth: 'Auto width',
|
||||
resizeAutoHeight: 'Auto height',
|
||||
resizeFixed: 'Fixed size',
|
||||
layoutGrids: 'Layout grid',
|
||||
addLayoutGrid: 'Add layout grid',
|
||||
removeLayoutGrid: 'Remove layout grid',
|
||||
layoutGrids: 'Layout guide',
|
||||
addLayoutGrid: 'Add layout guide',
|
||||
removeLayoutGrid: 'Remove layout guide',
|
||||
gridColumns: 'Columns',
|
||||
gridRows: 'Rows',
|
||||
gridGrid: 'Grid',
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import AppearanceSection from './properties/AppearanceSection.vue'
|
|||
import EffectsSection from './properties/EffectsSection.vue'
|
||||
import ExportSection from './properties/ExportSection.vue'
|
||||
import FillSection from './properties/FillSection.vue'
|
||||
import LayoutGridSection from './properties/LayoutSection/LayoutGridSection.vue'
|
||||
import LayoutSection from './properties/LayoutSection/LayoutSection.vue'
|
||||
import MaskSection from './properties/MaskSection.vue'
|
||||
import PageSection from './properties/PageSection.vue'
|
||||
|
|
@ -39,6 +40,10 @@ const isComponentType = computed(() => {
|
|||
return type ? COMPONENT_TYPES.has(type) : false
|
||||
})
|
||||
const selectedIcon = computed(() => (node.value ? nodeIcon(node.value) : undefined))
|
||||
const supportsLayoutGuides = computed(() => {
|
||||
const type = node.value?.type
|
||||
return type === 'FRAME' || type === 'COMPONENT' || type === 'COMPONENT_SET' || type === 'INSTANCE'
|
||||
})
|
||||
const { panels } = useI18n()
|
||||
</script>
|
||||
|
||||
|
|
@ -131,6 +136,7 @@ const { panels } = useI18n()
|
|||
<TypographySection v-if="node.type === 'TEXT'" />
|
||||
<FillSection />
|
||||
<StrokeSection />
|
||||
<LayoutGridSection v-if="supportsLayoutGuides" />
|
||||
<EffectsSection />
|
||||
|
||||
<ExportSection />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n, useLayoutControlsContext, useSceneComputed } from '@open-pencil/vue'
|
||||
import { useEditor, useI18n, useSceneComputed } from '@open-pencil/vue'
|
||||
|
||||
import NumberField from '@/components/inputs/NumberField.vue'
|
||||
import IconButton from '@/components/ui/IconButton.vue'
|
||||
|
|
@ -10,13 +10,14 @@ import PanelFieldGroup from '@/components/ui/panel/PanelFieldGroup.vue'
|
|||
import PanelGrid from '@/components/ui/panel/PanelGrid.vue'
|
||||
import PanelItemRow from '@/components/ui/panel/PanelItemRow.vue'
|
||||
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
||||
import SharedStyleField from '@/components/properties/shared-style/SharedStyleField.vue'
|
||||
|
||||
import type { LayoutGrid } from '@open-pencil/scene-graph'
|
||||
|
||||
const ctx = useLayoutControlsContext()
|
||||
const editor = useEditor()
|
||||
const { panels } = useI18n()
|
||||
|
||||
const selectedNode = useSceneComputed(() => ctx.editor.getSelectedNode() ?? null)
|
||||
const selectedNode = useSceneComputed(() => editor.getSelectedNode() ?? null)
|
||||
const grids = computed<LayoutGrid[]>(() => selectedNode.value?.layoutGrids ?? [])
|
||||
|
||||
const patternOptions = computed(() => [
|
||||
|
|
@ -41,21 +42,21 @@ function defaultGrid(): LayoutGrid {
|
|||
function commit(next: LayoutGrid[], label: string) {
|
||||
const node = selectedNode.value
|
||||
if (!node) return
|
||||
ctx.editor.updateNodeWithUndo(node.id, { layoutGrids: next }, label)
|
||||
editor.updateNodeWithUndo(node.id, { layoutGrids: next }, label)
|
||||
}
|
||||
|
||||
function add() {
|
||||
commit([...grids.value, defaultGrid()], 'Add layout grid')
|
||||
commit([...grids.value, defaultGrid()], 'Add layout guide')
|
||||
}
|
||||
|
||||
function remove(index: number) {
|
||||
commit(
|
||||
grids.value.filter((_, i) => i !== index),
|
||||
'Remove layout grid'
|
||||
'Remove layout guide'
|
||||
)
|
||||
}
|
||||
|
||||
function patch(index: number, changes: Partial<LayoutGrid>, label = 'Edit layout grid') {
|
||||
function patch(index: number, changes: Partial<LayoutGrid>, label = 'Edit layout guide') {
|
||||
commit(
|
||||
grids.value.map((grid, i) => (i === index ? { ...grid, ...changes } : grid)),
|
||||
label
|
||||
|
|
@ -74,6 +75,7 @@ function isGrid(grid: LayoutGrid): boolean {
|
|||
|
||||
<template>
|
||||
<PanelSection :label="panels.layoutGrids" :empty="grids.length === 0">
|
||||
<SharedStyleField kind="grid" :label="panels.gridStyle" />
|
||||
<template #actions>
|
||||
<IconButton :label="panels.addLayoutGrid" @click="add">
|
||||
<icon-lucide-plus class="size-3.5" />
|
||||
|
|
@ -136,7 +138,7 @@ function isGrid(grid: LayoutGrid): boolean {
|
|||
<IconButton
|
||||
:label="panels.toggleVisibility"
|
||||
:active="grid.visible === false"
|
||||
@click="patch(index, { visible: grid.visible === false }, 'Toggle layout grid')"
|
||||
@click="patch(index, { visible: grid.visible === false }, 'Toggle layout guide')"
|
||||
>
|
||||
<icon-lucide-eye-off v-if="grid.visible === false" class="size-3.5" />
|
||||
<icon-lucide-eye v-else class="size-3.5" />
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ import AutoLayoutControls from '@/components/properties/LayoutSection/AutoLayout
|
|||
import ClipContentControl from '@/components/properties/LayoutSection/ClipContentControl.vue'
|
||||
import FlexControls from '@/components/properties/LayoutSection/FlexControls.vue'
|
||||
import GridControls from '@/components/properties/LayoutSection/GridControls.vue'
|
||||
import LayoutGridSection from '@/components/properties/LayoutSection/LayoutGridSection.vue'
|
||||
import IconButton from '@/components/ui/IconButton.vue'
|
||||
import PaddingControls from '@/components/properties/LayoutSection/PaddingControls.vue'
|
||||
import SizeControls from '@/components/properties/LayoutSection/size/SizeControls.vue'
|
||||
import TextResizingControl from '@/components/properties/LayoutSection/TextResizingControl.vue'
|
||||
import SharedStyleField from '@/components/properties/shared-style/SharedStyleField.vue'
|
||||
import PanelSection from '@/components/ui/panel/PanelSection.vue'
|
||||
|
||||
const { panels } = useI18n()
|
||||
|
|
@ -21,45 +19,37 @@ const CONTAINER_TYPES = ['FRAME', 'COMPONENT', 'COMPONENT_SET', 'INSTANCE']
|
|||
<template>
|
||||
<LayoutControlsRoot v-slot="ctx">
|
||||
<template v-if="ctx.node">
|
||||
<PanelSection :label="panels.layout">
|
||||
<SharedStyleField kind="grid" :label="panels.gridStyle" />
|
||||
<PanelSection :label="ctx.node.layoutMode === 'NONE' ? panels.layout : panels.autoLayout">
|
||||
<template v-if="CONTAINER_TYPES.includes(ctx.node.type)" #actions>
|
||||
<IconButton
|
||||
v-if="ctx.node.layoutMode === 'NONE'"
|
||||
:label="panels.addAutoLayout"
|
||||
@click="ctx.editor.setLayoutMode(ctx.node.id, 'VERTICAL')"
|
||||
>
|
||||
<icon-lucide-plus class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-else
|
||||
:label="panels.removeAutoLayout"
|
||||
@click="ctx.editor.setLayoutMode(ctx.node.id, 'NONE')"
|
||||
>
|
||||
<icon-lucide-minus class="size-3.5" />
|
||||
</IconButton>
|
||||
</template>
|
||||
|
||||
<AutoLayoutControls v-if="CONTAINER_TYPES.includes(ctx.node.type)" />
|
||||
<TextResizingControl v-if="ctx.node.type === 'TEXT'" />
|
||||
<SizeControls />
|
||||
|
||||
<template v-if="CONTAINER_TYPES.includes(ctx.node.type) && ctx.node.layoutMode !== 'NONE'">
|
||||
<FlexControls v-if="ctx.isFlex" />
|
||||
<template v-if="ctx.isGrid">
|
||||
<GridControls />
|
||||
<PaddingControls />
|
||||
<ClipContentControl />
|
||||
</template>
|
||||
</template>
|
||||
</PanelSection>
|
||||
|
||||
<template v-if="CONTAINER_TYPES.includes(ctx.node.type)">
|
||||
<PanelSection :label="panels.autoLayout">
|
||||
<template #actions>
|
||||
<IconButton
|
||||
v-if="ctx.node.layoutMode === 'NONE'"
|
||||
:label="panels.addAutoLayout"
|
||||
@click="ctx.editor.setLayoutMode(ctx.node.id, 'VERTICAL')"
|
||||
>
|
||||
<icon-lucide-plus class="size-3.5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-else
|
||||
:label="panels.removeAutoLayout"
|
||||
@click="ctx.editor.setLayoutMode(ctx.node.id, 'NONE')"
|
||||
>
|
||||
<icon-lucide-minus class="size-3.5" />
|
||||
</IconButton>
|
||||
</template>
|
||||
|
||||
<AutoLayoutControls />
|
||||
|
||||
<template v-if="ctx.node.layoutMode !== 'NONE'">
|
||||
<FlexControls v-if="ctx.isFlex" />
|
||||
<template v-if="ctx.isGrid">
|
||||
<GridControls />
|
||||
<PaddingControls />
|
||||
<ClipContentControl />
|
||||
</template>
|
||||
</template>
|
||||
</PanelSection>
|
||||
|
||||
<LayoutGridSection />
|
||||
</template>
|
||||
</template>
|
||||
</LayoutControlsRoot>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ test('Shift+A wraps selection in auto-layout frame', async () => {
|
|||
expect(expectDefined(node, 'node').childIds.length).toBe(2)
|
||||
|
||||
frameId = expectDefined(node, 'node').id
|
||||
await expect(propertySection(page, 'Layout')).toHaveScreenshot('layout-size-controls.png')
|
||||
await expect(propertySection(page, 'Auto layout')).toHaveScreenshot('layout-size-controls.png')
|
||||
await expect(propertySection(page, 'Layout')).toHaveCount(0)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
|
|
@ -158,7 +159,7 @@ test('padding controls set horizontal and vertical padding pairs', async () => {
|
|||
test('size dropdown adds and removes min width', async () => {
|
||||
await selectFrame()
|
||||
|
||||
const layout = propertySection(page, 'Layout')
|
||||
const layout = propertySection(page, 'Auto layout')
|
||||
await propertyField(page, 'width').getByRole('combobox', { name: 'Width' }).click()
|
||||
await page.getByRole('option', { name: 'Add min width' }).click()
|
||||
await canvas.waitForRender()
|
||||
|
|
@ -274,5 +275,7 @@ test('remove auto-layout sets layoutMode to NONE', async () => {
|
|||
|
||||
const frame = await getNodeById(page, frameId)
|
||||
expect(expectDefined(frame, 'frame').layoutMode).toBe('NONE')
|
||||
await expect(propertySection(page, 'Layout')).toBeVisible()
|
||||
await expect(propertySection(page, 'Auto layout')).toHaveCount(0)
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 9.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
|
@ -114,7 +114,7 @@ test('applies fill, stroke, effect, and grid styles from local definitions', asy
|
|||
await chooseStyle('Fill', 'Fill style', 'Brand/Primary')
|
||||
await chooseStyle('Stroke', 'Stroke style', 'Brand/Primary')
|
||||
await chooseStyle('Effects', 'Effect style', 'Effects/Card')
|
||||
await chooseStyle('Layout', 'Grid style', 'Grid/12 columns')
|
||||
await chooseStyle('Layout guide', 'Grid style', 'Grid/12 columns')
|
||||
|
||||
expect(await targetStyles()).toMatchObject({
|
||||
fillStyleId: '1:100',
|
||||
|
|
|
|||
Loading…
Reference in a new issue