openpencil/src/components/PropertiesPanel.vue
Danila Poyarkov d30ab4c5d5 Add variables support: collections, modes, bindings, UI panel
Core:
- Variable, VariableCollection types on SceneGraph
- resolveVariable with alias chain resolution + cycle detection
- Mode switching (activeMode per collection)
- Bind/unbind variables to node properties (fills/strokes colors)
- Renderer resolves variable bindings before painting

Import:
- Parse VARIABLE type NodeChanges from .fig files
- Extract paint variable bindings (fills/strokes)

UI:
- VariablesPanel with reka-ui Tabs (collections) + Editable (names/values)
- FillSection: variable picker (reka-ui Popover + Combobox)
- Bound fills show purple variable name badge with detach button
- Toggle variables panel from bottom of layers sidebar

7 new unit tests (83 total)
2026-03-01 01:47:52 +03:00

96 lines
3.2 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue'
import { useEditorStore } from '@/stores/editor'
import AppearanceSection from './properties/AppearanceSection.vue'
import EffectsSection from './properties/EffectsSection.vue'
import ExportSection from './properties/ExportSection.vue'
import FillSection from './properties/FillSection.vue'
import LayoutSection from './properties/LayoutSection.vue'
import PageSection from './properties/PageSection.vue'
import PositionSection from './properties/PositionSection.vue'
import StrokeSection from './properties/StrokeSection.vue'
import TypographySection from './properties/TypographySection.vue'
const store = useEditorStore()
const node = computed(() => store.selectedNode.value)
const multiCount = computed(() => store.selectedNodes.value.length)
const isComponentType = computed(() => {
const t = node.value?.type
return t === 'COMPONENT' || t === 'COMPONENT_SET' || t === 'INSTANCE'
})
</script>
<template>
<aside
class="flex min-w-0 flex-1 flex-col overflow-hidden border-l border-border bg-panel"
style="contain: paint layout style"
>
<!-- Tabs -->
<div class="flex h-10 shrink-0 items-center gap-1 border-b border-border px-2">
<button class="rounded px-2.5 py-1 text-xs font-semibold text-surface">Design</button>
<button class="rounded px-2.5 py-1 text-xs text-muted">Prototype</button>
<span
class="ml-auto cursor-pointer rounded px-1.5 py-0.5 text-[11px] text-muted hover:bg-hover"
>
{{ Math.round(store.state.zoom * 100) }}%
</span>
</div>
<!-- Multi-select summary -->
<div v-if="multiCount > 1" class="flex-1 overflow-y-auto pb-4">
<div class="flex items-center gap-1.5 border-b border-border px-3 py-2">
<span class="text-[11px] text-muted">Mixed</span>
<span class="text-xs font-semibold">{{ multiCount }} layers</span>
</div>
<AppearanceSection />
</div>
<!-- Single selection -->
<div v-else-if="node" class="flex-1 overflow-y-auto pb-4">
<!-- Node header -->
<div class="flex items-center gap-1.5 border-b border-border px-3 py-2">
<span class="text-[11px]" :class="isComponentType ? 'text-[#9747ff]' : 'text-muted'">{{
node.type
}}</span>
<span class="text-xs font-semibold">{{ node.name }}</span>
</div>
<!-- Component actions -->
<div
v-if="node.type === 'INSTANCE'"
class="flex flex-col gap-1 border-b border-border px-3 py-2"
>
<button
class="rounded bg-[#9747ff]/10 px-2 py-1 text-left text-[11px] text-[#9747ff] hover:bg-[#9747ff]/20"
@click="store.goToMainComponent()"
>
Go to Main Component
</button>
<button
class="rounded px-2 py-1 text-left text-[11px] text-muted hover:bg-hover"
@click="store.detachInstance()"
>
Detach Instance
</button>
</div>
<PositionSection />
<LayoutSection />
<AppearanceSection />
<TypographySection v-if="node.type === 'TEXT'" />
<FillSection />
<StrokeSection />
<EffectsSection />
<ExportSection />
</div>
<div v-else class="flex-1 overflow-y-auto pb-4">
<PageSection />
</div>
</aside>
</template>