This commit is contained in:
242
app/components/designer/DesignerToolbar.vue
Normal file
242
app/components/designer/DesignerToolbar.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<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;
|
||||
onBackgroundChange: (background: string) => void;
|
||||
activeFill: string | null;
|
||||
activeStroke: string | null;
|
||||
activeBackground: string;
|
||||
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 backgroundValue = ref(props.activeBackground ?? "#ffffff");
|
||||
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.activeBackground,
|
||||
(next) => {
|
||||
backgroundValue.value = next ?? "#ffffff";
|
||||
}
|
||||
);
|
||||
|
||||
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 handleBackgroundChange = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const value = input.value;
|
||||
backgroundValue.value = value;
|
||||
props.onBackgroundChange(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-200 bg-slate-50 px-4 py-3">
|
||||
<!-- Add Elements Group -->
|
||||
<div class="flex items-center gap-1 border-r border-slate-200 pr-3">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-9 w-9 items-center justify-center rounded-lg text-lg font-bold text-slate-700 transition hover:bg-slate-200"
|
||||
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-700 transition hover:bg-slate-200"
|
||||
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-700 transition hover:bg-slate-200"
|
||||
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-700 transition hover:bg-slate-200"
|
||||
title="Upload Image"
|
||||
@click="openFilePicker"
|
||||
>
|
||||
🖼
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Color Pickers Group -->
|
||||
<div class="flex items-center gap-2 border-r border-slate-200 pr-3">
|
||||
<label class="flex items-center gap-1.5" title="Canvas Background">
|
||||
<span class="text-xs text-slate-600">Canvas</span>
|
||||
<input
|
||||
type="color"
|
||||
class="h-8 w-12 cursor-pointer rounded border border-slate-300 bg-white p-0.5"
|
||||
:value="backgroundValue"
|
||||
@input="handleBackgroundChange"
|
||||
/>
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5" title="Fill Color">
|
||||
<span class="text-xs text-slate-600">Fill</span>
|
||||
<input
|
||||
type="color"
|
||||
class="h-8 w-12 cursor-pointer rounded border border-slate-300 bg-white 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-600">Stroke</span>
|
||||
<input
|
||||
type="color"
|
||||
class="h-8 w-12 cursor-pointer rounded border border-slate-300 bg-white p-0.5"
|
||||
:disabled="stylingDisabled"
|
||||
:value="strokeValue"
|
||||
@input="handleStrokeChange"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Zoom Controls -->
|
||||
<div class="flex items-center gap-2 border-r border-slate-200 pr-3">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-8 w-8 items-center justify-center rounded border border-slate-300 bg-white text-sm font-bold text-slate-900 transition hover:bg-slate-50"
|
||||
title="Zoom Out"
|
||||
@click="props.onZoomOut"
|
||||
>
|
||||
–
|
||||
</button>
|
||||
<span class="min-w-12 text-center text-xs font-medium text-slate-700">
|
||||
{{ zoomLabel }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-8 w-8 items-center justify-center rounded border border-slate-300 bg-white text-sm font-bold text-slate-900 transition hover:bg-slate-50"
|
||||
title="Zoom In"
|
||||
@click="props.onZoomIn"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded border border-slate-300 bg-white px-2 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
title="Reset Zoom"
|
||||
@click="props.onZoomReset"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Clear Button -->
|
||||
<button
|
||||
type="button"
|
||||
class="ml-auto rounded border border-rose-300 bg-rose-50 px-3 py-1.5 text-xs font-medium text-rose-700 transition hover:bg-rose-100"
|
||||
@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>
|
||||
|
||||
Reference in New Issue
Block a user