Files
slipmatz-web/app/components/designer/DesignerToolbar.vue
Frank John Begornia 8a9703c24a Redesign toolbar as horizontal layout above canvas
- Move DesignerToolbar from sidebar to top of canvas container
- Convert vertical toolbar to horizontal layout with grouped controls
- Add element buttons (Text, Circle, Rectangle, Image) on left
- Inline color pickers for Fill and Stroke with compact design
- Horizontal zoom controls (-, percentage, +, Reset)
- Clear Canvas button positioned on the right
- Use border separators between control groups
- Remove vertical card layout in favor of toolbar-style UI
2025-11-16 01:24:50 +08:00

217 lines
6.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, ref, watch } from "vue";
const props = defineProps<{
onAddText: () => void;
onAddCircle: () => void;
onAddRectangle: () => void;
onClear: () => void;
onImportImage: (file: File) => Promise<void>;
onFillChange: (fill: string) => void;
onStrokeChange: (stroke: string) => void;
activeFill: string | null;
activeStroke: string | null;
canStyleSelection: boolean;
zoom: number;
minZoom: number;
maxZoom: number;
onZoomChange: (zoom: number) => void;
onZoomIn: () => void;
onZoomOut: () => void;
onZoomReset: () => void;
}>();
const emit = defineEmits<{
(e: "request-image"): void;
}>();
const fileInput = ref<HTMLInputElement | null>(null);
const fillValue = ref(props.activeFill ?? "#111827");
const strokeValue = ref(props.activeStroke ?? "#3b82f6");
const zoomSliderValue = ref(Math.round(props.zoom * 100));
watch(
() => props.activeFill,
(next) => {
fillValue.value = next ?? "#111827";
}
);
watch(
() => props.activeStroke,
(next) => {
strokeValue.value = next ?? "#3b82f6";
}
);
watch(
() => props.zoom,
(next) => {
zoomSliderValue.value = Math.round(next * 100);
}
);
const openFilePicker = () => {
if (!fileInput.value) {
return;
}
fileInput.value.value = "";
fileInput.value.click();
};
const handleFileChange = async (event: Event) => {
const input = event.target as HTMLInputElement;
const files = input.files;
if (!files || !files.length) {
return;
}
const [file] = files;
if (!file) {
return;
}
await props.onImportImage(file);
};
const stylingDisabled = computed(() => !props.canStyleSelection);
const handleFillChange = (event: Event) => {
const input = event.target as HTMLInputElement;
const value = input.value;
fillValue.value = value;
props.onFillChange(value);
};
const handleStrokeChange = (event: Event) => {
const input = event.target as HTMLInputElement;
const value = input.value;
strokeValue.value = value;
props.onStrokeChange(value);
};
const handleZoomInput = (event: Event) => {
const input = event.target as HTMLInputElement;
const value = Number(input.value);
if (Number.isNaN(value)) {
return;
}
zoomSliderValue.value = value;
props.onZoomChange(value / 100);
};
const zoomLabel = computed(() => `${Math.round(props.zoom * 100)}%`);
</script>
<template>
<div class="flex flex-wrap items-center gap-2 border-b border-slate-800/60 bg-slate-900/50 px-4 py-3">
<!-- Add Elements Group -->
<div class="flex items-center gap-1 border-r border-slate-700/50 pr-3">
<button
type="button"
class="flex h-9 w-9 items-center justify-center rounded-lg text-lg font-bold text-slate-300 transition hover:bg-slate-800"
title="Add Text"
@click="props.onAddText"
>
T
</button>
<button
type="button"
class="flex h-9 w-9 items-center justify-center rounded-lg text-xl font-bold text-slate-300 transition hover:bg-slate-800"
title="Add Circle"
@click="props.onAddCircle"
>
</button>
<button
type="button"
class="flex h-9 w-9 items-center justify-center rounded-lg text-xl font-bold text-slate-300 transition hover:bg-slate-800"
title="Add Rectangle"
@click="props.onAddRectangle"
>
</button>
<button
type="button"
class="flex h-9 w-9 items-center justify-center rounded-lg text-xl font-bold text-slate-300 transition hover:bg-slate-800"
title="Upload Image"
@click="openFilePicker"
>
🖼
</button>
</div>
<!-- Color Pickers Group -->
<div class="flex items-center gap-2 border-r border-slate-700/50 pr-3">
<label class="flex items-center gap-1.5" title="Fill Color">
<span class="text-xs text-slate-400">Fill</span>
<input
type="color"
class="h-8 w-12 cursor-pointer rounded border border-slate-700/70 bg-slate-800/80 p-0.5"
:disabled="stylingDisabled"
:value="fillValue"
@input="handleFillChange"
/>
</label>
<label class="flex items-center gap-1.5" title="Stroke Color">
<span class="text-xs text-slate-400">Stroke</span>
<input
type="color"
class="h-8 w-12 cursor-pointer rounded border border-slate-700/70 bg-slate-800/80 p-0.5"
:disabled="stylingDisabled"
:value="strokeValue"
@input="handleStrokeChange"
/>
</label>
</div>
<!-- Zoom Controls -->
<div class="flex items-center gap-2 border-r border-slate-700/50 pr-3">
<button
type="button"
class="flex h-8 w-8 items-center justify-center rounded border border-slate-700/60 bg-slate-800/80 text-sm font-bold text-slate-100 transition hover:bg-slate-700"
title="Zoom Out"
@click="props.onZoomOut"
>
</button>
<span class="min-w-12 text-center text-xs font-medium text-slate-300">
{{ zoomLabel }}
</span>
<button
type="button"
class="flex h-8 w-8 items-center justify-center rounded border border-slate-700/60 bg-slate-800/80 text-sm font-bold text-slate-100 transition hover:bg-slate-700"
title="Zoom In"
@click="props.onZoomIn"
>
+
</button>
<button
type="button"
class="rounded border border-slate-700/60 bg-slate-800/80 px-2 py-1 text-xs font-medium text-slate-200 transition hover:bg-slate-700"
title="Reset Zoom"
@click="props.onZoomReset"
>
Reset
</button>
</div>
<!-- Clear Button -->
<button
type="button"
class="ml-auto rounded border border-red-500/40 px-3 py-1.5 text-xs font-medium text-red-300 transition hover:bg-red-500/10"
@click="props.onClear"
>
Clear Canvas
</button>
<!-- Hidden File Input -->
<input
ref="fileInput"
type="file"
accept="image/png,image/jpeg,image/webp,image/svg+xml"
class="hidden"
@change="handleFileChange"
/>
</div>
</template>