Appearance section: opacity + corner radius + visibility toggle

- Opacity with blend icon, corner radius with radius icon
- Eye/eye-off toggle in section header
- Layers panel: hidden layers dimmed (opacity-50) with permanent
  eye-off indicator
- Removed duplicate corner radius from LayoutSection
This commit is contained in:
Danila Poyarkov 2026-02-28 09:20:31 +03:00
parent bfe9726035
commit 2af2a280c6
5 changed files with 48 additions and 15 deletions

2
components.d.ts vendored
View file

@ -20,11 +20,13 @@ declare module 'vue' {
IconLucideAlignLeft: typeof import('~icons/lucide/align-left')['default']
IconLucideAlignRight: typeof import('~icons/lucide/align-right')['default']
IconLucideBaseline: typeof import('~icons/lucide/baseline')['default']
IconLucideBlend: typeof import('~icons/lucide/blend')['default']
IconLucideCheck: typeof import('~icons/lucide/check')['default']
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideEye: typeof import('~icons/lucide/eye')['default']
IconLucideEyeOff: typeof import('~icons/lucide/eye-off')['default']
IconLucideRadius: typeof import('~icons/lucide/radius')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
LayersPanel: typeof import('./src/components/LayersPanel.vue')['default']
LayoutSection: typeof import('./src/components/properties/LayoutSection.vue')['default']

View file

@ -218,13 +218,14 @@ function updateDropTarget(ev: PointerEvent) {
@select="onSelect"
>
<button
class="flex w-full cursor-pointer items-center gap-1 rounded border-none py-1 text-left text-xs"
class="group/row flex w-full cursor-pointer items-center gap-1 rounded border-none py-1 text-left text-xs"
:class="[
store.state.selectedIds.has(item.value.id)
? 'bg-accent text-white'
: 'bg-transparent text-surface hover:bg-hover',
dragging && dragNodeId === item.value.id ? 'opacity-30' : '',
dropIntoId === item.value.id ? 'ring-2 ring-accent ring-inset' : ''
dropIntoId === item.value.id ? 'ring-2 ring-accent ring-inset' : '',
!item.value.visible ? 'opacity-50' : ''
]"
:style="{ paddingLeft: `${8 + (item.level - 1) * 16}px` }"
@pointerdown.prevent="onPointerDown($event, item.value.id)"
@ -239,7 +240,11 @@ function updateDropTarget(ev: PointerEvent) {
</span>
<span v-else class="w-4 shrink-0" />
<component :is="nodeIcons[item.value.type] ?? IconSquare" class="size-3 shrink-0 opacity-70" />
<span class="truncate">{{ item.value.name }}</span>
<span class="min-w-0 flex-1 truncate">{{ item.value.name }}</span>
<icon-lucide-eye-off
v-if="!item.value.visible"
class="mr-1 size-3 shrink-0 text-muted"
/>
</button>
</TreeItem>
</div>

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 :opacity="store.selectedNodes.value[0]?.opacity ?? 1" />
<AppearanceSection v-if="store.selectedNodes.value[0]" :node="store.selectedNodes.value[0]" />
</div>
<!-- Single selection -->
@ -46,7 +46,7 @@ const multiCount = computed(() => store.selectedNodes.value.length)
<PositionSection :node="node" />
<LayoutSection :node="node" />
<AppearanceSection :opacity="node.opacity" />
<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" />

View file

@ -2,24 +2,54 @@
import ScrubInput from '../ScrubInput.vue'
import { useNodeProps } from '../../composables/use-node-props'
const { updateProp, commitProp } = useNodeProps()
import type { SceneNode } from '../../engine/scene-graph'
defineProps<{ opacity: number }>()
const { node } = defineProps<{ node: SceneNode }>()
const { store, updateProp, commitProp } = useNodeProps()
function toggleVisibility() {
store.updateNodeWithUndo(node.id, { visible: !node.visible }, 'Toggle visibility')
store.requestRender()
}
</script>
<template>
<div class="border-b border-border px-3 py-2">
<label class="mb-1.5 block text-[11px] text-muted">Appearance</label>
<div class="mb-1.5 flex items-center justify-between">
<label class="text-[11px] text-muted">Appearance</label>
<button
class="flex cursor-pointer items-center justify-center rounded border-none bg-transparent p-0.5 text-muted hover:bg-hover hover:text-surface"
:class="{ 'text-accent': !node.visible }"
title="Toggle visibility"
@click="toggleVisibility"
>
<icon-lucide-eye v-if="node.visible" class="size-3.5" />
<icon-lucide-eye-off v-else class="size-3.5" />
</button>
</div>
<div class="flex gap-1.5">
<ScrubInput
icon="⊘"
suffix="%"
:model-value="Math.round(opacity * 100)"
:model-value="Math.round(node.opacity * 100)"
:min="0"
:max="100"
@update:model-value="updateProp('opacity', $event / 100)"
@commit="(v: number, p: number) => commitProp('opacity', v / 100, p / 100)"
/>
>
<template #icon>
<icon-lucide-blend class="size-3" />
</template>
</ScrubInput>
<ScrubInput
:model-value="node.cornerRadius"
:min="0"
@update:model-value="updateProp('cornerRadius', $event)"
@commit="(v: number, p: number) => commitProp('cornerRadius', v, p)"
>
<template #icon>
<icon-lucide-radius class="size-3" />
</template>
</ScrubInput>
</div>
</div>
</template>

View file

@ -145,10 +145,6 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
</div>
</div>
</div>
<!-- Corner radius -->
<div class="mt-1.5 flex gap-1.5">
<ScrubInput icon="↻" :model-value="node.cornerRadius" :min="0" @update:model-value="updateProp('cornerRadius', $event)" @commit="(v: number, p: number) => commitProp('cornerRadius', v, p)" />
</div>
</div>
<!-- Auto Layout -->