[FEAT] Improved UI
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative border-3 border-dashed rounded-2xl p-8 sm:p-12 text-center transition-all duration-300 cursor-pointer group overflow-hidden"
|
||||
class="relative border-3 border-dashed rounded-2xl p-8 sm:p-12 text-center cursor-pointer group overflow-hidden"
|
||||
:class="{
|
||||
'border-blue-400 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/40 scale-[1.02]': isDragging,
|
||||
'border-blue-400 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/40': isDragging,
|
||||
'border-gray-300 bg-gray-50/50 hover:border-blue-400 hover:bg-blue-50/80 dark:border-gray-600 dark:bg-gray-800/30 dark:hover:border-blue-400 dark:hover:bg-blue-900/30': !isDragging,
|
||||
}"
|
||||
@dragenter.prevent="isDragging = true"
|
||||
@@ -12,12 +12,12 @@
|
||||
@click="openFileDialog"
|
||||
data-rybbit-event="file-upload-area"
|
||||
>
|
||||
<div class="absolute inset-0 bg-blue-400/0 group-hover:bg-blue-400/5 transition-all duration-300"></div>
|
||||
<div class="absolute inset-0 bg-blue-400/0 group-hover:bg-blue-400/5"></div>
|
||||
|
||||
<input ref="fileInput" type="file" multiple accept="image/*,.json" class="hidden" @change="handleFileChange" />
|
||||
|
||||
<div class="relative z-10">
|
||||
<div class="mb-6 transform transition-transform duration-300" :class="isDragging ? 'scale-110' : 'group-hover:scale-105'">
|
||||
<div class="mb-6" :class="isDragging ? 'scale-110' : ''">
|
||||
<div class="w-20 h-20 sm:w-24 sm:h-24 mx-auto mb-4 bg-blue-100 dark:bg-blue-900/50 rounded-2xl flex items-center justify-center shadow-lg">
|
||||
<i class="fas fa-cloud-upload-alt text-4xl sm:text-5xl text-blue-600 dark:text-blue-400"></i>
|
||||
</div>
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="h-px flex-1 bg-gray-300 dark:bg-gray-600"></div>
|
||||
</div>
|
||||
|
||||
<button class="px-8 py-3.5 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white font-semibold rounded-xl shadow-lg hover:shadow-xl transition-all transform hover:scale-105 flex items-center justify-center gap-3 mx-auto" data-rybbit-event="select-files">
|
||||
<button class="px-8 py-3.5 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white font-semibold rounded-xl shadow-lg hover:shadow-xl flex items-center justify-center gap-3 mx-auto" data-rybbit-event="select-files">
|
||||
<i class="fas fa-folder-open text-lg"></i>
|
||||
<span>Browse Files</span>
|
||||
</button>
|
||||
|
||||
@@ -29,137 +29,13 @@
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<div class="space-y-3 w-full max-w-full overflow-hidden">
|
||||
<!-- Compact tip banner -->
|
||||
<div class="bg-cyan-50/80 dark:bg-cyan-900/20 rounded-md px-3 py-2 border border-cyan-100 dark:border-cyan-800/50 flex items-center gap-2">
|
||||
<i class="fas fa-lightbulb text-xs text-cyan-500 dark:text-cyan-400"></i>
|
||||
<p class="text-xs text-cyan-700 dark:text-cyan-300"><span class="font-medium">Tip:</span> Right-click sprites for quick actions • Hold Ctrl/Cmd to multi-select • Delete key removes selection</p>
|
||||
</div>
|
||||
|
||||
<!-- Compact Toolbar -->
|
||||
<section class="w-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<div class="flex flex-wrap items-center gap-0.5 px-1.5 py-1">
|
||||
<!-- Selection Tools -->
|
||||
<div class="flex items-center">
|
||||
<Tooltip text="Select multiple sprites at once. Also works with Ctrl/Cmd+Click.">
|
||||
<button
|
||||
@click="isMultiSelectMode = !isMultiSelectMode"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', isMultiSelectMode ? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-object-group mr-1"></i>Multi
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Show blue borders around selected sprites for visibility.">
|
||||
<button
|
||||
@click="showActiveBorder = !showActiveBorder"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', showActiveBorder ? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-vector-square mr-1"></i>Borders
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="w-px h-4 bg-gray-200 dark:bg-gray-600 mx-1"></div>
|
||||
|
||||
<!-- Display Options -->
|
||||
<div class="flex items-center">
|
||||
<Tooltip text="Disable anti-aliasing for crisp pixel art rendering.">
|
||||
<button
|
||||
@click="settingsStore.pixelPerfect = !settingsStore.pixelPerfect"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', settingsStore.pixelPerfect ? 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-th mr-1"></i>Pixel
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Drag sprites between cells to swap their positions.">
|
||||
<button
|
||||
@click="allowCellSwap = !allowCellSwap"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', allowCellSwap ? 'bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-exchange-alt mr-1"></i>Swap
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Show ghost overlays of all sprites for alignment comparison.">
|
||||
<button
|
||||
@click="showAllSprites = !showAllSprites"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', showAllSprites ? 'bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-clone mr-1"></i>Compare
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="w-px h-4 bg-gray-200 dark:bg-gray-600 mx-1"></div>
|
||||
|
||||
<!-- Canvas Options -->
|
||||
<div class="flex items-center">
|
||||
<Tooltip text="Add padding around sprites to prevent bleeding artifacts.">
|
||||
<button
|
||||
@click="settingsStore.negativeSpacingEnabled = !settingsStore.negativeSpacingEnabled"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', settingsStore.negativeSpacingEnabled ? 'bg-teal-100 dark:bg-teal-900/40 text-teal-700 dark:text-teal-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-compress-alt mr-1"></i>Spacing
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Show checkerboard pattern to visualize transparent areas.">
|
||||
<button
|
||||
@click="settingsStore.checkerboardEnabled = !settingsStore.checkerboardEnabled"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', settingsStore.checkerboardEnabled ? 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-200' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-chess-board mr-1"></i>Grid
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Display X,Y offset coordinates on each sprite.">
|
||||
<button
|
||||
@click="showOffsetLabels = !showOffsetLabels"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', showOffsetLabels ? 'bg-cyan-100 dark:bg-cyan-900/40 text-cyan-700 dark:text-cyan-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-tag mr-1"></i>Labels
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="w-px h-4 bg-gray-200 dark:bg-gray-600 mx-1"></div>
|
||||
|
||||
<!-- Background Color (Compact) -->
|
||||
<div class="flex items-center gap-1.5 px-1">
|
||||
<span class="text-[10px] font-medium text-gray-400 uppercase">Bg</span>
|
||||
<select v-model="bgSelectValue" class="px-1.5 py-0.5 text-xs border border-gray-200 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-700 dark:text-gray-200 outline-none cursor-pointer hover:border-gray-300">
|
||||
<option value="transparent">None</option>
|
||||
<option value="#ffffff">White</option>
|
||||
<option value="#000000">Black</option>
|
||||
<option value="#f9fafb">Gray</option>
|
||||
<option value="custom">Pick</option>
|
||||
</select>
|
||||
<div v-if="bgSelectValue === 'custom'" class="relative w-5 h-5 rounded overflow-hidden border border-gray-300 dark:border-gray-600">
|
||||
<input type="color" v-model="customColor" @input="settingsStore.setBackgroundColor(customColor)" class="absolute -top-1 -left-1 w-8 h-8 cursor-pointer p-0 border-0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<!-- Zoom Controls (Compact) -->
|
||||
<div class="flex items-center bg-gray-100 dark:bg-gray-700 rounded px-1 py-0.5">
|
||||
<button @click="zoomOut" class="p-1 hover:bg-white dark:hover:bg-gray-600 text-gray-500 dark:text-gray-400 rounded transition-all cursor-pointer" title="Zoom Out">
|
||||
<i class="fas fa-search-minus text-[10px]"></i>
|
||||
</button>
|
||||
<span class="px-1.5 text-[10px] font-mono text-gray-500 dark:text-gray-400 min-w-[4ch] text-center">{{ Math.round(zoom * 100) }}%</span>
|
||||
<button @click="zoomIn" class="p-1 hover:bg-white dark:hover:bg-gray-600 text-gray-500 dark:text-gray-400 rounded transition-all cursor-pointer" title="Zoom In">
|
||||
<i class="fas fa-search-plus text-[10px]"></i>
|
||||
</button>
|
||||
<button @click="resetZoom" class="p-1 hover:bg-white dark:hover:bg-gray-600 text-gray-500 dark:text-gray-400 rounded transition-all cursor-pointer ml-0.5" title="Reset Zoom">
|
||||
<i class="fas fa-compress-arrows-alt text-[10px]"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="h-full w-full flex flex-col p-4">
|
||||
<div class="relative bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm overflow-auto max-h-[calc(100vh-340px)] min-h-[400px] w-full">
|
||||
<div class="canvas-container touch-manipulation relative inline-block min-w-full">
|
||||
<div
|
||||
ref="gridContainerRef"
|
||||
:style="{
|
||||
transform: `scale(${zoom})`,
|
||||
transform: `scale(${props.zoom})`,
|
||||
transformOrigin: 'top left',
|
||||
width: `${gridDimensions.width}px`,
|
||||
height: `${gridDimensions.height}px`,
|
||||
@@ -294,10 +170,8 @@
|
||||
import { ref, onMounted, watch, onUnmounted, toRef, computed, nextTick } from 'vue';
|
||||
import { useSettingsStore } from '@/stores/useSettingsStore';
|
||||
import type { Sprite } from '@/types/sprites';
|
||||
import { useZoom } from '@/composables/useZoom';
|
||||
import { useDragSprite } from '@/composables/useDragSprite';
|
||||
import { useFileDrop } from '@/composables/useFileDrop';
|
||||
import Tooltip from '@/components/utilities/Tooltip.vue';
|
||||
|
||||
import type { Layer } from '@/types/sprites';
|
||||
|
||||
@@ -305,6 +179,12 @@
|
||||
layers: Layer[];
|
||||
activeLayerId: string;
|
||||
columns: number;
|
||||
zoom: number;
|
||||
isMultiSelectMode: boolean;
|
||||
showActiveBorder: boolean;
|
||||
allowCellSwap: boolean;
|
||||
showAllSprites: boolean;
|
||||
showOffsetLabels: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -324,23 +204,9 @@
|
||||
|
||||
const gridContainerRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const {
|
||||
zoom,
|
||||
increase: zoomIn,
|
||||
decrease: zoomOut,
|
||||
reset: resetZoom,
|
||||
} = useZoom({
|
||||
min: 0.5,
|
||||
max: 3,
|
||||
step: 0.25,
|
||||
initial: 1,
|
||||
});
|
||||
|
||||
const allowCellSwap = ref(false);
|
||||
|
||||
const getMousePosition = (event: MouseEvent, z?: number) => {
|
||||
if (!gridContainerRef.value) return null;
|
||||
const currentZoom = z ?? zoom.value;
|
||||
const currentZoom = z ?? props.zoom;
|
||||
const rect = gridContainerRef.value.getBoundingClientRect();
|
||||
const scaleX = gridContainerRef.value.offsetWidth / (rect.width / currentZoom);
|
||||
const scaleY = gridContainerRef.value.offsetHeight / (rect.height / currentZoom);
|
||||
@@ -367,8 +233,8 @@
|
||||
sprites: computed(() => props.layers.find(l => l.id === props.activeLayerId)?.sprites ?? []),
|
||||
layers: toRef(props, 'layers'),
|
||||
columns: toRef(props, 'columns'),
|
||||
zoom,
|
||||
allowCellSwap,
|
||||
zoom: toRef(props, 'zoom'),
|
||||
allowCellSwap: toRef(props, 'allowCellSwap'),
|
||||
negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'),
|
||||
manualCellSizeEnabled: toRef(settingsStore, 'manualCellSizeEnabled'),
|
||||
manualCellWidth: toRef(settingsStore, 'manualCellWidth'),
|
||||
@@ -389,8 +255,6 @@
|
||||
onAddSpriteWithResize: file => emit('addSpriteWithResize', file),
|
||||
});
|
||||
|
||||
const showAllSprites = ref(false);
|
||||
const showOffsetLabels = ref(false);
|
||||
const showContextMenu = ref(false);
|
||||
const contextMenuX = ref(0);
|
||||
const contextMenuY = ref(0);
|
||||
@@ -398,14 +262,14 @@
|
||||
const selectedSpriteIds = ref<Set<string>>(new Set());
|
||||
const replacingSpriteId = ref<string | null>(null);
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
const customColor = ref('#ffffff');
|
||||
const isMultiSelectMode = ref(false);
|
||||
const showActiveBorder = ref(true);
|
||||
|
||||
// Clear selection when toggling multi-select mode
|
||||
watch(isMultiSelectMode, () => {
|
||||
selectedSpriteIds.value.clear();
|
||||
});
|
||||
watch(
|
||||
() => props.isMultiSelectMode,
|
||||
() => {
|
||||
selectedSpriteIds.value.clear();
|
||||
}
|
||||
);
|
||||
|
||||
// Grid metrics
|
||||
const gridMetrics = computed(() => calculateMaxDimensions());
|
||||
@@ -467,52 +331,6 @@
|
||||
return '0 0';
|
||||
};
|
||||
|
||||
// Background select handling
|
||||
const presetBgColors = ['transparent', '#ffffff', '#000000', '#f9fafb'] as const;
|
||||
const isHexColor = (val: string) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(val);
|
||||
|
||||
if (isHexColor(settingsStore.backgroundColor) && !presetBgColors.includes(settingsStore.backgroundColor as any)) {
|
||||
customColor.value = settingsStore.backgroundColor;
|
||||
}
|
||||
|
||||
const isCustomMode = ref(isHexColor(settingsStore.backgroundColor) && !presetBgColors.includes(settingsStore.backgroundColor as any));
|
||||
|
||||
const bgSelectValue = computed<string>({
|
||||
get() {
|
||||
if (isCustomMode.value) {
|
||||
const val = settingsStore.backgroundColor;
|
||||
if (isHexColor(val)) {
|
||||
customColor.value = val;
|
||||
}
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
const val = settingsStore.backgroundColor;
|
||||
if (presetBgColors.includes(val as any)) return val;
|
||||
if (isHexColor(val)) {
|
||||
customColor.value = val;
|
||||
isCustomMode.value = true;
|
||||
return 'custom';
|
||||
}
|
||||
return 'transparent';
|
||||
},
|
||||
set(v: string) {
|
||||
if (v === 'custom') {
|
||||
isCustomMode.value = true;
|
||||
const fallback = '#ffffff';
|
||||
const current = settingsStore.backgroundColor;
|
||||
const fromStore = isHexColor(current) ? current : null;
|
||||
const fromLocal = isHexColor(customColor.value) ? customColor.value : null;
|
||||
const color = fromStore || fromLocal || fallback;
|
||||
customColor.value = color;
|
||||
settingsStore.setBackgroundColor(color);
|
||||
} else {
|
||||
isCustomMode.value = false;
|
||||
settingsStore.setBackgroundColor(v);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const startDrag = (event: MouseEvent) => {
|
||||
// If the click originated from an interactive element (button, link, input), ignore drag handling
|
||||
const target = event.target as HTMLElement;
|
||||
@@ -527,7 +345,7 @@
|
||||
// Handle right-click for context menu
|
||||
if ('button' in event && (event as MouseEvent).button === 2) {
|
||||
event.preventDefault();
|
||||
const pos = getMousePosition(event, zoom.value);
|
||||
const pos = getMousePosition(event, props.zoom);
|
||||
if (!pos) return;
|
||||
|
||||
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
|
||||
@@ -556,12 +374,12 @@
|
||||
if ('button' in event && (event as MouseEvent).button !== 0) return;
|
||||
|
||||
// Handle selection logic for left click
|
||||
const pos = getMousePosition(event, zoom.value);
|
||||
const pos = getMousePosition(event, props.zoom);
|
||||
if (pos) {
|
||||
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
|
||||
if (clickedSprite) {
|
||||
// Selection logic with multi-select mode check
|
||||
if (event.ctrlKey || event.metaKey || isMultiSelectMode.value) {
|
||||
if (event.ctrlKey || event.metaKey || props.isMultiSelectMode) {
|
||||
// Toggle selection
|
||||
if (selectedSpriteIds.value.has(clickedSprite.id)) {
|
||||
selectedSpriteIds.value.delete(clickedSprite.id);
|
||||
@@ -707,12 +525,6 @@
|
||||
});
|
||||
|
||||
// Watch for background color changes
|
||||
watch(
|
||||
() => settingsStore.backgroundColor,
|
||||
async () => {
|
||||
await nextTick();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<div
|
||||
ref="previewContainerRef"
|
||||
class="relative touch-manipulation inline-block"
|
||||
:class="{ 'ring-2 ring-blue-500 ring-offset-2': isDragOver }"
|
||||
:style="{
|
||||
transform: `scale(${zoom})`,
|
||||
transformOrigin: 'top left',
|
||||
@@ -24,6 +25,9 @@
|
||||
backgroundPosition: settingsStore.backgroundColor === 'transparent' ? '0 0, 0 10px, 10px -10px, -10px 0px' : '0 0',
|
||||
border: `1px solid ${settingsStore.darkMode ? '#4b5563' : '#e5e7eb'}`,
|
||||
}"
|
||||
@dragover.prevent="onDragOver"
|
||||
@dragleave="onDragLeave"
|
||||
@drop.prevent="onDrop"
|
||||
>
|
||||
<!-- Background sprites (dimmed for comparison) -->
|
||||
<template v-if="showAllSprites">
|
||||
@@ -81,6 +85,14 @@
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Drop zone overlay -->
|
||||
<div v-if="isDragOver" class="absolute inset-0 bg-blue-500/20 flex items-center justify-center pointer-events-none z-10 rounded-lg">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg px-4 py-3 flex items-center gap-2 border border-blue-300 dark:border-blue-600">
|
||||
<i class="fas fa-plus-circle text-blue-500"></i>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">Drop to add sprite at frame {{ currentFrameIndex + 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -247,6 +259,7 @@
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateSprite', id: string, x: number, y: number): void;
|
||||
(e: 'updateSpriteInLayer', layerId: string, spriteId: string, x: number, y: number): void;
|
||||
(e: 'dropSprite', layerId: string, frameIndex: number, files: File[]): void;
|
||||
}>();
|
||||
|
||||
const previewContainerRef = ref<HTMLDivElement | null>(null);
|
||||
@@ -283,6 +296,28 @@
|
||||
const isDraggable = ref(false);
|
||||
const repositionAllLayers = ref(false);
|
||||
const showAllSprites = ref(false);
|
||||
const isDragOver = ref(false);
|
||||
|
||||
// Drag and drop for new sprites
|
||||
const onDragOver = () => {
|
||||
isDragOver.value = true;
|
||||
};
|
||||
|
||||
const onDragLeave = () => {
|
||||
isDragOver.value = false;
|
||||
};
|
||||
|
||||
const onDrop = (event: DragEvent) => {
|
||||
isDragOver.value = false;
|
||||
|
||||
if (!event.dataTransfer?.files.length) return;
|
||||
|
||||
const files = Array.from(event.dataTransfer.files).filter(file => file.type.startsWith('image/'));
|
||||
|
||||
if (files.length === 0) return;
|
||||
|
||||
emit('dropSprite', props.activeLayerId, currentFrameIndex.value, files);
|
||||
};
|
||||
|
||||
const compositeFrames = computed<Sprite[]>(() => {
|
||||
// Show frames from the active layer for the thumbnail list
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
const cellHeight = ref(64);
|
||||
const sensitivity = ref(50);
|
||||
const removeEmpty = ref(true);
|
||||
const preserveCellSize = ref(false);
|
||||
const preserveCellSize = ref(true);
|
||||
const previewSprites = ref<SpritePreview[]>([]);
|
||||
const isProcessing = ref(false);
|
||||
const imageElement = ref<HTMLImageElement | null>(null);
|
||||
|
||||
87
src/components/layout/Navbar.vue
Normal file
87
src/components/layout/Navbar.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<nav class="sticky top-0 z-50 w-full glass border-b border-gray-200/50 dark:border-gray-800/50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Logo & Brand -->
|
||||
<div class="flex-shrink-0 flex items-center gap-3">
|
||||
<router-link to="/" class="flex items-center gap-2 group">
|
||||
<div class="w-8 h-8 rounded-lg bg-gray-900 dark:bg-white flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform duration-200">
|
||||
<i class="fas fa-layer-group text-white dark:text-gray-900 text-sm"></i>
|
||||
</div>
|
||||
<span class="font-bold text-xl tracking-tight text-gray-900 dark:text-white group-hover:opacity-80 transition-opacity"> Spritesheet generator </span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center gap-6">
|
||||
<div class="flex items-center gap-4 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
<router-link to="/" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Generator</router-link>
|
||||
<router-link to="/blog" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Blog</router-link>
|
||||
<router-link to="/about" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">About</router-link>
|
||||
<router-link to="/faq" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">FAQ</router-link>
|
||||
</div>
|
||||
|
||||
<div class="h-4 w-px bg-gray-200 dark:bg-gray-700"></div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="https://gitea.adhd.sh/root/spritesheet-generator" target="_blank" rel="noopener noreferrer" class="text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors" title="Source Code">
|
||||
<i class="fab fa-github text-lg"></i>
|
||||
</a>
|
||||
<a href="https://discord.gg/JTev3nzeDa" target="_blank" rel="noopener noreferrer" class="text-gray-500 hover:text-[#5865F2] dark:text-gray-400 dark:hover:text-[#5865F2] transition-colors" title="Discord Community">
|
||||
<i class="fab fa-discord text-lg"></i>
|
||||
</a>
|
||||
<button @click="$emit('open-help')" class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 transition-colors" title="Help">
|
||||
<i class="fas fa-question-circle text-lg"></i>
|
||||
</button>
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<div class="flex md:hidden">
|
||||
<DarkModeToggle />
|
||||
<button @click="isMobileMenuOpen = !isMobileMenuOpen" class="ml-4 p-2 rounded-md text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
|
||||
<i :class="isMobileMenuOpen ? 'fas fa-times' : 'fas fa-bars'" class="text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div v-show="isMobileMenuOpen" class="md:hidden border-t border-gray-200 dark:border-gray-800 bg-white/95 dark:bg-gray-950/95 backdrop-blur-xl">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1">
|
||||
<router-link to="/" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" @click="isMobileMenuOpen = false">Generator</router-link>
|
||||
<router-link to="/blog" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" @click="isMobileMenuOpen = false">Blog</router-link>
|
||||
<router-link to="/about" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" @click="isMobileMenuOpen = false">About</router-link>
|
||||
<router-link to="/faq" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" @click="isMobileMenuOpen = false">FAQ</router-link>
|
||||
<div class="border-t border-gray-200 dark:border-gray-800 my-2 pt-2 flex gap-4 px-3">
|
||||
<a href="https://gitea.adhd.sh/root/spritesheet-generator" target="_blank" class="text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white">
|
||||
<i class="fab fa-github text-xl"></i>
|
||||
</a>
|
||||
<a href="https://discord.gg/JTev3nzeDa" target="_blank" class="text-gray-500 hover:text-[#5865F2] dark:text-gray-400 dark:hover:text-[#5865F2]">
|
||||
<i class="fab fa-discord text-xl"></i>
|
||||
</a>
|
||||
<button
|
||||
@click="
|
||||
$emit('open-help');
|
||||
isMobileMenuOpen = false;
|
||||
"
|
||||
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
|
||||
>
|
||||
<i class="fas fa-question-circle text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import DarkModeToggle from '../utilities/DarkModeToggle.vue';
|
||||
|
||||
defineEmits(['open-help']);
|
||||
|
||||
const isMobileMenuOpen = ref(false);
|
||||
</script>
|
||||
Reference in New Issue
Block a user