Page properties panel with canvas background color picker

When nothing is selected, the right panel shows Page section with
a color picker for the canvas background (like Figma). The page
color is stored in reactive state and passed to the renderer.
This commit is contained in:
Danila Poyarkov 2026-02-28 11:42:13 +03:00
parent 159ee6b7c9
commit ffd305c125
6 changed files with 45 additions and 3 deletions

1
components.d.ts vendored
View file

@ -40,6 +40,7 @@ declare module 'vue' {
IconLucideSearch: typeof import('~icons/lucide/search')['default']
LayersPanel: typeof import('./src/components/LayersPanel.vue')['default']
LayoutSection: typeof import('./src/components/properties/LayoutSection.vue')['default']
PageSection: typeof import('./src/components/properties/PageSection.vue')['default']
PositionSection: typeof import('./src/components/properties/PositionSection.vue')['default']
PropertiesPanel: typeof import('./src/components/PropertiesPanel.vue')['default']
ScrubInput: typeof import('./src/components/ScrubInput.vue')['default']

View file

@ -6,6 +6,7 @@ import { useEditorStore } from '../stores/editor'
import AppearanceSection from './properties/AppearanceSection.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'
@ -62,6 +63,8 @@ const multiCount = computed(() => store.selectedNodes.value.length)
</div>
</div>
<div v-else class="px-3 py-4 text-xs text-muted">No selection</div>
<div v-else class="flex-1 overflow-y-auto pb-4">
<PageSection />
</div>
</aside>
</template>

View file

@ -0,0 +1,34 @@
<script setup lang="ts">
import ColorPicker from '../ColorPicker.vue'
import { useEditorStore } from '../../stores/editor'
import { colorToHexRaw, parseColor } from '../../engine/color'
import type { Color } from '../../types'
const store = useEditorStore()
function updateColor(color: Color) {
store.state.pageColor = color
store.requestRender()
}
function updateHex(hex: string) {
const color = parseColor(hex.startsWith('#') ? hex : `#${hex}`)
if (!color) return
updateColor(color)
}
</script>
<template>
<div class="border-b border-border px-3 py-2">
<label class="mb-1.5 block text-[11px] text-muted">Page</label>
<div class="flex items-center gap-1.5">
<ColorPicker :color="store.state.pageColor" @update="updateColor" />
<input
class="min-w-0 flex-1 border-none bg-transparent font-mono text-xs text-surface outline-none"
:value="colorToHexRaw(store.state.pageColor)"
@change="updateHex(($event.target as HTMLInputElement).value)"
/>
</div>
</div>
</template>

View file

@ -56,6 +56,7 @@ export function useCanvas(canvasRef: Ref<HTMLCanvasElement | null>, store: Edito
renderer.viewportWidth = canvasRef.value?.clientWidth ?? 0
renderer.viewportHeight = canvasRef.value?.clientHeight ?? 0
renderer.showRulers = !new URLSearchParams(window.location.search).has('no-rulers')
renderer.pageColor = store.state.pageColor
renderer.render(store.graph, store.state.selectedIds, {
editingTextId: store.state.editingTextId,
marquee: store.state.marquee,

View file

@ -63,6 +63,7 @@ export class SkiaRenderer {
viewportWidth = 0
viewportHeight = 0
showRulers = true
pageColor = CANVAS_BG_COLOR
private selColor(alpha = 1) {
return this.ck.Color4f(SELECTION_COLOR.r, SELECTION_COLOR.g, SELECTION_COLOR.b, alpha)
@ -127,7 +128,7 @@ export class SkiaRenderer {
render(graph: SceneGraph, selectedIds: Set<string>, overlays: RenderOverlays = {}): void {
const canvas = this.surface.getCanvas()
canvas.clear(this.ck.Color4f(CANVAS_BG_COLOR.r, CANVAS_BG_COLOR.g, CANVAS_BG_COLOR.b, 1))
canvas.clear(this.ck.Color4f(this.pageColor.r, this.pageColor.g, this.pageColor.b, 1))
// Scene layer (world coordinates)
canvas.save()

View file

@ -1,6 +1,7 @@
import { reactive, shallowRef, computed } from 'vue'
import { DEFAULT_SHAPE_FILL, DEFAULT_FRAME_FILL } from '../constants'
import { DEFAULT_SHAPE_FILL, DEFAULT_FRAME_FILL, CANVAS_BG_COLOR } from '../constants'
import type { Color } from '../types'
import {
parseFigmaClipboard,
importClipboardNodes,
@ -88,6 +89,7 @@ export function createEditorStore() {
penCursorX: null as number | null,
penCursorY: null as number | null,
panX: 0,
pageColor: { ...CANVAS_BG_COLOR } as Color,
panY: 0,
zoom: 1,
renderVersion: 0