fix(app): add size limit dropdowns

This commit is contained in:
Danila Poyarkov 2026-05-01 10:50:25 +03:00
parent 4301a92a5e
commit 7b0ae8fa2a
6 changed files with 123 additions and 45 deletions

View file

@ -180,6 +180,13 @@ export function createLayoutActions({
editor.updateNode(node.value.id, { [prop]: value })
}
function setSizeLimitToCurrent(prop: SizeLimitProp) {
const n = node.value
if (!n) return
const value = prop === 'minWidth' || prop === 'maxWidth' ? n.width : n.height
editor.updateNodeWithUndo(n.id, { [prop]: Math.round(value) }, `Set ${prop}`)
}
function commitSizeLimit(prop: SizeLimitProp, _value: number, previous: number) {
if (!node.value) return
editor.commitNodeUpdate(node.value.id, { [prop]: previous }, `Change ${prop}`)
@ -258,6 +265,7 @@ export function createLayoutActions({
return {
updateProp,
updateSizeLimit,
setSizeLimitToCurrent,
commitSizeLimit,
addSizeLimit,
removeSizeLimit,

View file

@ -63,6 +63,7 @@ export function useLayout() {
trackSizingOptions: createTrackSizingOptions(panels.value),
updateProp: layoutActions.updateProp,
updateSizeLimit: layoutActions.updateSizeLimit,
setSizeLimitToCurrent: layoutActions.setSizeLimitToCurrent,
commitSizeLimit: layoutActions.commitSizeLimit,
addSizeLimit: layoutActions.addSizeLimit,
removeSizeLimit: layoutActions.removeSizeLimit,

View file

@ -221,6 +221,8 @@ export const panelMessages = i18n('panels', {
maxWidthShort: 'Max W',
minHeightShort: 'Min H',
maxHeightShort: 'Max H',
setToCurrentWidth: 'Set to current width',
setToCurrentHeight: 'Set to current height',
sizingFillFr: 'Fill (fr)',
sizingFixedPx: 'Fixed (px)'
})

View file

@ -30,6 +30,7 @@ provideLayoutControls(
:track-sizing-options="ctx.trackSizingOptions"
:update-prop="ctx.updateProp"
:update-size-limit="ctx.updateSizeLimit"
:set-size-limit-to-current="ctx.setSizeLimitToCurrent"
:commit-size-limit="ctx.commitSizeLimit"
:add-size-limit="ctx.addSizeLimit"
:remove-size-limit="ctx.removeSizeLimit"

View file

@ -20,9 +20,24 @@ import type { SizeLimitProp } from '@open-pencil/vue'
type SizeSelectValue = LayoutSizing | `add-${SizeLimitProp}` | `remove-${SizeLimitProp}`
type ActiveSizeLimit = {
prop: SizeLimitProp
testId: string
icon: () => string
value: () => number | null
setLabel: () => string
removeLabel: () => string
}
const ctx = useLayoutControlsContext()
const widthFieldRef = ref<HTMLElement | null>(null)
const heightFieldRef = ref<HTMLElement | null>(null)
const limitFieldRefs = ref<Record<SizeLimitProp, HTMLElement | null>>({
minWidth: null,
maxWidth: null,
minHeight: null,
maxHeight: null
})
const { panels } = useI18n()
const sizingSelect = useSelectUI({ item: 'rounded py-1.5 pr-2 pl-6 text-xs' })
@ -40,6 +55,41 @@ const widthLimitItems = [
}
]
const activeSizeLimits: ActiveSizeLimit[] = [
{
prop: 'minWidth',
testId: 'layout-min-width-input',
icon: () => panels.value.minWidthShort,
value: () => ctx.node.minWidth,
setLabel: () => panels.value.setToCurrentWidth,
removeLabel: () => panels.value.removeMinWidth
},
{
prop: 'maxWidth',
testId: 'layout-max-width-input',
icon: () => panels.value.maxWidthShort,
value: () => ctx.node.maxWidth,
setLabel: () => panels.value.setToCurrentWidth,
removeLabel: () => panels.value.removeMaxWidth
},
{
prop: 'minHeight',
testId: 'layout-min-height-input',
icon: () => panels.value.minHeightShort,
value: () => ctx.node.minHeight,
setLabel: () => panels.value.setToCurrentHeight,
removeLabel: () => panels.value.removeMinHeight
},
{
prop: 'maxHeight',
testId: 'layout-max-height-input',
icon: () => panels.value.maxHeightShort,
value: () => ctx.node.maxHeight,
setLabel: () => panels.value.setToCurrentHeight,
removeLabel: () => panels.value.removeMaxHeight
}
]
const heightLimitItems = [
{
prop: 'minHeight' as const,
@ -63,6 +113,11 @@ function anchorRef(element: HTMLElement | null): HTMLElement | undefined {
return element ?? undefined
}
function handleLimitSelect(prop: SizeLimitProp, value: string) {
if (value === 'CURRENT') ctx.setSizeLimitToCurrent(prop)
else if (value === 'REMOVE') ctx.removeSizeLimit(prop)
}
function handleSizeSelect(axis: 'width' | 'height', value: SizeSelectValue) {
if (value === 'FIXED' || value === 'HUG' || value === 'FILL') {
if (axis === 'width') ctx.setWidthSizing(value)
@ -210,41 +265,54 @@ function handleSizeSelect(axis: 'width' | 'height', value: SizeSelectValue) {
"
class="mt-1.5 grid grid-cols-2 gap-1.5"
>
<ScrubInput
v-if="ctx.node.minWidth != null"
data-test-id="layout-min-width-input"
:icon="panels.minWidthShort"
:model-value="Math.round(ctx.node.minWidth)"
:min="0"
@update:model-value="ctx.updateSizeLimit('minWidth', $event)"
@commit="(v: number, p: number) => ctx.commitSizeLimit('minWidth', v, p)"
/>
<ScrubInput
v-if="ctx.node.maxWidth != null"
data-test-id="layout-max-width-input"
:icon="panels.maxWidthShort"
:model-value="Math.round(ctx.node.maxWidth)"
:min="0"
@update:model-value="ctx.updateSizeLimit('maxWidth', $event)"
@commit="(v: number, p: number) => ctx.commitSizeLimit('maxWidth', v, p)"
/>
<ScrubInput
v-if="ctx.node.minHeight != null"
data-test-id="layout-min-height-input"
:icon="panels.minHeightShort"
:model-value="Math.round(ctx.node.minHeight)"
:min="0"
@update:model-value="ctx.updateSizeLimit('minHeight', $event)"
@commit="(v: number, p: number) => ctx.commitSizeLimit('minHeight', v, p)"
/>
<ScrubInput
v-if="ctx.node.maxHeight != null"
data-test-id="layout-max-height-input"
:icon="panels.maxHeightShort"
:model-value="Math.round(ctx.node.maxHeight)"
:min="0"
@update:model-value="ctx.updateSizeLimit('maxHeight', $event)"
@commit="(v: number, p: number) => ctx.commitSizeLimit('maxHeight', v, p)"
/>
<template v-for="item in activeSizeLimits" :key="item.prop">
<div
v-if="item.value() != null"
:ref="(el) => (limitFieldRefs[item.prop] = el as HTMLElement | null)"
class="min-w-0"
>
<ScrubInput
:data-test-id="item.testId"
:icon="item.icon()"
:model-value="Math.round(item.value() ?? 0)"
:min="0"
@update:model-value="ctx.updateSizeLimit(item.prop, $event)"
@commit="(v: number, p: number) => ctx.commitSizeLimit(item.prop, v, p)"
>
<template #suffix>
<SelectRoot
:model-value="'VALUE'"
@update:model-value="(value) => handleLimitSelect(item.prop, value as string)"
>
<SelectTrigger
:data-test-id="`${item.testId}-menu`"
:reference="anchorRef(limitFieldRefs[item.prop])"
class="flex shrink-0 cursor-pointer items-center self-stretch border-none bg-transparent px-1 text-[11px] text-muted outline-none"
@pointerdown.stop
>
<icon-lucide-chevron-down class="size-3" />
</SelectTrigger>
<SelectPortal>
<SelectContent
position="popper"
align="start"
:side-offset="4"
:class="sizingSelect.content"
>
<SelectViewport class="p-0.5">
<SelectItem value="CURRENT" :class="sizingSelect.item">
<SelectItemText>{{ item.setLabel() }}</SelectItemText>
</SelectItem>
<SelectItem value="REMOVE" :class="sizingSelect.item">
<SelectItemText>{{ item.removeLabel() }}</SelectItemText>
</SelectItem>
</SelectViewport>
</SelectContent>
</SelectPortal>
</SelectRoot>
</template>
</ScrubInput>
</div>
</template>
</div>
</template>

View file

@ -27,8 +27,12 @@ export class CanvasHelper {
}
async waitForInit() {
await this.page.locator('canvas[data-ready="1"]').waitFor({ timeout: 30000 })
await this.page.locator('[data-test-id="canvas-loading"]').waitFor({ state: 'hidden', timeout: 30000 })
await this.page
.locator('[data-test-id="canvas-element"][data-ready="1"]')
.waitFor({ timeout: 30000 })
await this.page
.locator('[data-test-id="canvas-loading"]')
.waitFor({ state: 'hidden', timeout: 30000 })
await this.page.locator('#loader').waitFor({ state: 'detached', timeout: 30000 })
}
@ -53,13 +57,7 @@ export class CanvasHelper {
await this.page.mouse.click(box.x + canvasX, box.y + canvasY)
}
async drag(
fromX: number,
fromY: number,
toX: number,
toY: number,
steps = 10
) {
async drag(fromX: number, fromY: number, toX: number, toY: number, steps = 10) {
const box = await this.canvasBounds()
await this.page.mouse.move(box.x + fromX, box.y + fromY)
await this.page.mouse.down()