first commit

This commit is contained in:
Frank John Begornia
2025-11-02 00:23:22 +08:00
commit e2955debb7
18 changed files with 13383 additions and 0 deletions

View File

@@ -0,0 +1,238 @@
<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>
<section class="rounded-2xl border border-slate-800/60 bg-slate-900/80 p-4 shadow-lg shadow-slate-950/30">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold uppercase tracking-wide text-slate-400">
Canvas Tools
</h3>
<button
type="button"
class="rounded-lg border border-red-500/40 px-3 py-1 text-xs font-medium text-red-300 transition hover:bg-red-500/10"
@click="props.onClear"
>
Clear Canvas
</button>
</div>
<div class="mt-4 grid grid-cols-2 gap-3 md:grid-cols-4">
<button
type="button"
class="flex flex-col items-center justify-center rounded-xl border border-slate-700/60 bg-slate-800/80 px-4 py-6 text-sm font-medium text-slate-100 transition hover:border-sky-500/70 hover:bg-sky-500/10 hover:text-sky-200"
@click="props.onAddText"
>
<span class="mb-2 text-3xl font-semibold">T</span>
<span class="text-xs uppercase tracking-wide">Add Text</span>
</button>
<button
type="button"
class="flex flex-col items-center justify-center rounded-xl border border-slate-700/60 bg-slate-800/80 px-4 py-6 text-sm font-medium text-slate-100 transition hover:border-sky-500/70 hover:bg-sky-500/10 hover:text-sky-200"
@click="props.onAddCircle"
>
<span class="mb-2 text-3xl font-semibold"></span>
<span class="text-xs uppercase tracking-wide">Add Circle</span>
</button>
<button
type="button"
class="flex flex-col items-center justify-center rounded-xl border border-slate-700/60 bg-slate-800/80 px-4 py-6 text-sm font-medium text-slate-100 transition hover:border-sky-500/70 hover:bg-sky-500/10 hover:text-sky-200"
@click="props.onAddRectangle"
>
<span class="mb-2 text-3xl font-semibold"></span>
<span class="text-xs uppercase tracking-wide">Add Rectangle</span>
</button>
<button
type="button"
class="flex flex-col items-center justify-center rounded-xl border border-slate-700/60 bg-slate-800/80 px-4 py-6 text-sm font-medium text-slate-100 transition hover:border-sky-500/70 hover:bg-sky-500/10 hover:text-sky-200"
@click="openFilePicker"
>
<span class="mb-2 text-3xl font-semibold"></span>
<span class="text-xs uppercase tracking-wide">Upload Image</span>
</button>
</div>
<input
ref="fileInput"
type="file"
accept="image/png,image/jpeg,image/webp,image/svg+xml"
class="hidden"
@change="handleFileChange"
/>
<div class="mt-5 rounded-xl border border-slate-800/60 bg-slate-900/70 p-4">
<div class="flex items-center justify-between gap-3">
<h4 class="text-xs font-semibold uppercase tracking-wide text-slate-400">
Styling
</h4>
<span
v-if="stylingDisabled"
class="text-[11px] font-medium uppercase tracking-wide text-slate-500"
>
Select an object
</span>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
Fill
<input
type="color"
class="h-10 w-full cursor-pointer rounded-lg border border-slate-700/70 bg-slate-800/80 p-1"
:disabled="stylingDisabled"
:value="fillValue"
@input="handleFillChange"
/>
</label>
<label class="flex flex-col gap-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
Stroke
<input
type="color"
class="h-10 w-full cursor-pointer rounded-lg border border-slate-700/70 bg-slate-800/80 p-1"
:disabled="stylingDisabled"
:value="strokeValue"
@input="handleStrokeChange"
/>
</label>
</div>
</div>
<div class="mt-5 rounded-xl border border-slate-800/60 bg-slate-900/70 p-4">
<div class="flex items-center justify-between gap-3">
<h4 class="text-xs font-semibold uppercase tracking-wide text-slate-400">
View
</h4>
<span class="text-[11px] font-medium uppercase tracking-wide text-slate-300">
{{ zoomLabel }}
</span>
</div>
<div class="mt-4 flex items-center gap-3">
<button
type="button"
class="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-700/60 bg-slate-800/80 text-lg font-bold text-slate-100 transition hover:border-sky-500/70 hover:bg-sky-500/10"
@click="props.onZoomOut"
>
</button>
<input
type="range"
class="flex-1 accent-sky-500"
:min="Math.round(props.minZoom * 100)"
:max="Math.round(props.maxZoom * 100)"
step="5"
:value="zoomSliderValue"
@input="handleZoomInput"
/>
<button
type="button"
class="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-700/60 bg-slate-800/80 text-lg font-bold text-slate-100 transition hover:border-sky-500/70 hover:bg-sky-500/10"
@click="props.onZoomIn"
>
+
</button>
</div>
<button
type="button"
class="mt-3 w-full rounded-lg border border-slate-700/60 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-slate-200 transition hover:border-sky-500/70 hover:bg-sky-500/10 hover:text-sky-200"
@click="props.onZoomReset"
>
Reset Zoom
</button>
</div>
</section>
</template>