openpencil/src/components/DesignPanel.vue
2026-08-14 19:31:41 +03:00

189 lines
6.6 KiB
Vue

<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n, useSelectionState, useEditorCommands } from '@open-pencil/vue'
import { useEditorStore } from '@/app/editor/active-store'
import { openLibraryReview, useLibraryService } from '@/app/libraries'
import { COMPONENT_TYPES, nodeIcon } from '@/app/editor/icons'
import PanelHeader from '@/components/ui/panel/PanelHeader.vue'
import Tip from '@/components/ui/Tip.vue'
import VariablesDialog from './variables/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 LayoutGridSection from './properties/LayoutSection/LayoutGridSection.vue'
import LayoutSection from './properties/LayoutSection/LayoutSection.vue'
import MaskSection from './properties/MaskSection.vue'
import PageSection from './properties/PageSection.vue'
import ConstraintsSection from './properties/constraints/ConstraintsSection.vue'
import PositionSection from './properties/PositionSection.vue'
import SelectionActionsControl from './properties/SelectionActionsControl.vue'
import StrokeSection from './properties/StrokeSection.vue'
import TypographySection from './properties/TypographySection.vue'
import VariablesSection from './properties/VariablesSection.vue'
import ComponentPropertiesSection from './properties/component-properties/ComponentPropertiesSection.vue'
import VariantAuthoringSection from './properties/component-properties/VariantAuthoringSection.vue'
import InstanceUpdateAction from './properties/component-properties/instance-update/InstanceUpdateAction.vue'
import FramePresetsSection from './properties/frame-presets/FramePresetsSection.vue'
import FramePresetSelect from './properties/frame-presets/FramePresetSelect.vue'
const variablesOpen = ref(false)
const store = useEditorStore()
const libraryService = useLibraryService()
const activeTool = computed(() => store.state.activeTool)
const { selectedNode: node, selectedCount: multiCount } = useSelectionState()
const showBooleanOperations = computed(() => multiCount.value >= 2)
const { getCommand } = useEditorCommands()
const goToMainComponent = getCommand('selection.goToMainComponent')
const detachInstance = getCommand('selection.detachInstance')
const isComponentType = computed(() => {
const type = node.value?.type
return type ? COMPONENT_TYPES.has(type) : false
})
const selectedIcon = computed(() => (node.value ? nodeIcon(node.value) : undefined))
function openSelectedInstanceReview() {
const instance = node.value
if (instance?.type !== 'INSTANCE' || !instance.componentId) return
const component = store.graph.getNode(instance.componentId)
const identity = component?.librarySource?.identity
if (!identity) return
openLibraryReview({
libraryId: identity.libraryId,
assetKey: identity.assetKey,
instanceIds: [instance.id],
initialInstanceId: instance.id
})
}
const supportsLayoutGuides = computed(() => {
const type = node.value?.type
return type === 'FRAME' || type === 'COMPONENT' || type === 'COMPONENT_SET' || type === 'INSTANCE'
})
const { panels } = useI18n()
</script>
<template>
<!-- Frame tool presets replace selection properties, matching Figma. -->
<div
v-if="activeTool === 'FRAME'"
class="scrollbar-thin flex-1 overflow-x-hidden overflow-y-auto pb-4"
>
<FramePresetsSection />
</div>
<!-- Multi-select summary -->
<div
v-else-if="multiCount > 1"
data-test-id="design-panel-multi"
class="scrollbar-thin flex-1 overflow-x-hidden overflow-y-auto pb-4"
>
<PanelHeader>
<template #icon>
<icon-lucide-layers-3 class="size-3.5" aria-hidden="true" />
</template>
<span role="heading" aria-level="2">
{{ panels.layersCount({ count: String(multiCount) }) }}
</span>
<template #actions>
<SelectionActionsControl :show-boolean-operations="showBooleanOperations" />
</template>
</PanelHeader>
<ComponentPropertiesSection />
<PositionSection />
<ConstraintsSection />
<AppearanceSection />
<FillSection />
<StrokeSection />
<EffectsSection />
<ExportSection />
</div>
<!-- Single selection -->
<div
v-else-if="node"
data-test-id="design-panel-single"
class="scrollbar-thin flex-1 overflow-x-hidden overflow-y-auto pb-4"
>
<PanelHeader :component="isComponentType">
<template #icon>
<Tip :label="node.type">
<span role="img" :aria-label="node.type" class="contents">
<component :is="selectedIcon" class="size-3.5" />
</span>
</Tip>
</template>
<span role="heading" aria-level="2">{{ node.name }}</span>
<template #actions>
<InstanceUpdateAction
v-if="node.type === 'INSTANCE'"
:node="node"
:editor="store"
:service="libraryService"
@review="openSelectedInstanceReview"
/>
<SelectionActionsControl />
</template>
</PanelHeader>
<!-- Component actions -->
<div
v-if="node.type === 'INSTANCE'"
class="flex flex-col gap-1 border-b border-border px-3 py-2"
>
<button
type="button"
class="rounded bg-component/10 px-2 py-1 text-left text-[11px] text-component hover:bg-component/20"
@click="goToMainComponent.run()"
>
{{ panels.goToMainComponent }}
</button>
<button
type="button"
class="rounded px-2 py-1 text-left text-[11px] text-muted hover:bg-hover"
@click="detachInstance.run()"
>
{{ panels.detachInstance }}
</button>
</div>
<ComponentPropertiesSection v-if="node.type === 'INSTANCE'" />
<VariantAuthoringSection
v-if="
node.type === 'COMPONENT_SET' ||
(node.type === 'COMPONENT' &&
node.parentId &&
store.graph.getNode(node.parentId)?.type === 'COMPONENT_SET')
"
/>
<FramePresetSelect v-if="node.type === 'FRAME'" />
<PositionSection />
<ConstraintsSection />
<LayoutSection />
<AppearanceSection />
<MaskSection />
<TypographySection v-if="node.type === 'TEXT'" />
<FillSection />
<StrokeSection />
<LayoutGridSection v-if="supportsLayoutGuides" />
<EffectsSection />
<ExportSection />
</div>
<div
v-else
data-test-id="design-panel-empty"
class="scrollbar-thin flex-1 overflow-x-hidden overflow-y-auto pb-4"
>
<PageSection />
<VariablesSection @open-dialog="variablesOpen = true" />
<ExportSection />
</div>
<VariablesDialog v-model:open="variablesOpen" />
</template>