openpencil/src/components/properties/StrokeSection.vue
2026-03-29 18:54:14 +03:00

203 lines
6.7 KiB
Vue

<script setup lang="ts">
import { ref } from 'vue'
import {
applySolidStrokeColor,
PropertyListRoot,
useColorVariableBinding,
useStrokeControls,
useOkHCL,
useI18n
} from '@open-pencil/vue'
import ColorStyleRow from '@/components/properties/ColorStyleRow.vue'
import AppSelect from '@/components/ui/AppSelect.vue'
import ColorInput from '@/components/ColorInput.vue'
import ScrubInput from '@/components/ScrubInput.vue'
import Tip from '@/components/ui/Tip.vue'
import { iconButton } from '@/components/ui/icon-button'
import { sectionLabel, sectionWrapper } from '@/components/ui/section'
import type { SceneNode, Stroke } from '@open-pencil/core'
const strokeCtx = useStrokeControls()
const strokeVarCtx = useColorVariableBinding('strokes')
const okhcl = useOkHCL()
const { panels } = useI18n()
const expandedSides = ref(false)
function onToggleSides(activeNode: SceneNode) {
const next = !expandedSides.value
expandedSides.value = next
if (next && !activeNode.independentStrokeWeights) {
const weight = activeNode.strokes[0]?.weight ?? 1
strokeCtx.selectSide('CUSTOM', {
...activeNode,
borderTopWeight: weight,
borderRightWeight: weight,
borderBottomWeight: weight,
borderLeftWeight: weight
} as SceneNode)
} else if (!next && activeNode.independentStrokeWeights) {
strokeCtx.selectSide('ALL', activeNode)
}
}
</script>
<template>
<PropertyListRoot
v-slot="{ items, isMixed, activeNode, add, remove, patch, toggleVisibility }"
prop-key="strokes"
:label="panels.stroke"
>
<div data-test-id="stroke-section" :class="sectionWrapper()">
<div class="flex items-center justify-between">
<label :class="sectionLabel()">{{ panels.stroke }}</label>
<button
data-test-id="stroke-section-add"
:class="iconButton()"
@click="add(strokeCtx.defaultStroke)"
>
+
</button>
</div>
<p v-if="isMixed" class="text-[11px] text-muted">{{ panels.mixedStrokesHelp }}</p>
<ColorStyleRow
v-for="(stroke, i) in items as Stroke[]"
:key="`${i}:${stroke.visible ? 'visible' : 'hidden'}`"
:item="stroke"
:index="i"
:active-node-id="activeNode?.id ?? null"
:binding-api="strokeVarCtx"
:visibility-test-id="`stroke-visibility-${i}`"
unbind-test-id="stroke-unbind-variable"
data-test-id="stroke-item"
:data-test-index="i"
@patch="patch(i, $event)"
@toggle-visibility="toggleVisibility(i)"
@remove="remove(i)"
>
<ColorInput
class="min-w-0 flex-1"
:color="stroke.color"
:okhcl="
activeNode
? {
fieldFormat: okhcl.getFieldFormat(activeNode, i, 'stroke'),
fieldOptions: okhcl.fieldOptions,
okhcl: okhcl.getStrokeOkHCLColor(activeNode, i),
...okhcl.getStrokePreviewInfo(activeNode, i),
setFieldFormat: ($event) => okhcl.setStrokeFieldFormat(activeNode, i, $event),
updateOkHCL: ($event) => okhcl.updateStrokeOkHCL(activeNode, i, $event)
}
: null
"
editable
@update="patch(i, applySolidStrokeColor($event))"
/>
</ColorStyleRow>
<div
v-if="!isMixed && (items as unknown[]).length > 0"
class="mt-1 flex items-center gap-1.5"
>
<AppSelect
class="w-[72px]"
:model-value="strokeCtx.currentAlign(activeNode)"
:options="strokeCtx.alignOptions"
@update:model-value="strokeCtx.updateAlign($event as Stroke['align'], activeNode!)"
/>
<ScrubInput
v-if="!expandedSides"
class="flex-1"
:model-value="activeNode!.strokes[0]?.weight ?? 1"
:min="0"
@update:model-value="patch(0, { weight: $event })"
>
<template #icon>
<svg
class="size-3"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<line x1="1" y1="3" x2="11" y2="3" />
<line x1="1" y1="6" x2="11" y2="6" />
<line x1="1" y1="9" x2="11" y2="9" />
</svg>
</template>
</ScrubInput>
<Tip :label="panels.strokeSides">
<button
data-test-id="stroke-sides-toggle"
class="flex size-[26px] shrink-0 cursor-pointer items-center justify-center rounded border border-border bg-input text-muted hover:bg-hover hover:text-surface"
:class="{ '!border-accent !text-accent': expandedSides }"
@click="onToggleSides(activeNode!)"
>
<svg class="size-3.5" viewBox="0 0 14 14" fill="currentColor">
<rect x="1" y="1" width="5" height="5" rx="1" />
<rect x="8" y="1" width="5" height="5" rx="1" />
<rect x="1" y="8" width="5" height="5" rx="1" />
<rect x="8" y="8" width="5" height="5" rx="1" />
</svg>
</button>
</Tip>
</div>
<div
v-if="!isMixed && (items as unknown[]).length > 0 && expandedSides"
class="mt-1.5 grid grid-cols-2 gap-1.5"
>
<ScrubInput
v-for="side in strokeCtx.borderSides"
:key="side"
:model-value="
activeNode![
`border${side[0].toUpperCase()}${side.slice(1)}Weight` as keyof SceneNode
] as number
"
:min="0"
@update:model-value="strokeCtx.updateBorderWeight(side, $event, activeNode!)"
>
<template #icon>
<svg class="size-3" viewBox="0 0 12 12" fill="none" stroke-width="1.5">
<rect
x="1"
y="1"
width="10"
height="10"
rx="1"
stroke="currentColor"
stroke-opacity="0.3"
stroke-dasharray="2 2"
/>
<line v-if="side === 'top'" x1="1" y1="1" x2="11" y2="1" stroke="currentColor" />
<line
v-else-if="side === 'right'"
x1="11"
y1="1"
x2="11"
y2="11"
stroke="currentColor"
/>
<line
v-else-if="side === 'bottom'"
x1="1"
y1="11"
x2="11"
y2="11"
stroke="currentColor"
/>
<line v-else x1="1" y1="1" x2="1" y2="11" stroke="currentColor" />
</svg>
</template>
</ScrubInput>
</div>
</div>
</PropertyListRoot>
</template>