[FEAT] Improved UI

This commit is contained in:
2026-01-01 00:47:28 +01:00
parent 784c95555f
commit 89d369598f
19 changed files with 1192 additions and 976 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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);

View 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>