Remove prop drilling from property sections

All sections now get selected node from useNodeProps() composable
instead of receiving it as a prop from PropertiesPanel.
This commit is contained in:
Danila Poyarkov 2026-02-28 09:27:03 +03:00
parent 2af2a280c6
commit de83a716cd
8 changed files with 69 additions and 77 deletions

View file

@ -33,7 +33,7 @@ const multiCount = computed(() => store.selectedNodes.value.length)
<span class="text-[11px] text-muted">Mixed</span>
<span class="text-xs font-semibold">{{ multiCount }} layers</span>
</div>
<AppearanceSection v-if="store.selectedNodes.value[0]" :node="store.selectedNodes.value[0]" />
<AppearanceSection />
</div>
<!-- Single selection -->
@ -44,12 +44,12 @@ const multiCount = computed(() => store.selectedNodes.value.length)
<span class="text-xs font-semibold">{{ node.name }}</span>
</div>
<PositionSection :node="node" />
<LayoutSection :node="node" />
<AppearanceSection :node="node" />
<TypographySection v-if="node.type === 'TEXT'" :node="node" />
<FillSection :node-id="node.id" :fills="node.fills" />
<StrokeSection :node-id="node.id" :strokes="node.strokes" />
<PositionSection />
<LayoutSection />
<AppearanceSection />
<TypographySection v-if="node.type === 'TEXT'" />
<FillSection />
<StrokeSection />
<!-- Effects -->
<div class="border-b border-border px-3 py-2">

View file

@ -2,13 +2,10 @@
import ScrubInput from '../ScrubInput.vue'
import { useNodeProps } from '../../composables/use-node-props'
import type { SceneNode } from '../../engine/scene-graph'
const { node } = defineProps<{ node: SceneNode }>()
const { store, updateProp, commitProp } = useNodeProps()
const { store, node, updateProp, commitProp } = useNodeProps()
function toggleVisibility() {
store.updateNodeWithUndo(node.id, { visible: !node.visible }, 'Toggle visibility')
store.updateNodeWithUndo(node.value.id, { visible: !node.value.visible }, 'Toggle visibility')
store.requestRender()
}
</script>

View file

@ -1,19 +1,18 @@
<script setup lang="ts">
import ColorPicker from '../ColorPicker.vue'
import ScrubInput from '../ScrubInput.vue'
import { useEditorStore } from '../../stores/editor'
import { useNodeProps } from '../../composables/use-node-props'
import { DEFAULT_SHAPE_FILL } from '../../constants'
import { colorToHexRaw, parseColor } from '../../engine/color'
import type { Color, Fill } from '../../engine/scene-graph'
import type { Color } from '../../engine/scene-graph'
const props = defineProps<{ nodeId: string; fills: Fill[] }>()
const store = useEditorStore()
const { store, node } = useNodeProps()
function updateColor(index: number, color: Color) {
const fills = [...props.fills]
const fills = [...node.value.fills]
fills[index] = { ...fills[index], color }
store.updateNodeWithUndo(props.nodeId, { fills }, 'Change fill')
store.updateNodeWithUndo(node.value.id, { fills }, 'Change fill')
}
function updateHex(index: number, hex: string) {
@ -23,23 +22,23 @@ function updateHex(index: number, hex: string) {
}
function updateOpacity(index: number, opacity: number) {
const fills = [...props.fills]
const fills = [...node.value.fills]
fills[index] = { ...fills[index], opacity: Math.max(0, Math.min(1, opacity / 100)) }
store.updateNodeWithUndo(props.nodeId, { fills }, 'Change fill')
store.updateNodeWithUndo(node.value.id, { fills }, 'Change fill')
}
function toggleVisibility(index: number) {
const fills = [...props.fills]
const fills = [...node.value.fills]
fills[index] = { ...fills[index], visible: !fills[index].visible }
store.updateNodeWithUndo(props.nodeId, { fills }, 'Change fill')
store.updateNodeWithUndo(node.value.id, { fills }, 'Change fill')
}
function add() {
store.updateNodeWithUndo(props.nodeId, { fills: [...props.fills, { ...DEFAULT_SHAPE_FILL }] }, 'Add fill')
store.updateNodeWithUndo(node.value.id, { fills: [...node.value.fills, { ...DEFAULT_SHAPE_FILL }] }, 'Add fill')
}
function remove(index: number) {
store.updateNodeWithUndo(props.nodeId, { fills: props.fills.filter((_, i) => i !== index) }, 'Remove fill')
store.updateNodeWithUndo(node.value.id, { fills: node.value.fills.filter((_, i) => i !== index) }, 'Remove fill')
}
</script>
@ -49,7 +48,7 @@ function remove(index: number) {
<label class="mb-1 block text-[11px] text-muted">Fill</label>
<button class="cursor-pointer rounded border-none bg-transparent px-1 text-base leading-none text-muted hover:bg-hover hover:text-surface" @click="add">+</button>
</div>
<div v-for="(fill, i) in fills" :key="i" class="group flex items-center gap-1.5 py-0.5">
<div v-for="(fill, i) in node.fills" :key="i" class="group flex items-center gap-1.5 py-0.5">
<ColorPicker :color="fill.color" @update="updateColor(i, $event)" />
<input
class="min-w-0 flex-1 border-none bg-transparent font-mono text-xs text-surface outline-none"

View file

@ -7,8 +7,7 @@ import { useNodeProps } from '../../composables/use-node-props'
import type { SceneNode, LayoutSizing, LayoutAlign, LayoutCounterAlign } from '../../engine/scene-graph'
const { node } = defineProps<{ node: SceneNode }>()
const { store, updateProp, commitProp } = useNodeProps()
const { store, node, updateProp, commitProp } = useNodeProps()
const showIndividualPadding = ref(false)
const widthSizingOpen = ref(false)
@ -20,30 +19,30 @@ onClickOutside(widthDimRef, () => { widthSizingOpen.value = false })
onClickOutside(heightDimRef, () => { heightSizingOpen.value = false })
const isInAutoLayout = computed(() => {
if (!node.parentId) return false
const parent = store.graph.getNode(node.parentId)
if (!node.value.parentId) return false
const parent = store.graph.getNode(node.value.parentId)
return parent ? parent.layoutMode !== 'NONE' : false
})
const widthSizing = computed(() => {
if (node.layoutMode !== 'NONE') {
return node.layoutMode === 'HORIZONTAL' ? node.primaryAxisSizing : node.counterAxisSizing
if (node.value.layoutMode !== 'NONE') {
return node.value.layoutMode === 'HORIZONTAL' ? node.value.primaryAxisSizing : node.value.counterAxisSizing
}
if (isInAutoLayout.value && node.layoutGrow > 0) return 'FILL'
if (isInAutoLayout.value && node.value.layoutGrow > 0) return 'FILL'
return 'FIXED'
})
const heightSizing = computed(() => {
if (node.layoutMode !== 'NONE') {
return node.layoutMode === 'VERTICAL' ? node.primaryAxisSizing : node.counterAxisSizing
if (node.value.layoutMode !== 'NONE') {
return node.value.layoutMode === 'VERTICAL' ? node.value.primaryAxisSizing : node.value.counterAxisSizing
}
if (isInAutoLayout.value && node.layoutAlignSelf === 'STRETCH') return 'FILL'
if (isInAutoLayout.value && node.value.layoutAlignSelf === 'STRETCH') return 'FILL'
return 'FIXED'
})
function setWidthSizing(sizing: LayoutSizing) {
if (node.layoutMode !== 'NONE') {
if (node.layoutMode === 'HORIZONTAL') updateProp('primaryAxisSizing', sizing)
if (node.value.layoutMode !== 'NONE') {
if (node.value.layoutMode === 'HORIZONTAL') updateProp('primaryAxisSizing', sizing)
else updateProp('counterAxisSizing', sizing)
} else if (isInAutoLayout.value) {
updateProp('layoutGrow', sizing === 'FILL' ? 1 : 0)
@ -52,8 +51,8 @@ function setWidthSizing(sizing: LayoutSizing) {
}
function setHeightSizing(sizing: LayoutSizing) {
if (node.layoutMode !== 'NONE') {
if (node.layoutMode === 'VERTICAL') updateProp('primaryAxisSizing', sizing)
if (node.value.layoutMode !== 'NONE') {
if (node.value.layoutMode === 'VERTICAL') updateProp('primaryAxisSizing', sizing)
else updateProp('counterAxisSizing', sizing)
} else if (isInAutoLayout.value) {
updateProp('layoutAlignSelf', sizing === 'FILL' ? 'STRETCH' : 'AUTO')
@ -68,17 +67,17 @@ function sizingLabel(s: string) {
}
function hasUniformPadding() {
return node.paddingTop === node.paddingRight &&
node.paddingRight === node.paddingBottom &&
node.paddingBottom === node.paddingLeft
return node.value.paddingTop === node.value.paddingRight &&
node.value.paddingRight === node.value.paddingBottom &&
node.value.paddingBottom === node.value.paddingLeft
}
function setUniformPadding(v: number) {
store.updateNode(node.id, { paddingTop: v, paddingRight: v, paddingBottom: v, paddingLeft: v })
store.updateNode(node.value.id, { paddingTop: v, paddingRight: v, paddingBottom: v, paddingLeft: v })
}
function commitUniformPadding(_value: number, previous: number) {
store.commitNodeUpdate(node.id, {
store.commitNodeUpdate(node.value.id, {
paddingTop: previous, paddingRight: previous, paddingBottom: previous, paddingLeft: previous
} as unknown as Partial<SceneNode>, 'Change padding')
}
@ -96,7 +95,7 @@ const ALIGN_GRID: Array<{ primary: LayoutAlign; counter: LayoutCounterAlign }> =
]
function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
store.updateNodeWithUndo(node.id, { primaryAxisAlign: primary, counterAxisAlign: counter }, 'Change alignment')
store.updateNodeWithUndo(node.value.id, { primaryAxisAlign: primary, counterAxisAlign: counter }, 'Change alignment')
}
</script>

View file

@ -2,10 +2,7 @@
import ScrubInput from '../ScrubInput.vue'
import { useNodeProps } from '../../composables/use-node-props'
import type { SceneNode } from '../../engine/scene-graph'
const { node } = defineProps<{ node: SceneNode }>()
const { updateProp, commitProp } = useNodeProps()
const { node, updateProp, commitProp } = useNodeProps()
</script>
<template>

View file

@ -1,18 +1,17 @@
<script setup lang="ts">
import ColorPicker from '../ColorPicker.vue'
import ScrubInput from '../ScrubInput.vue'
import { useEditorStore } from '../../stores/editor'
import { useNodeProps } from '../../composables/use-node-props'
import { colorToHexRaw, parseColor } from '../../engine/color'
import type { Color, Stroke } from '../../engine/scene-graph'
const props = defineProps<{ nodeId: string; strokes: Stroke[] }>()
const store = useEditorStore()
const { store, node } = useNodeProps()
function updateColor(index: number, color: Color) {
const strokes = [...props.strokes]
const strokes = [...node.value.strokes]
strokes[index] = { ...strokes[index], color }
store.updateNodeWithUndo(props.nodeId, { strokes }, 'Change stroke')
store.updateNodeWithUndo(node.value.id, { strokes }, 'Change stroke')
}
function updateHex(index: number, hex: string) {
@ -22,30 +21,30 @@ function updateHex(index: number, hex: string) {
}
function updateWeight(index: number, weight: number) {
const strokes = [...props.strokes]
const strokes = [...node.value.strokes]
strokes[index] = { ...strokes[index], weight }
store.updateNodeWithUndo(props.nodeId, { strokes }, 'Change stroke')
store.updateNodeWithUndo(node.value.id, { strokes }, 'Change stroke')
}
function updateOpacity(index: number, opacity: number) {
const strokes = [...props.strokes]
const strokes = [...node.value.strokes]
strokes[index] = { ...strokes[index], opacity: Math.max(0, Math.min(1, opacity / 100)) }
store.updateNodeWithUndo(props.nodeId, { strokes }, 'Change stroke')
store.updateNodeWithUndo(node.value.id, { strokes }, 'Change stroke')
}
function toggleVisibility(index: number) {
const strokes = [...props.strokes]
const strokes = [...node.value.strokes]
strokes[index] = { ...strokes[index], visible: !strokes[index].visible }
store.updateNodeWithUndo(props.nodeId, { strokes }, 'Change stroke')
store.updateNodeWithUndo(node.value.id, { strokes }, 'Change stroke')
}
function add() {
const stroke: Stroke = { color: { r: 0, g: 0, b: 0, a: 1 }, weight: 1, opacity: 1, visible: true, align: 'CENTER' }
store.updateNodeWithUndo(props.nodeId, { strokes: [...props.strokes, stroke] }, 'Add stroke')
store.updateNodeWithUndo(node.value.id, { strokes: [...node.value.strokes, stroke] }, 'Add stroke')
}
function remove(index: number) {
store.updateNodeWithUndo(props.nodeId, { strokes: props.strokes.filter((_, i) => i !== index) }, 'Remove stroke')
store.updateNodeWithUndo(node.value.id, { strokes: node.value.strokes.filter((_, i) => i !== index) }, 'Remove stroke')
}
</script>
@ -55,7 +54,7 @@ function remove(index: number) {
<label class="mb-1 block text-[11px] text-muted">Stroke</label>
<button class="cursor-pointer rounded border-none bg-transparent px-1 text-base leading-none text-muted hover:bg-hover hover:text-surface" @click="add">+</button>
</div>
<div v-for="(stroke, i) in strokes" :key="i" class="group flex items-center gap-1.5 py-0.5">
<div v-for="(stroke, i) in node.strokes" :key="i" class="group flex items-center gap-1.5 py-0.5">
<ColorPicker :color="stroke.color" @update="updateColor(i, $event)" />
<input
class="min-w-0 flex-1 border-none bg-transparent font-mono text-xs text-surface outline-none"
@ -79,11 +78,11 @@ function remove(index: number) {
</button>
<button class="cursor-pointer border-none bg-transparent p-0 text-sm leading-none text-muted hover:text-surface" @click="remove(i)">−</button>
</div>
<div v-if="strokes.length > 0" class="mt-1 flex items-center gap-1.5">
<div v-if="node.strokes.length > 0" class="mt-1 flex items-center gap-1.5">
<ScrubInput
class="w-16"
icon="W"
:model-value="strokes[0].weight"
:model-value="node.strokes[0].weight"
:min="0"
@update:model-value="updateWeight(0, $event)"
/>

View file

@ -6,10 +6,7 @@ import ScrubInput from '../ScrubInput.vue'
import { useNodeProps } from '../../composables/use-node-props'
import { listFamilies, loadFont } from '../../engine/fonts'
import type { SceneNode } from '../../engine/scene-graph'
const { node } = defineProps<{ node: SceneNode }>()
const { store, updateProp, commitProp } = useNodeProps()
const { store, node, updateProp, commitProp } = useNodeProps()
const fontPickerOpen = ref(false)
const fontSearch = ref('')
@ -38,14 +35,14 @@ const WEIGHTS = [
]
const currentWeightLabel = computed(() =>
WEIGHTS.find((w) => w.value === node.fontWeight)?.label ?? 'Regular'
WEIGHTS.find((w) => w.value === node.value.fontWeight)?.label ?? 'Regular'
)
type TextAlign = 'LEFT' | 'CENTER' | 'RIGHT'
async function openFontPicker() {
fontPickerOpen.value = true
fontSearch.value = node.fontFamily
fontSearch.value = node.value.fontFamily
if (fontFamilies.value.length === 0) {
fontFamilies.value = await listFamilies()
}
@ -57,25 +54,25 @@ async function openFontPicker() {
async function selectFamily(family: string) {
await loadFont(family, currentWeightLabel.value)
store.updateNodeWithUndo(node.id, { fontFamily: family }, 'Change font')
store.updateNodeWithUndo(node.value.id, { fontFamily: family }, 'Change font')
store.requestRender()
fontPickerOpen.value = false
}
async function selectWeight(weight: number) {
const label = WEIGHTS.find((w) => w.value === weight)?.label ?? 'Regular'
await loadFont(node.fontFamily, label)
store.updateNodeWithUndo(node.id, { fontWeight: weight }, 'Change font weight')
await loadFont(node.value.fontFamily, label)
store.updateNodeWithUndo(node.value.id, { fontWeight: weight }, 'Change font weight')
store.requestRender()
}
function setAlign(align: 'LEFT' | 'CENTER' | 'RIGHT') {
store.updateNodeWithUndo(node.id, { textAlignHorizontal: align }, 'Change text alignment')
store.updateNodeWithUndo(node.value.id, { textAlignHorizontal: align }, 'Change text alignment')
store.requestRender()
}
onMounted(async () => {
await loadFont(node.fontFamily, currentWeightLabel.value)
await loadFont(node.value.fontFamily, currentWeightLabel.value)
})
</script>

View file

@ -1,9 +1,13 @@
import { computed } from 'vue'
import { useEditorStore } from '../stores/editor'
import type { SceneNode } from '../engine/scene-graph'
export function useNodeProps() {
const store = useEditorStore()
const node = computed(() => store.selectedNode.value!)
const nodes = computed(() => store.selectedNodes.value)
function updateProp(key: string, value: number | string) {
if (store.selectedNodes.value.length > 1) {
@ -29,5 +33,5 @@ export function useNodeProps() {
}
}
return { store, updateProp, commitProp }
return { store, node, nodes, updateProp, commitProp }
}