[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

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