feat(components): add variant inspector UI

Show variant property dropdowns in the design panel when an instance
of a component set is selected. Switching a dropdown value finds the
matching variant component and updates the instance's componentId.

- VariantSection.vue with property dropdowns using AppSelect
- Bridge variant actions to the editor return object
- i18n for en, ru, zh-CN

Ref #239
This commit is contained in:
Danila Poyarkov 2026-05-07 19:48:59 +03:00
parent fee44ebdb7
commit c79c9106fb
6 changed files with 79 additions and 3 deletions

View file

@ -28,6 +28,13 @@ export function createComponentBridge(
createInstanceFromComponent: components.createInstanceFromComponent,
detachInstance: () => components.detachInstance(selection.getSelectedNode()),
goToMainComponent: () =>
components.goToMainComponent(selection.getSelectedNode(), pages.switchPage)
components.goToMainComponent(selection.getSelectedNode(), pages.switchPage),
getComponentSetPropertyDefs: components.getComponentSetPropertyDefs,
addPropertyDefinition: components.addPropertyDefinition,
removePropertyDefinition: components.removePropertyDefinition,
renamePropertyDefinition: components.renamePropertyDefinition,
collectVariantOptions: components.collectVariantOptions,
findVariantByValues: components.findVariantByValues,
switchInstanceVariant: components.switchInstanceVariant
}
}

View file

@ -130,6 +130,7 @@ export const panelMessages = i18n('panels', {
export: 'Export',
typography: 'Typography',
variables: 'Variables',
variants: 'Variants',
constraints: 'Constraints',
addFill: 'Add fill',

View file

@ -175,7 +175,8 @@
"create": "Создать",
"createNumberVariable": "Создать числовую переменную из {value}",
"strokeDash": "Пунктирная обводка",
"add": "Добавить"
"add": "Добавить",
"variants": "Варианты"
},
"pages": {
"newPage": "Новая страница",

View file

@ -175,7 +175,8 @@
"create": "创建",
"createNumberVariable": "从 {value} 创建数字变量",
"strokeDash": "虚线描边",
"add": "添加"
"add": "添加",
"variants": "变体"
},
"pages": {
"newPage": "新建页面",

View file

@ -14,6 +14,7 @@ import PositionSection from './properties/PositionSection.vue'
import StrokeSection from './properties/StrokeSection.vue'
import TypographySection from './properties/TypographySection.vue'
import VariablesSection from './properties/VariablesSection.vue'
import VariantSection from './properties/VariantSection.vue'
const variablesOpen = ref(false)
const { selectedNode: node, selectedCount: multiCount } = useSelectionState()
@ -87,6 +88,8 @@ const { panels } = useI18n()
</button>
</div>
<VariantSection v-if="node.type === 'INSTANCE'" />
<PositionSection />
<LayoutSection />
<AppearanceSection />

View file

@ -0,0 +1,63 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n, useSelectionState } from '@open-pencil/vue'
import AppSelect from '@/components/ui/AppSelect.vue'
import { useSectionUI } from '@/components/ui/section'
import { useEditorStore } from '@/app/editor/active-store'
const editor = useEditorStore()
const { selectedNode: node } = useSelectionState()
const sectionCls = useSectionUI()
const { panels } = useI18n()
const instanceComponent = computed(() => {
if (!node.value || node.value.type !== 'INSTANCE' || !node.value.componentId) return null
return editor.graph.getNode(node.value.componentId) ?? null
})
const componentSetId = computed(() => {
const comp = instanceComponent.value
if (!comp) return null
const parent = comp.parentId ? editor.graph.getNode(comp.parentId) : null
return parent?.type === 'COMPONENT_SET' ? parent.id : null
})
const variantOptions = computed(() => {
const csId = componentSetId.value
if (!csId) return new Map<string, Set<string>>()
return editor.collectVariantOptions(csId)
})
const currentValues = computed(() => {
return instanceComponent.value?.componentPropertyValues ?? {}
})
const hasVariants = computed(() => variantOptions.value.size > 0)
function switchVariant(propertyName: string, newValue: string) {
if (!node.value) return
editor.switchInstanceVariant(node.value.id, propertyName, newValue)
}
</script>
<template>
<div v-if="hasVariants" data-test-id="variant-section" :class="sectionCls.wrapper">
<label class="mb-1.5 block text-[11px] font-medium text-component">{{ panels.variants }}</label>
<div class="flex flex-col gap-1.5">
<div
v-for="[propName, options] in variantOptions"
:key="propName"
class="flex flex-col gap-0.5"
>
<label class="text-[10px] text-muted">{{ propName }}</label>
<AppSelect
:model-value="currentValues[propName] ?? ''"
:options="[...options].map((v) => ({ value: v, label: v }))"
@update:model-value="switchVariant(propName, $event)"
/>
</div>
</div>
</div>
</template>