feat(layout): layout-grid editor section

Add a Layout grid section for frames/components with pattern (columns/rows/
grid), count, gutter/section size, and margin, plus visibility and remove
actions. The canvas already rendered layout grids from imported files; this
wires creation and editing through the standard panel list pattern with
undo support and icon+tooltip pattern options.
This commit is contained in:
Danila Poyarkov 2026-07-20 15:09:28 +03:00
parent 14d4a17a33
commit efa784a039
3 changed files with 161 additions and 0 deletions

View file

@ -250,6 +250,16 @@ export const panelMessageDefaults = {
resizeAutoWidth: 'Auto width',
resizeAutoHeight: 'Auto height',
resizeFixed: 'Fixed size',
layoutGrids: 'Layout grid',
addLayoutGrid: 'Add layout grid',
removeLayoutGrid: 'Remove layout grid',
gridColumns: 'Columns',
gridRows: 'Rows',
gridGrid: 'Grid',
gridCount: 'Count',
gridGutter: 'Gutter',
gridMargin: 'Margin',
gridSectionSize: 'Section size',
searchFonts: 'Search fonts...'
} as const

View file

@ -0,0 +1,148 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n, useLayoutControlsContext, useSceneComputed } from '@open-pencil/vue'
import NumberField from '@/components/inputs/NumberField.vue'
import IconButton from '@/components/ui/IconButton.vue'
import SegmentedControl from '@/components/ui/SegmentedControl.vue'
import Tip from '@/components/ui/Tip.vue'
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 type { LayoutGrid } from '@open-pencil/scene-graph'
const ctx = useLayoutControlsContext()
const { panels } = useI18n()
const selectedNode = useSceneComputed(() => ctx.editor.getSelectedNode() ?? null)
const grids = computed<LayoutGrid[]>(() => selectedNode.value?.layoutGrids ?? [])
const patternOptions = computed(() => [
{ value: 'COLUMNS' as const, label: panels.value.gridColumns },
{ value: 'ROWS' as const, label: panels.value.gridRows },
{ value: 'GRID' as const, label: panels.value.gridGrid }
])
function defaultGrid(): LayoutGrid {
return {
visible: true,
color: { r: 1, g: 0, b: 0, a: 0.1 },
pattern: 'COLUMNS',
alignment: 'STRETCH',
count: 5,
gutterSize: 20,
offset: 0,
sectionSize: 0
}
}
function commit(next: LayoutGrid[], label: string) {
const node = selectedNode.value
if (!node) return
ctx.editor.updateNodeWithUndo(node.id, { layoutGrids: next }, label)
}
function add() {
commit([...grids.value, defaultGrid()], 'Add layout grid')
}
function remove(index: number) {
commit(
grids.value.filter((_, i) => i !== index),
'Remove layout grid'
)
}
function patch(index: number, changes: Partial<LayoutGrid>, label = 'Edit layout grid') {
commit(
grids.value.map((grid, i) => (i === index ? { ...grid, ...changes } : grid)),
label
)
}
function gridPattern(grid: LayoutGrid): 'COLUMNS' | 'ROWS' | 'GRID' {
if (grid.pattern) return grid.pattern
return grid.axis === 'Y' ? 'ROWS' : 'COLUMNS'
}
function isGrid(grid: LayoutGrid): boolean {
return gridPattern(grid) === 'GRID'
}
</script>
<template>
<PanelSection :label="panels.layoutGrids" :empty="grids.length === 0">
<template #actions>
<IconButton :label="panels.addLayoutGrid" @click="add">
<icon-lucide-plus class="size-3.5" />
</IconButton>
</template>
<PanelItemRow v-for="(grid, index) in grids" :key="index" class="items-start">
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
<SegmentedControl
:model-value="gridPattern(grid)"
:options="patternOptions"
:label="panels.layoutGrids"
@change="patch(index, { pattern: $event as LayoutGrid['pattern'] }, 'Change grid pattern')"
>
<template #option="{ option }">
<Tip :label="option.label" class="flex items-center justify-center">
<icon-lucide-columns-3 v-if="option.value === 'COLUMNS'" class="size-3.5" />
<icon-lucide-rows-3 v-else-if="option.value === 'ROWS'" class="size-3.5" />
<icon-lucide-layout-grid v-else class="size-3.5" />
</Tip>
</template>
</SegmentedControl>
<PanelGrid columns="two">
<PanelFieldGroup :label="panels.gridCount">
<NumberField
:model-value="grid.count ?? grid.numSections ?? 1"
:min="1"
:aria-label="panels.gridCount"
@update:model-value="patch(index, { count: $event })"
/>
</PanelFieldGroup>
<PanelFieldGroup v-if="!isGrid(grid)" :label="panels.gridGutter">
<NumberField
:model-value="grid.gutterSize ?? 0"
:min="0"
:aria-label="panels.gridGutter"
@update:model-value="patch(index, { gutterSize: $event })"
/>
</PanelFieldGroup>
<PanelFieldGroup v-if="isGrid(grid)" :label="panels.gridSectionSize">
<NumberField
:model-value="grid.sectionSize ?? 0"
:min="1"
:aria-label="panels.gridSectionSize"
@update:model-value="patch(index, { sectionSize: $event })"
/>
</PanelFieldGroup>
<PanelFieldGroup :label="panels.gridMargin">
<NumberField
:model-value="grid.offset ?? 0"
:aria-label="panels.gridMargin"
@update:model-value="patch(index, { offset: $event })"
/>
</PanelFieldGroup>
</PanelGrid>
</div>
<template #rail>
<IconButton
:label="panels.toggleVisibility"
:active="grid.visible === false"
@click="patch(index, { visible: grid.visible === false }, 'Toggle layout grid')"
>
<icon-lucide-eye-off v-if="grid.visible === false" class="size-3.5" />
<icon-lucide-eye v-else class="size-3.5" />
</IconButton>
<IconButton :label="panels.removeLayoutGrid" @click="remove(index)">
<icon-lucide-minus class="size-3.5" />
</IconButton>
</template>
</PanelItemRow>
</PanelSection>
</template>

View file

@ -5,6 +5,7 @@ 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 PaddingControls from '@/components/properties/LayoutSection/PaddingControls.vue'
import SizeControls from '@/components/properties/LayoutSection/size/SizeControls.vue'
import TextResizingControl from '@/components/properties/LayoutSection/TextResizingControl.vue'
@ -38,6 +39,8 @@ const CONTAINER_TYPES = ['FRAME', 'COMPONENT', 'COMPONENT_SET', 'INSTANCE']
</template>
</template>
</PanelSection>
<LayoutGridSection />
</template>
</template>
</LayoutControlsRoot>