openpencil/src/components/properties/FillSection.vue
Danila Poyarkov 1b7934b98e feat(vue): add property panel primitives
- Add headless PropertySection and accessible SegmentedControl anatomy

- Make PropertyList controlled and exactly typed with an undo-aware editor adapter

- Consolidate SDK metadata loaders and link untranslated docs to canonical pages
2026-07-13 12:36:03 +03:00

115 lines
4 KiB
Vue

<script setup lang="ts">
import { useFillControls, useOkHCL, useI18n, inputValue } from '@open-pencil/vue'
import { colorToHexRaw, parseColor } from '@open-pencil/core/color'
import FillPicker from '@/components/fill-picker/FillPicker.vue'
import PropertyListRoot from '@/components/properties/PropertyListRoot.vue'
import IconButton from '@/components/ui/IconButton.vue'
import PanelSection from '@/components/ui/panel/PanelSection.vue'
import ColorStyleRow from '@/components/properties/ColorStyleRow.vue'
import {
boundVariableSwatchBackground,
displayFillWithBoundVariable
} from '@/components/properties/color-style-row'
import { fillLabel } from '@/components/properties/fill-label'
import { createFillOkhclAdapter } from '@/components/properties/fill-okhcl'
import type { Fill, SceneNode } from '@open-pencil/scene-graph'
const fillCtx = useFillControls()
const okhcl = useOkHCL()
const { panels } = useI18n()
function updateFill(
activeNode: SceneNode | null | undefined,
index: number,
fill: Fill,
update: (index: number, fill: Fill) => void
) {
if (activeNode && fillCtx.getBoundVariable(activeNode.id, index)) {
fillCtx.unbindVariable(activeNode.id, index)
}
update(index, fill)
}
function updateFillHex(
activeNode: SceneNode | null | undefined,
index: number,
fill: Fill,
hex: string,
update: (index: number, fill: Fill) => void
) {
if (fill.type !== 'SOLID') return
const parsed = parseColor(hex.startsWith('#') ? hex : `#${hex}`)
if (!parsed) return
updateFill(activeNode, index, { ...fill, color: { ...parsed, a: fill.color.a } }, update)
}
</script>
<template>
<PropertyListRoot
v-slot="{ items, isMixed, activeNode, actions }"
prop-key="fills"
:label="panels.fill"
>
<PanelSection :label="panels.fill" data-test-id="fill-section">
<template #actions>
<IconButton
:label="panels.addFill"
data-test-id="fill-section-add"
@click="actions.add({ ...fillCtx.defaultFill })"
>
<icon-lucide-plus class="size-3.5" />
</IconButton>
</template>
<p v-if="isMixed" class="text-[11px] text-muted">{{ panels.mixedFillsHelp }}</p>
<ColorStyleRow
v-for="(fill, i) in items"
:key="`${i}:${fill.visible ? 'visible' : 'hidden'}`"
:item="fill"
:index="i"
:active-node-id="activeNode?.id ?? null"
:binding-api="fillCtx"
:variable-color="fill.type === 'SOLID' ? fill.color : undefined"
data-test-id="fill-item"
:data-test-index="i"
:remove-label="panels.removeFill"
@patch="actions.patch(i, $event)"
@toggle-visibility="actions.toggleVisibility(i)"
@remove="actions.remove(i)"
>
<FillPicker
:fill="activeNode ? displayFillWithBoundVariable(fillCtx, activeNode.id, i, fill) : fill"
:okhcl="createFillOkhclAdapter(okhcl, activeNode, i)"
:swatch-background="
activeNode ? boundVariableSwatchBackground(fillCtx, activeNode.id, i) : undefined
"
@update="updateFill(activeNode, i, $event, actions.update)"
/>
<input
v-if="
fill.type === 'SOLID' && !(activeNode && fillCtx.getBoundVariable(activeNode.id, i))
"
data-test-id="fill-hex-input"
class="min-w-0 flex-1 border-none bg-transparent font-mono text-xs text-surface outline-none"
:value="colorToHexRaw(fill.color)"
maxlength="6"
@change="updateFillHex(activeNode, i, fill, inputValue($event), actions.update)"
/>
<span
v-else
class="min-w-0 flex-1 truncate font-mono text-xs"
:class="
activeNode && fillCtx.getBoundVariable(activeNode.id, i)
? 'rounded bg-violet-500/10 px-1 text-violet-400'
: 'text-surface'
"
>
{{ fillLabel(fill, activeNode ? fillCtx.getBoundVariable(activeNode.id, i) : undefined) }}
</span>
</ColorStyleRow>
</PanelSection>
</PropertyListRoot>
</template>