openpencil/src/components/properties/FillSection.vue
Danila Poyarkov c96f87bf9b feat(editor): add shared style bindings
- Model imported fill, stroke, text, effect, and grid style definitions and references

- Add SDK and app selectors with mixed-selection batching, detach-on-edit, and undo restoration

- Preserve style definitions through .fig export and extracted subgraphs

- Document the shared-style composable and update compatibility coverage
2026-07-17 20:28:22 +03:00

152 lines
5.3 KiB
Vue

<script setup lang="ts">
import {
BindableValueRoot,
useColorBindingProvider,
useFillControls,
useI18n,
useOkHCL
} from '@open-pencil/vue'
import FillPicker from '@/components/fill-picker/FillPicker.vue'
import PropertyItemRow from '@/components/properties/item-list/PropertyItemRow.vue'
import PaintField from '@/components/properties/paint/PaintField.vue'
import PaintValue from '@/components/properties/paint/PaintValue.vue'
import {
applyPaintMutation,
cancelPaintMutation,
commitPaintMutation,
paintBindingTargets
} from '@/components/properties/paint/binding'
import { fillLabel } from '@/components/properties/fill-label'
import { createFillOkhclAdapter } from '@/components/properties/paint/okhcl'
import PropertyListRoot from '@/components/properties/PropertyListRoot.vue'
import SharedStyleField from '@/components/properties/shared-style/SharedStyleField.vue'
import VariableBindingPicker from '@/components/properties/binding/VariableBindingPicker.vue'
import IconButton from '@/components/ui/IconButton.vue'
import PanelSection from '@/components/ui/panel/PanelSection.vue'
import { colorToHexRaw } from '@open-pencil/core/color'
import type { Fill } from '@open-pencil/scene-graph'
import type { Color } from '@open-pencil/scene-graph/primitives'
import type { BindableValueActions } from '@open-pencil/vue'
const fillCtx = useFillControls()
const okhcl = useOkHCL()
const colorProvider = useColorBindingProvider()
const { panels, dialogs } = useI18n()
function displayFill(fill: Fill, resolvedColor: Color | undefined): Fill {
return fill.type === 'SOLID' && resolvedColor ? { ...fill, color: resolvedColor } : fill
}
function updatePickerFill(
binding: BindableValueActions<Color>,
flush: () => void,
nextFill: Fill,
update: (fill: Fill) => void
) {
applyPaintMutation(binding, flush, () => update(nextFill))
}
function updateSolidColor(
binding: BindableValueActions<Color>,
flush: () => void,
fill: Fill,
color: Color,
update: (fill: Fill) => void
) {
if (fill.type !== 'SOLID') return
if (applyPaintMutation(binding, flush, () => update({ ...fill, color })))
commitPaintMutation(binding)
}
</script>
<template>
<PropertyListRoot
v-slot="{ items, isMixed, activeNode, selectedNodeIds, flush, actions }"
prop-key="fills"
:label="panels.fill"
>
<PanelSection :label="panels.fill" :empty="!isMixed && items.length === 0">
<template #actions>
<IconButton :label="panels.addFill" @click="actions.add({ ...fillCtx.defaultFill })">
<icon-lucide-plus class="size-3.5" />
</IconButton>
</template>
<SharedStyleField kind="fill" :label="panels.fillStyle" />
<p v-if="isMixed" class="text-[11px] text-muted">{{ panels.mixedFillsHelp }}</p>
<PropertyItemRow
v-for="(fill, index) in items"
:key="`${index}:${fill.visible ? 'visible' : 'hidden'}`"
prop-key="fills"
:index="index"
:visibility-label="panels.toggleVisibility"
:remove-label="panels.removeFill"
>
<BindableValueRoot
v-slot="binding"
:provider="colorProvider"
:targets="paintBindingTargets(selectedNodeIds, 'fills', index)"
:value="fill.color"
batch-label="Change fill color"
>
<PaintField
:opacity="fill.opacity"
:opacity-label="panels.opacity"
@update:opacity="actions.patch(index, { opacity: $event })"
>
<template #preview>
<FillPicker
:fill="displayFill(fill, binding.resolvedValue)"
:okhcl="createFillOkhclAdapter(okhcl, activeNode, index)"
@update="
updatePickerFill(binding.actions, flush, $event, (next) =>
actions.update(index, next)
)
"
@open-change="!$event && commitPaintMutation(binding.actions)"
@cancel="cancelPaintMutation(binding.actions)"
/>
</template>
<template #value>
<PaintValue
v-if="fill.type === 'SOLID'"
:color="fill.color"
:resolved-color="binding.resolvedValue"
:variable-name="binding.variable?.name"
:label="panels.fill"
@update="
updateSolidColor(binding.actions, flush, fill, $event, (next) =>
actions.update(index, next)
)
"
/>
<span v-else class="min-w-0 flex-1 truncate font-mono text-xs text-surface">
{{ fillLabel(fill) }}
</span>
</template>
<template v-if="fill.type === 'SOLID'" #binding>
<VariableBindingPicker
:trigger-label="panels.applyVariable"
:search-placeholder="dialogs.search"
:empty-label="panels.noVariablesFound"
:detach-label="panels.detachVariable"
:create-label="
panels.createColorVariable({ value: `#${colorToHexRaw(fill.color)}` })
"
:create-name-placeholder="panels.variableName"
:create-submit-label="panels.create"
/>
</template>
</PaintField>
</BindableValueRoot>
</PropertyItemRow>
</PanelSection>
</PropertyListRoot>
</template>