2026-03-01 07:55:52 +00:00
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, ref } from 'vue'
|
|
|
|
|
|
|
|
|
|
import { useEditorStore } from '@/stores/editor'
|
|
|
|
|
|
|
|
|
|
import VariablesDialog from './VariablesDialog.vue'
|
|
|
|
|
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'
|
|
|
|
|
import VariablesSection from './properties/VariablesSection.vue'
|
|
|
|
|
|
|
|
|
|
const store = useEditorStore()
|
|
|
|
|
const variablesOpen = ref(false)
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
<!-- Multi-select summary -->
|
2026-03-04 14:18:26 +00:00
|
|
|
<div
|
|
|
|
|
v-if="multiCount > 1"
|
|
|
|
|
data-test-id="design-panel-multi"
|
2026-03-05 06:03:26 +00:00
|
|
|
class="flex-1 overflow-x-hidden overflow-y-auto scrollbar-thin pb-4"
|
2026-03-04 14:18:26 +00:00
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
data-test-id="design-multi-header"
|
|
|
|
|
class="flex items-center gap-1.5 border-b border-border px-3 py-2"
|
|
|
|
|
>
|
2026-03-01 07:55:52 +00:00
|
|
|
<span class="text-[11px] text-muted">Mixed</span>
|
|
|
|
|
<span class="text-xs font-semibold">{{ multiCount }} layers</span>
|
|
|
|
|
</div>
|
Multi-selection properties panel with tests
Show position, appearance, fill, stroke, and effects sections
when multiple nodes are selected. Shared values display normally,
differing values show 'Mixed'. Editing applies to all selected.
- Shared composable (use-multi-props) for isMulti, active, activeNode
- ScrubInput supports MIXED symbol as model-value
- Per-node previous values for correct undo on multi-edit
- Fix flip transform encoding (scale first column only)
- Fix npm publish auth for MCP package
- 22 new tests: flip, alignment, multi-node merging, kiwi roundtrip
2026-03-03 17:35:51 +00:00
|
|
|
<PositionSection />
|
2026-03-01 07:55:52 +00:00
|
|
|
<AppearanceSection />
|
Multi-selection properties panel with tests
Show position, appearance, fill, stroke, and effects sections
when multiple nodes are selected. Shared values display normally,
differing values show 'Mixed'. Editing applies to all selected.
- Shared composable (use-multi-props) for isMulti, active, activeNode
- ScrubInput supports MIXED symbol as model-value
- Per-node previous values for correct undo on multi-edit
- Fix flip transform encoding (scale first column only)
- Fix npm publish auth for MCP package
- 22 new tests: flip, alignment, multi-node merging, kiwi roundtrip
2026-03-03 17:35:51 +00:00
|
|
|
<FillSection />
|
|
|
|
|
<StrokeSection />
|
|
|
|
|
<EffectsSection />
|
2026-03-01 07:55:52 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Single selection -->
|
2026-03-04 14:18:26 +00:00
|
|
|
<div
|
|
|
|
|
v-else-if="node"
|
|
|
|
|
data-test-id="design-panel-single"
|
2026-03-05 06:03:26 +00:00
|
|
|
class="flex-1 overflow-x-hidden overflow-y-auto scrollbar-thin pb-4"
|
2026-03-04 14:18:26 +00:00
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
data-test-id="design-node-header"
|
|
|
|
|
class="flex items-center gap-1.5 border-b border-border px-3 py-2"
|
|
|
|
|
>
|
2026-03-01 07:55:52 +00:00
|
|
|
<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
|
2026-03-04 14:18:26 +00:00
|
|
|
data-test-id="design-go-to-component"
|
2026-03-01 07:55:52 +00:00
|
|
|
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
|
2026-03-04 14:18:26 +00:00
|
|
|
data-test-id="design-detach-instance"
|
2026-03-01 07:55:52 +00:00
|
|
|
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>
|
|
|
|
|
|
2026-03-04 14:18:26 +00:00
|
|
|
<div
|
|
|
|
|
v-else
|
|
|
|
|
data-test-id="design-panel-empty"
|
2026-03-05 06:03:26 +00:00
|
|
|
class="flex-1 overflow-x-hidden overflow-y-auto scrollbar-thin pb-4"
|
2026-03-04 14:18:26 +00:00
|
|
|
>
|
2026-03-01 07:55:52 +00:00
|
|
|
<PageSection />
|
|
|
|
|
<VariablesSection @open-dialog="variablesOpen = true" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<VariablesDialog v-model:open="variablesOpen" />
|
|
|
|
|
</template>
|