189 lines
6.6 KiB
Vue
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>
|