203 lines
6.7 KiB
Vue
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>
|