241 lines
9.3 KiB
Vue
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>
|