openpencil/src/components/properties/LayoutSection/FlexControls.vue
2026-07-26 11:15:57 +03:00

241 lines
9.3 KiB
Vue

<script setup lang="ts">
import { ref } from 'vue'
import { tv } from 'tailwind-variants'
import {
SelectContent,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectPortal,
SelectRoot,
SelectTrigger,
SelectViewport
} from 'reka-ui'
import AppSelect from '@/components/ui/AppSelect.vue'
import layoutAlignmentTheme from '@/theme/layout-alignment'
import VariableNumberField from '@/components/properties/VariableNumberField.vue'
import ClipContentControl from '@/components/properties/LayoutSection/ClipContentControl.vue'
import PaddingControls from '@/components/properties/LayoutSection/PaddingControls.vue'
import { useSelectUI } from '@/components/ui/select'
import { useI18n, useLayoutControlsContext } from '@open-pencil/vue'
import type { LayoutDirection, LayoutAlign } from '@open-pencil/scene-graph'
const ctx = useLayoutControlsContext()
const layoutAlignment = tv(layoutAlignmentTheme)
const alignmentStyles = layoutAlignment()
const gapFieldRef = ref<HTMLElement | null>(null)
const { panels } = useI18n()
const gapSelect = useSelectUI({ item: 'rounded py-1.5 pr-2 pl-6 text-xs' })
function anchorRef(element: HTMLElement | null): HTMLElement | undefined {
return element ?? undefined
}
function setGapMode(value: string) {
ctx.setGapAuto(value === 'AUTO')
}
function isAlignmentActive(primary: LayoutAlign, counter: string) {
if (ctx.gapAuto)
return ctx.node.primaryAxisAlign === 'SPACE_BETWEEN' && ctx.node.counterAxisAlign === counter
return ctx.node.primaryAxisAlign === primary && ctx.node.counterAxisAlign === counter
}
function alignmentCellClass(primary: LayoutAlign, counter: string) {
return layoutAlignment({ active: isAlignmentActive(primary, counter) }).cell()
}
</script>
<template>
<div class="mt-2">
<label class="mb-1 block text-[11px] text-muted">{{ panels.direction }}</label>
<AppSelect
:model-value="ctx.layoutDirection"
:options="[
{ value: 'AUTO', label: panels.auto },
{ value: 'LTR', label: 'LTR' },
{ value: 'RTL', label: 'RTL' }
]"
@update:model-value="ctx.setLayoutDirection($event as LayoutDirection)"
/>
</div>
<div class="mt-2 flex items-center gap-1.5">
<template v-if="ctx.node.layoutWrap === 'WRAP'">
<VariableNumberField
data-test-id="layout-gap-input"
class="min-w-0 flex-1"
:label="ctx.node.layoutMode === 'VERTICAL' ? panels.verticalGap : panels.horizontalGap"
:model-value="Math.round(ctx.node.itemSpacing)"
:min="0"
:node-id="ctx.node.id"
binding-path="itemSpacing"
@update:model-value="ctx.updateProp('itemSpacing', $event)"
@commit="(v: number, p: number) => ctx.commitProp('itemSpacing', v, p)"
>
<template #icon>
<icon-lucide-align-vertical-space-between
v-if="ctx.node.layoutMode === 'VERTICAL'"
class="size-3.5"
/>
<icon-lucide-align-horizontal-space-between v-else class="size-3.5" />
</template>
</VariableNumberField>
<VariableNumberField
data-test-id="layout-cross-gap-input"
class="min-w-0 flex-1"
:label="ctx.node.layoutMode === 'VERTICAL' ? panels.horizontalGap : panels.verticalGap"
:model-value="Math.round(ctx.node.counterAxisSpacing)"
:min="0"
:node-id="ctx.node.id"
binding-path="counterAxisSpacing"
@update:model-value="ctx.updateProp('counterAxisSpacing', $event)"
@commit="(v: number, p: number) => ctx.commitProp('counterAxisSpacing', v, p)"
>
<template #icon>
<icon-lucide-align-horizontal-space-between
v-if="ctx.node.layoutMode === 'VERTICAL'"
class="size-3.5"
/>
<icon-lucide-align-vertical-space-between v-else class="size-3.5" />
</template>
</VariableNumberField>
</template>
<template v-else>
<div
v-if="ctx.gapAuto"
ref="gapFieldRef"
data-test-id="layout-gap-input"
class="group flex h-[26px] min-w-0 flex-1 items-center rounded border border-border bg-input focus-within:border-accent"
>
<span class="flex shrink-0 items-center justify-center self-stretch px-[5px] text-muted">
<icon-lucide-align-vertical-space-between
v-if="ctx.node.layoutMode === 'VERTICAL'"
class="size-3.5"
/>
<icon-lucide-align-horizontal-space-between v-else class="size-3.5" />
</span>
<span class="flex-1 truncate text-xs text-surface">{{ panels.auto }}</span>
<SelectRoot :model-value="'AUTO'" @update:model-value="setGapMode">
<SelectTrigger
data-test-id="layout-gap-menu"
:reference="anchorRef(gapFieldRef)"
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="gapSelect.content"
>
<SelectViewport class="p-0.5">
<SelectItem value="FIXED" :class="gapSelect.item">
<SelectItemText>{{ Math.round(ctx.node.itemSpacing) }}</SelectItemText>
</SelectItem>
<SelectItem value="AUTO" :class="gapSelect.item">
<SelectItemIndicator
class="absolute left-1.5 inline-flex items-center justify-center"
>
<icon-lucide-check class="size-3 text-accent" />
</SelectItemIndicator>
<SelectItemText>{{ panels.auto }}</SelectItemText>
</SelectItem>
</SelectViewport>
</SelectContent>
</SelectPortal>
</SelectRoot>
</div>
<div v-else ref="gapFieldRef" class="min-w-0 flex-1">
<VariableNumberField
data-test-id="layout-gap-input"
class="w-full"
:model-value="Math.round(ctx.node.itemSpacing)"
:min="0"
:node-id="ctx.node.id"
binding-path="itemSpacing"
@update:model-value="ctx.updateProp('itemSpacing', $event)"
@commit="(v: number, p: number) => ctx.commitProp('itemSpacing', v, p)"
>
<template #icon>
<icon-lucide-align-vertical-space-between
v-if="ctx.node.layoutMode === 'VERTICAL'"
class="size-3.5"
/>
<icon-lucide-align-horizontal-space-between v-else class="size-3.5" />
</template>
<template #after-variable>
<SelectRoot :model-value="'FIXED'" @update:model-value="setGapMode">
<SelectTrigger
data-test-id="layout-gap-menu"
:reference="anchorRef(gapFieldRef)"
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="gapSelect.content"
>
<SelectViewport class="p-0.5">
<SelectItem value="FIXED" :class="gapSelect.item">
<SelectItemIndicator
class="absolute left-1.5 inline-flex items-center justify-center"
>
<icon-lucide-check class="size-3 text-accent" />
</SelectItemIndicator>
<SelectItemText>{{ Math.round(ctx.node.itemSpacing) }}</SelectItemText>
</SelectItem>
<SelectItem value="AUTO" :class="gapSelect.item">
<SelectItemText>{{ panels.auto }}</SelectItemText>
</SelectItem>
</SelectViewport>
</SelectContent>
</SelectPortal>
</SelectRoot>
</template>
</VariableNumberField>
</div>
</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"
>
<icon-lucide-minus
v-if="ctx.showIndividualPadding || !ctx.hasUniformPadding"
class="size-3"
/>
<icon-lucide-plus v-else class="size-3" />
</button>
</div>
<PaddingControls />
<ClipContentControl />
<div class="mt-2">
<label class="mb-1 block text-[11px] text-muted">{{ panels.alignment }}</label>
<div data-test-id="layout-alignment-grid" :class="alignmentStyles.grid()">
<button
v-for="cell in ctx.alignGrid"
:key="`${cell.primary}-${cell.counter}`"
:data-active="isAlignmentActive(cell.primary, cell.counter) || undefined"
:class="alignmentCellClass(cell.primary, cell.counter)"
@click="ctx.setAlignment(ctx.gapAuto ? 'SPACE_BETWEEN' : cell.primary, cell.counter)"
>
<span :class="alignmentStyles.dot()" />
</button>
</div>
</div>
</template>