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
This commit is contained in:
@@ -102,53 +102,108 @@ const zoomLabel = computed(() => `${Math.round(props.zoom * 100)}%`);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 flex-wrap items-center gap-2 border-b border-slate-800/60 bg-slate-900/50 px-4 py-3">
|
||||||
<div class="flex items-center justify-between">
|
<!-- Add Elements Group -->
|
||||||
<h3 class="text-sm font-semibold uppercase tracking-wide text-slate-400">
|
<div class="flex items-center gap-1 border-r border-slate-700/50 pr-3">
|
||||||
Canvas Tools
|
|
||||||
</h3>
|
|
||||||
<button
|
<button
|
||||||
type="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"
|
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"
|
@click="props.onClear"
|
||||||
>
|
>
|
||||||
Clear Canvas
|
Clear Canvas
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<div class="mt-4 grid grid-cols-2 gap-3 md:grid-cols-4">
|
<!-- Hidden File Input -->
|
||||||
<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
|
<input
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
type="file"
|
type="file"
|
||||||
@@ -156,83 +211,6 @@ const zoomLabel = computed(() => `${Math.round(props.zoom * 100)}%`);
|
|||||||
class="hidden"
|
class="hidden"
|
||||||
@change="handleFileChange"
|
@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>
|
||||||
<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>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -379,6 +379,25 @@ const handleCheckout = async () => {
|
|||||||
@select="handleTemplateSelect"
|
@select="handleTemplateSelect"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DesignerPreview
|
||||||
|
:preview-url="previewUrl"
|
||||||
|
:template-label="templateLabel"
|
||||||
|
:production-pixels="productionPixelSize"
|
||||||
|
:is-exporting="isExporting || isDesignLoading"
|
||||||
|
:is-checkout-pending="isCheckoutPending"
|
||||||
|
:checkout-price="DESIGN_PRICE_USD"
|
||||||
|
:checkout-error="checkoutError"
|
||||||
|
@export="handleExport"
|
||||||
|
@download-preview="downloadPreview"
|
||||||
|
@download-production="downloadProduction"
|
||||||
|
@checkout="handleCheckout"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<div
|
||||||
|
class="rounded-3xl border border-slate-800/60 bg-linear-to-br from-slate-900 via-slate-950 to-black shadow-2xl shadow-slate-950/60"
|
||||||
|
>
|
||||||
<DesignerToolbar
|
<DesignerToolbar
|
||||||
:on-add-text="addTextbox"
|
:on-add-text="addTextbox"
|
||||||
:on-add-circle="() => addShape('circle')"
|
:on-add-circle="() => addShape('circle')"
|
||||||
@@ -398,26 +417,7 @@ const handleCheckout = async () => {
|
|||||||
:on-zoom-out="zoomOut"
|
:on-zoom-out="zoomOut"
|
||||||
:on-zoom-reset="resetZoom"
|
:on-zoom-reset="resetZoom"
|
||||||
/>
|
/>
|
||||||
|
<div class="p-6">
|
||||||
<DesignerPreview
|
|
||||||
:preview-url="previewUrl"
|
|
||||||
:template-label="templateLabel"
|
|
||||||
:production-pixels="productionPixelSize"
|
|
||||||
:is-exporting="isExporting || isDesignLoading"
|
|
||||||
:is-checkout-pending="isCheckoutPending"
|
|
||||||
:checkout-price="DESIGN_PRICE_USD"
|
|
||||||
:checkout-error="checkoutError"
|
|
||||||
@export="handleExport"
|
|
||||||
@download-preview="downloadPreview"
|
|
||||||
@download-production="downloadProduction"
|
|
||||||
@checkout="handleCheckout"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-6">
|
|
||||||
<div
|
|
||||||
class="rounded-3xl border border-slate-800/60 bg-linear-to-br from-slate-900 via-slate-950 to-black p-6 shadow-2xl shadow-slate-950/60"
|
|
||||||
>
|
|
||||||
<DesignerCanvas
|
<DesignerCanvas
|
||||||
:size="displaySize"
|
:size="displaySize"
|
||||||
:background-color="selectedTemplate.backgroundColor"
|
:background-color="selectedTemplate.backgroundColor"
|
||||||
@@ -431,6 +431,7 @@ const handleCheckout = async () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user