feat(app): expose wrap cross axis gap

This commit is contained in:
Danila Poyarkov 2026-04-30 19:27:01 +03:00
parent 806c512a86
commit 84124721ff
4 changed files with 76 additions and 30 deletions

View file

@ -199,6 +199,8 @@ export const panelMessages = i18n('panels', {
direction: 'Direction',
flow: 'Flow',
gapAuto: 'Auto gap',
horizontalGap: 'Horizontal gap',
verticalGap: 'Vertical gap',
auto: 'Auto',
columns: 'Columns',
rows: 'Rows',

View file

@ -26,36 +26,62 @@ const { panels } = useI18n()
</div>
<div class="mt-2 flex items-center gap-1.5">
<button
v-if="ctx.gapAuto"
data-test-id="layout-gap-input"
class="group flex h-[26px] min-w-0 flex-1 cursor-pointer items-center rounded border border-accent/50 bg-accent/10 text-xs text-accent hover:bg-accent/15"
@click="ctx.setGapAuto(false)"
>
<span class="flex shrink-0 items-center justify-center self-stretch px-[5px] text-accent/80">
{{ ctx.node.layoutMode === 'VERTICAL' ? '↕' : '↔' }}
</span>
<span class="flex-1 truncate text-left">{{ panels.auto }}</span>
</button>
<ScrubInput
v-else
data-test-id="layout-gap-input"
class="flex-1"
:icon="ctx.node.layoutMode === 'VERTICAL' ? '↕' : '↔'"
:model-value="Math.round(ctx.node.itemSpacing)"
:min="0"
@update:model-value="ctx.updateProp('itemSpacing', $event)"
@commit="(v: number, p: number) => ctx.commitProp('itemSpacing', v, p)"
/>
<button
data-test-id="layout-gap-auto-toggle"
:title="panels.gapAuto"
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-transparent text-[10px] text-muted hover:bg-hover hover:text-surface"
:class="ctx.gapAuto ? 'border-accent bg-accent/10 text-accent' : ''"
@click="ctx.setGapAuto(!ctx.gapAuto)"
>
A
</button>
<template v-if="ctx.node.layoutWrap === 'WRAP'">
<ScrubInput
data-test-id="layout-gap-input"
class="min-w-0 flex-1"
:icon="ctx.node.layoutMode === 'VERTICAL' ? '↕' : '↔'"
:label="ctx.node.layoutMode === 'VERTICAL' ? panels.verticalGap : panels.horizontalGap"
:model-value="Math.round(ctx.node.itemSpacing)"
:min="0"
@update:model-value="ctx.updateProp('itemSpacing', $event)"
@commit="(v: number, p: number) => ctx.commitProp('itemSpacing', v, p)"
/>
<ScrubInput
data-test-id="layout-cross-gap-input"
class="min-w-0 flex-1"
:icon="ctx.node.layoutMode === 'VERTICAL' ? '↔' : '↕'"
:label="ctx.node.layoutMode === 'VERTICAL' ? panels.horizontalGap : panels.verticalGap"
:model-value="Math.round(ctx.node.counterAxisSpacing)"
:min="0"
@update:model-value="ctx.updateProp('counterAxisSpacing', $event)"
@commit="(v: number, p: number) => ctx.commitProp('counterAxisSpacing', v, p)"
/>
</template>
<template v-else>
<button
v-if="ctx.gapAuto"
data-test-id="layout-gap-input"
class="group flex h-[26px] min-w-0 flex-1 cursor-pointer items-center rounded border border-accent/50 bg-accent/10 text-xs text-accent hover:bg-accent/15"
@click="ctx.setGapAuto(false)"
>
<span
class="flex shrink-0 items-center justify-center self-stretch px-[5px] text-accent/80"
>
{{ ctx.node.layoutMode === 'VERTICAL' ? '↕' : '↔' }}
</span>
<span class="flex-1 truncate text-left">{{ panels.auto }}</span>
</button>
<ScrubInput
v-else
data-test-id="layout-gap-input"
class="flex-1"
:icon="ctx.node.layoutMode === 'VERTICAL' ? '↕' : '↔'"
:model-value="Math.round(ctx.node.itemSpacing)"
:min="0"
@update:model-value="ctx.updateProp('itemSpacing', $event)"
@commit="(v: number, p: number) => ctx.commitProp('itemSpacing', v, p)"
/>
<button
data-test-id="layout-gap-auto-toggle"
:title="panels.gapAuto"
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-transparent text-[10px] text-muted hover:bg-hover hover:text-surface"
:class="ctx.gapAuto ? 'border-accent bg-accent/10 text-accent' : ''"
@click="ctx.setGapAuto(!ctx.gapAuto)"
>
A
</button>
</template>
<button
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-transparent text-muted hover:bg-hover hover:text-surface"
@click="ctx.toggleIndividualPadding"

View file

@ -91,6 +91,22 @@ test('gap auto toggle sets space-between alignment', async () => {
canvas.assertNoErrors()
})
test('wrap mode exposes cross-axis gap control', async () => {
await selectFrame()
await page.locator('[data-test-id="layout-direction-wrap"]').click()
await canvas.waitForRender()
const before = await getNodeById(page, frameId)
const initialSpacing = before!.counterAxisSpacing
await canvas.dragScrubInput(page.locator('[data-test-id="layout-cross-gap-input"]'), 40)
const after = await getNodeById(page, frameId)
expect(after!.layoutWrap).toBe('WRAP')
expect(after!.counterAxisSpacing).toBeGreaterThan(initialSpacing + 5)
canvas.assertNoErrors()
})
test('uniform padding ScrubInput sets all four padding sides', async () => {
await selectFrame()

View file

@ -68,6 +68,8 @@ export function getNodeById(page: Page, id: string) {
primaryAxisAlign: n.primaryAxisAlign,
counterAxisAlign: n.counterAxisAlign,
itemSpacing: n.itemSpacing,
counterAxisSpacing: n.counterAxisSpacing,
layoutWrap: n.layoutWrap,
paddingTop: n.paddingTop,
paddingRight: n.paddingRight,
paddingBottom: n.paddingBottom,