[FEAT] Format code

This commit is contained in:
2026-01-01 18:46:46 +01:00
parent 221dcb7072
commit 65bdc2974f
17 changed files with 561 additions and 528 deletions

View File

@@ -227,7 +227,7 @@
(e: 'removeSprite', id: string): void;
(e: 'removeSprites', ids: string[]): void;
(e: 'replaceSprite', id: string, file: File): void;
(e: 'addSprite', file: File): void;
(e: 'addSprite', file: File, index?: number): void;
(e: 'addSpriteWithResize', file: File): void;
(e: 'rotateSprite', id: string, angle: number): void;
(e: 'flipSprite', id: string, direction: 'horizontal' | 'vertical'): void;
@@ -263,6 +263,7 @@
handleTouchStart,
handleTouchMove,
findSpriteAtPosition,
findCellAtPosition,
calculateMaxDimensions,
} = useDragSprite({
sprites: computed(() => props.layers.find(l => l.id === props.activeLayerId)?.sprites ?? []),
@@ -293,6 +294,7 @@
const showContextMenu = ref(false);
const contextMenuX = ref(0);
const contextMenuY = ref(0);
const contextMenuIndex = ref<number | null>(null);
const contextMenuSpriteId = ref<string | null>(null);
const selectedSpriteIds = ref<Set<string>>(new Set());
const replacingSpriteId = ref<string | null>(null);
@@ -383,6 +385,7 @@
if (!pos) return;
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
contextMenuIndex.value = findCellAtPosition(pos.x, pos.y)?.index ?? null;
contextMenuSpriteId.value = clickedSprite?.id || null;
if (clickedSprite) {
@@ -529,13 +532,18 @@
if (replacingSpriteId.value) {
emit('replaceSprite', replacingSpriteId.value, file);
} else {
emit('addSprite', file);
if (contextMenuIndex.value !== null) {
emit('addSprite', file, contextMenuIndex.value);
} else {
emit('addSprite', file);
}
}
} else {
alert('Please select an image file.');
}
}
replacingSpriteId.value = null;
contextMenuIndex.value = null;
input.value = '';
};

View File

@@ -1,131 +1,107 @@
<template>
<Teleport to="body">
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" @click.self="close">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-md p-6 relative border border-gray-200 dark:border-gray-700">
<button @click="close" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ isLogin ? 'Welcome Back' : 'Create Account' }}</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ isLogin ? 'Sign in to access your projects' : 'Join to save and manage your spritesheets' }}
</p>
</div>
<form @submit.prevent="handleSubmit" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
<input
v-model="email"
type="email"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
placeholder="you@example.com"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label>
<input
v-model="password"
type="password"
required
minlength="8"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
placeholder="••••••••"
/>
</div>
<div v-if="!isLogin">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Confirm Password</label>
<input
v-model="passwordConfirm"
type="password"
required
minlength="8"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
placeholder="••••••••"
/>
</div>
<div v-if="error" class="text-red-500 text-sm text-center bg-red-50 dark:bg-red-900/20 p-2 rounded">
{{ error }}
</div>
<button
type="submit"
:disabled="loading"
class="w-full btn btn-primary py-2.5 flex justify-center items-center"
>
<i v-if="loading" class="fas fa-spinner fa-spin mr-2"></i>
{{ isLogin ? 'Sign In' : 'Create Account' }}
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-md p-6 relative border border-gray-200 dark:border-gray-700">
<button @click="close" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
</form>
<div class="mt-6 text-center text-sm text-gray-600 dark:text-gray-400">
{{ isLogin ? "Don't have an account?" : 'Already have an account?' }}
<button @click="toggleMode" class="text-indigo-600 dark:text-indigo-400 font-medium hover:underline">
{{ isLogin ? 'Sign up' : 'Sign in' }}
</button>
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ isLogin ? 'Welcome Back' : 'Create Account' }}</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ isLogin ? 'Sign in to access your projects' : 'Join to save and manage your spritesheets' }}
</p>
</div>
<form @submit.prevent="handleSubmit" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
<input v-model="email" type="email" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" placeholder="you@example.com" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label>
<input v-model="password" type="password" required minlength="8" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" placeholder="••••••••" />
</div>
<div v-if="!isLogin">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Confirm Password</label>
<input v-model="passwordConfirm" type="password" required minlength="8" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" placeholder="••••••••" />
</div>
<div v-if="error" class="text-red-500 text-sm text-center bg-red-50 dark:bg-red-900/20 p-2 rounded">
{{ error }}
</div>
<button type="submit" :disabled="loading" class="w-full btn btn-primary py-2.5 flex justify-center items-center">
<i v-if="loading" class="fas fa-spinner fa-spin mr-2"></i>
{{ isLogin ? 'Sign In' : 'Create Account' }}
</button>
</form>
<div class="mt-6 text-center text-sm text-gray-600 dark:text-gray-400">
{{ isLogin ? "Don't have an account?" : 'Already have an account?' }}
<button @click="toggleMode" class="text-indigo-600 dark:text-indigo-400 font-medium hover:underline">
{{ isLogin ? 'Sign up' : 'Sign in' }}
</button>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useAuthStore } from '@/stores/useAuthStore';
import { ref } from 'vue';
import { useAuthStore } from '@/stores/useAuthStore';
const props = defineProps<{
isOpen: boolean;
}>();
const props = defineProps<{
isOpen: boolean;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const authStore = useAuthStore();
const isLogin = ref(true);
const email = ref('');
const password = ref('');
const passwordConfirm = ref('');
const loading = ref(false);
const error = ref('');
const authStore = useAuthStore();
const isLogin = ref(true);
const email = ref('');
const password = ref('');
const passwordConfirm = ref('');
const loading = ref(false);
const error = ref('');
const close = () => {
emit('close');
error.value = '';
email.value = '';
password.value = '';
passwordConfirm.value = '';
};
const close = () => {
emit('close');
error.value = '';
email.value = '';
password.value = '';
passwordConfirm.value = '';
};
const toggleMode = () => {
isLogin.value = !isLogin.value;
error.value = '';
};
const toggleMode = () => {
isLogin.value = !isLogin.value;
error.value = '';
};
const handleSubmit = async () => {
loading.value = true;
error.value = '';
try {
if (isLogin.value) {
await authStore.login(email.value, password.value);
} else {
if (password.value !== passwordConfirm.value) {
throw new Error("Passwords don't match");
const handleSubmit = async () => {
loading.value = true;
error.value = '';
try {
if (isLogin.value) {
await authStore.login(email.value, password.value);
} else {
if (password.value !== passwordConfirm.value) {
throw new Error("Passwords don't match");
}
await authStore.register(email.value, password.value, passwordConfirm.value);
}
await authStore.register(email.value, password.value, passwordConfirm.value);
close();
} catch (e: any) {
error.value = e.message || 'An error occurred';
// Better PB error handling
if (e?.data?.message) error.value = e.data.message;
} finally {
loading.value = false;
}
close();
} catch (e: any) {
error.value = e.message || 'An error occurred';
// Better PB error handling
if(e?.data?.message) error.value = e.data.message;
} finally {
loading.value = false;
}
};
};
</script>

View File

@@ -5,7 +5,7 @@
<!-- Left Side: Logo & Navigation -->
<div class="flex items-center gap-8">
<NavbarLogo />
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center">
<NavbarLinks />
@@ -16,22 +16,16 @@
<div class="hidden md:flex items-center gap-5">
<!-- Auth & Projects -->
<template v-if="authStore.user">
<NavbarProjectActions
@open-save-modal="openSaveModal"
@open-new-project-modal="isNewProjectModalOpen = true"
@open-project-list="isProjectListOpen = true"
/>
<div class="h-4 w-px bg-gray-200 dark:bg-gray-700"></div>
<NavbarProjectActions @open-save-modal="openSaveModal" @open-new-project-modal="isNewProjectModalOpen = true" @open-project-list="isProjectListOpen = true" />
<!-- User Dropdown -->
<NavbarUserMenu @open-auth-modal="isAuthModalOpen = true" />
<div class="h-4 w-px bg-gray-200 dark:bg-gray-700"></div>
<!-- User Dropdown -->
<NavbarUserMenu @open-auth-modal="isAuthModalOpen = true" />
</template>
<div v-else>
<button @click="isAuthModalOpen = true" class="btn btn-primary btn-sm shadow-indigo-500/20 shadow-lg">
Login / Register
</button>
<button @click="isAuthModalOpen = true" class="btn btn-primary btn-sm shadow-indigo-500/20 shadow-lg">Login / Register</button>
</div>
<div class="h-4 w-px bg-gray-200 dark:bg-gray-700"></div>
@@ -50,36 +44,16 @@
</div>
<!-- Mobile Menu -->
<NavbarMobileMenu
:is-open="isMobileMenuOpen"
@close="isMobileMenuOpen = false"
@open-help="$emit('open-help')"
/>
<NavbarMobileMenu :is-open="isMobileMenuOpen" @close="isMobileMenuOpen = false" @open-help="$emit('open-help')" />
</nav>
<AuthModal
:is-open="isAuthModalOpen"
@close="isAuthModalOpen = false"
/>
<ProjectList
:is-open="isProjectListOpen"
@close="isProjectListOpen = false"
@open-project="handleOpenProject"
/>
<SaveProjectModal
:is-open="isSaveProjectModalOpen"
:initial-name="projectStore.currentProject?.name"
@close="isSaveProjectModalOpen = false"
@save="handleSaveProject"
/>
<NewProjectModal
:is-open="isNewProjectModalOpen"
@close="isNewProjectModalOpen = false"
@create="handleCreateNewProject"
/>
<AuthModal :is-open="isAuthModalOpen" @close="isAuthModalOpen = false" />
<ProjectList :is-open="isProjectListOpen" @close="isProjectListOpen = false" @open-project="handleOpenProject" />
<SaveProjectModal :is-open="isSaveProjectModalOpen" :initial-name="projectStore.currentProject?.name" @close="isSaveProjectModalOpen = false" @save="handleSaveProject" />
<NewProjectModal :is-open="isNewProjectModalOpen" @close="isNewProjectModalOpen = false" @create="handleCreateNewProject" />
</template>
<script setup lang="ts">
import { ref, toRef } from 'vue';
import { ref } from 'vue';
import DarkModeToggle from '../utilities/DarkModeToggle.vue';
import { useAuthStore } from '@/stores/useAuthStore';
import AuthModal from '@/components/auth/AuthModal.vue';
@@ -87,9 +61,7 @@
import SaveProjectModal from '@/components/project/SaveProjectModal.vue';
import NewProjectModal from '@/components/project/NewProjectModal.vue';
import { useProjectStore, type Project } from '@/stores/useProjectStore';
import { useSettingsStore } from '@/stores/useSettingsStore';
import { useExportLayers } from '@/composables/useExportLayers';
import { useLayers, createEmptyLayer } from '@/composables/useLayers';
import { useProjectManager } from '@/composables/useProjectManager';
// Sub-components
import NavbarLogo from './navbar/NavbarLogo.vue';
@@ -99,7 +71,7 @@
import NavbarSocials from './navbar/NavbarSocials.vue';
import NavbarMobileMenu from './navbar/NavbarMobileMenu.vue';
defineEmits(['open-help']);
defineEmits(['open-help']);
const isMobileMenuOpen = ref(false);
const isAuthModalOpen = ref(false);
@@ -109,66 +81,25 @@
const authStore = useAuthStore();
const projectStore = useProjectStore();
const settingsStore = useSettingsStore();
const { layers, columns, activeLayerId } = useLayers();
const { generateProjectJSON, loadProjectData } = useExportLayers(
layers,
columns,
toRef(settingsStore, 'negativeSpacingEnabled'),
activeLayerId,
toRef(settingsStore, 'backgroundColor'),
toRef(settingsStore, 'manualCellSizeEnabled'),
toRef(settingsStore, 'manualCellWidth'),
toRef(settingsStore, 'manualCellHeight')
);
const { createProject, openProject, saveProject } = useProjectManager();
const handleOpenProject = async (project: Project) => {
try {
if (project.data) {
await loadProjectData(project.data);
}
projectStore.currentProject = project;
} catch (e) {
console.error("Failed to open project", e);
alert("Failed to open project data");
}
await openProject(project);
};
const openSaveModal = () => {
isSaveProjectModalOpen.value = true;
}
isSaveProjectModalOpen.value = true;
};
const handleSaveProject = async (name: string) => {
try {
const data = await generateProjectJSON();
if (projectStore.currentProject && projectStore.currentProject.name === name) {
await projectStore.updateProject(projectStore.currentProject.id, data);
} else {
await projectStore.createProject(name, data);
}
} catch (e) {
console.error(e);
alert("Failed to save project");
}
try {
await saveProject(name);
} catch {
// Error handled in composable
}
};
const handleCreateNewProject = (config: { width: number; height: number; columns: number; rows: number }) => {
// 1. Reset Settings
settingsStore.setManualCellSize(config.width, config.height);
settingsStore.manualCellSizeEnabled = true;
// 2. Reset Layers
const newLayer = createEmptyLayer('Base');
layers.value = [newLayer];
activeLayerId.value = newLayer.id;
// 3. Set Columns
columns.value = config.columns;
// 4. Reset Project Store
projectStore.currentProject = null;
createProject(config);
};
</script>

View File

@@ -8,5 +8,5 @@
</template>
<script setup lang="ts">
import { RouterLink } from 'vue-router';
import { RouterLink } from 'vue-router';
</script>

View File

@@ -10,5 +10,5 @@
</template>
<script setup lang="ts">
import { RouterLink } from 'vue-router';
import { RouterLink } from 'vue-router';
</script>

View File

@@ -36,11 +36,11 @@
</template>
<script setup lang="ts">
import { RouterLink } from 'vue-router';
import { RouterLink } from 'vue-router';
defineProps<{
isOpen: boolean;
}>();
defineProps<{
isOpen: boolean;
}>();
defineEmits(['close', 'open-help']);
defineEmits(['close', 'open-help']);
</script>

View File

@@ -3,20 +3,13 @@
<div class="flex items-center gap-2 group">
<div class="text-right hidden lg:block">
<p class="text-[10px] uppercase tracking-wider font-bold text-gray-400 dark:text-gray-500 mb-[-2px]">Current Project</p>
<button
@click="$emit('open-save-modal')"
class="text-sm font-bold text-gray-800 dark:text-gray-200 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors truncate max-w-[160px]"
title="Rename Project"
>
<button @click="$emit('open-save-modal')" class="text-sm font-bold text-gray-800 dark:text-gray-200 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors truncate max-w-[160px]" title="Rename Project">
{{ projectStore.currentProject?.name || 'Untitled Project' }}
</button>
</div>
<!-- Mobile/Tablet simplified view -->
<button
@click="$emit('open-save-modal')"
class="lg:hidden text-sm font-bold text-gray-800 dark:text-gray-200 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors truncate max-w-[120px]"
>
<button @click="$emit('open-save-modal')" class="lg:hidden text-sm font-bold text-gray-800 dark:text-gray-200 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors truncate max-w-[120px]">
{{ projectStore.currentProject?.name || 'Untitled' }}
</button>
</div>
@@ -24,23 +17,23 @@
<div class="h-8 w-px bg-gray-200 dark:bg-gray-700 mx-1"></div>
<div class="flex items-center gap-1">
<button @click="$emit('open-save-modal')" class="w-8 h-8 rounded-lg flex items-center justify-center text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 transition-all" title="Save Project">
<i class="fas fa-save"></i>
<button @click="$emit('open-save-modal')" class="w-8 h-8 rounded-lg flex items-center justify-center text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 transition-all" title="Save Project">
<i class="fas fa-save"></i>
</button>
<button @click="$emit('open-new-project-modal')" class="w-8 h-8 rounded-lg flex items-center justify-center text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 transition-all" title="New Project">
<i class="fas fa-plus"></i>
<i class="fas fa-plus"></i>
</button>
<button @click="$emit('open-project-list')" class="w-8 h-8 rounded-lg flex items-center justify-center text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 transition-all" title="My Projects">
<i class="fas fa-folder-open"></i>
<i class="fas fa-folder-open"></i>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useProjectStore } from '@/stores/useProjectStore';
import { useProjectStore } from '@/stores/useProjectStore';
defineEmits(['open-save-modal', 'open-project-list', 'open-new-project-modal']);
defineEmits(['open-save-modal', 'open-project-list', 'open-new-project-modal']);
const projectStore = useProjectStore();
const projectStore = useProjectStore();
</script>

View File

@@ -14,7 +14,7 @@
</template>
<script setup lang="ts">
import DarkModeToggle from '../../utilities/DarkModeToggle.vue';
import DarkModeToggle from '../../utilities/DarkModeToggle.vue';
defineEmits(['open-help']);
defineEmits(['open-help']);
</script>

View File

@@ -1,76 +1,68 @@
<template>
<div v-if="authStore.user" class="flex items-center gap-4">
<div class="relative" ref="userMenuRef">
<button
@click="isUserMenuOpen = !isUserMenuOpen"
class="w-9 h-9 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 text-white font-bold text-sm flex items-center justify-center shadow-md hover:shadow-lg transition-all border-2 border-white dark:border-gray-800"
>
{{ userInitials }}
</button>
<button @click="isUserMenuOpen = !isUserMenuOpen" class="w-9 h-9 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 text-white font-bold text-sm flex items-center justify-center shadow-md hover:shadow-lg transition-all border-2 border-white dark:border-gray-800">
{{ userInitials }}
</button>
<transition
enter-active-class="transition ease-out duration-200"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<div v-if="isUserMenuOpen" class="absolute right-0 mt-2 w-56 bg-white dark:bg-gray-900 rounded-xl shadow-xl border border-gray-200 dark:border-gray-800 overflow-hidden z-50">
<div class="p-4 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50">
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold tracking-wider mb-1">Signed in as</p>
<p class="text-sm font-medium text-gray-900 dark:text-white truncate" :title="authStore.user.email">{{ authStore.user.email }}</p>
</div>
<div class="p-1">
<button
@click="handleLogout"
class="w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600 dark:hover:text-red-400 rounded-lg transition-colors flex items-center gap-2"
>
<i class="fas fa-sign-out-alt"></i> Sign out
</button>
</div>
<transition
enter-active-class="transition ease-out duration-200"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<div v-if="isUserMenuOpen" class="absolute right-0 mt-2 w-56 bg-white dark:bg-gray-900 rounded-xl shadow-xl border border-gray-200 dark:border-gray-800 overflow-hidden z-50">
<div class="p-4 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50">
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold tracking-wider mb-1">Signed in as</p>
<p class="text-sm font-medium text-gray-900 dark:text-white truncate" :title="authStore.user.email">{{ authStore.user.email }}</p>
</div>
</transition>
<div class="p-1">
<button @click="handleLogout" class="w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600 dark:hover:text-red-400 rounded-lg transition-colors flex items-center gap-2">
<i class="fas fa-sign-out-alt"></i> Sign out
</button>
</div>
</div>
</transition>
</div>
</div>
<div v-else>
<button @click="$emit('open-auth-modal')" class="btn btn-primary btn-sm shadow-indigo-500/20 shadow-lg">
Login / Register
</button>
<button @click="$emit('open-auth-modal')" class="btn btn-primary btn-sm shadow-indigo-500/20 shadow-lg">Login / Register</button>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useAuthStore } from '@/stores/useAuthStore';
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useAuthStore } from '@/stores/useAuthStore';
defineEmits(['open-auth-modal']);
defineEmits(['open-auth-modal']);
const authStore = useAuthStore();
const isUserMenuOpen = ref(false);
const userMenuRef = ref<HTMLElement | null>(null);
const authStore = useAuthStore();
const isUserMenuOpen = ref(false);
const userMenuRef = ref<HTMLElement | null>(null);
const userInitials = computed(() => {
if (!authStore.user?.email) return '??';
return authStore.user.email.substring(0, 2).toUpperCase();
});
const userInitials = computed(() => {
if (!authStore.user?.email) return '??';
return authStore.user.email.substring(0, 2).toUpperCase();
});
const closeUserMenu = (e: MouseEvent) => {
const closeUserMenu = (e: MouseEvent) => {
if (userMenuRef.value && !userMenuRef.value.contains(e.target as Node)) {
isUserMenuOpen.value = false;
isUserMenuOpen.value = false;
}
};
};
const handleLogout = () => {
const handleLogout = () => {
isUserMenuOpen.value = false;
authStore.logout();
};
};
onMounted(() => {
onMounted(() => {
document.addEventListener('click', closeUserMenu);
});
});
onUnmounted(() => {
onUnmounted(() => {
document.removeEventListener('click', closeUserMenu);
});
});
</script>

View File

@@ -4,58 +4,31 @@
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl p-6 w-full max-w-sm border border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-gray-100">New Project</h2>
<form @submit.prevent="handleSubmit">
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 uppercase tracking-wider">Sprite Width</label>
<div class="relative">
<input
v-model.number="width"
type="number"
min="1"
required
class="w-full pl-3 pr-8 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
/>
<input v-model.number="width" type="number" min="1" required class="w-full pl-3 pr-8 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" />
<span class="absolute right-3 top-2 text-gray-400 text-xs">px</span>
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 uppercase tracking-wider">Sprite Height</label>
<div class="relative">
<input
v-model.number="height"
type="number"
min="1"
required
class="w-full pl-3 pr-8 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
/>
<input v-model.number="height" type="number" min="1" required class="w-full pl-3 pr-8 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" />
<span class="absolute right-3 top-2 text-gray-400 text-xs">px</span>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 mb-6">
<div>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 uppercase tracking-wider">Columns</label>
<input
v-model.number="columns"
type="number"
min="1"
max="20"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
/>
<input v-model.number="columns" type="number" min="1" max="20" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" />
</div>
<div>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 uppercase tracking-wider">Rows</label>
<input
v-model.number="rows"
type="number"
min="1"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
title="Rows are dynamic but this sets an initial expectation"
/>
<input v-model.number="rows" type="number" min="1" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" title="Rows are dynamic but this sets an initial expectation" />
</div>
</div>
@@ -70,46 +43,49 @@
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { ref, computed, watch } from 'vue';
const props = defineProps<{
isOpen: boolean;
}>();
const props = defineProps<{
isOpen: boolean;
}>();
const emit = defineEmits<{
(e: 'close'): void;
(e: 'create', config: { width: number; height: number; columns: number; rows: number }): void;
}>();
const emit = defineEmits<{
(e: 'close'): void;
(e: 'create', config: { width: number; height: number; columns: number; rows: number }): void;
}>();
const width = ref(64);
const height = ref(64);
const columns = ref(8);
const rows = ref(8);
const width = ref(64);
const height = ref(64);
const columns = ref(8);
const rows = ref(8);
const isValid = computed(() => {
return width.value > 0 && height.value > 0 && columns.value > 0 && rows.value > 0;
});
const isValid = computed(() => {
return width.value > 0 && height.value > 0 && columns.value > 0 && rows.value > 0;
});
// Reset to defaults when opened
watch(() => props.isOpen, (val) => {
if (val) {
width.value = 64;
height.value = 64;
columns.value = 8;
rows.value = 8;
}
});
// Reset to defaults when opened
watch(
() => props.isOpen,
val => {
if (val) {
width.value = 64;
height.value = 64;
columns.value = 8;
rows.value = 8;
}
}
);
const close = () => emit('close');
const handleSubmit = () => {
if (isValid.value) {
emit('create', {
width: width.value,
height: height.value,
columns: columns.value,
rows: rows.value
});
close();
}
};
const close = () => emit('close');
const handleSubmit = () => {
if (isValid.value) {
emit('create', {
width: width.value,
height: height.value,
columns: columns.value,
rows: rows.value,
});
close();
}
};
</script>

View File

@@ -1,104 +1,102 @@
<template>
<Teleport to="body">
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" @click.self="close">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-2xl p-6 relative border border-gray-200 dark:border-gray-700 max-h-[80vh] flex flex-col">
<div class="flex items-center justify-between mb-6 shrink-0">
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">My Projects</h2>
<button @click="close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar p-1">
<div v-if="loading" class="flex justify-center py-8">
<i class="fas fa-spinner fa-spin text-2xl text-indigo-500"></i>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-2xl p-6 relative border border-gray-200 dark:border-gray-700 max-h-[80vh] flex flex-col">
<div class="flex items-center justify-between mb-6 shrink-0">
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">My Projects</h2>
<button @click="close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
</div>
<div v-else-if="projects.length === 0" class="text-center py-12 text-gray-500 dark:text-gray-400">
<i class="fas fa-folder-open text-4xl mb-3 opacity-50"></i>
<p>No projects found. Save your work to see it here!</p>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar p-1">
<div v-if="loading" class="flex justify-center py-8">
<i class="fas fa-spinner fa-spin text-2xl text-indigo-500"></i>
</div>
<div v-else class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div
v-for="project in projects"
:key="project.id"
class="group p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 hover:border-indigo-500 dark:hover:border-indigo-500 transition-all cursor-pointer relative"
@click="selectProject(project)"
>
<div class="flex items-start justify-between mb-2">
<div v-else-if="projects.length === 0" class="text-center py-12 text-gray-500 dark:text-gray-400">
<i class="fas fa-folder-open text-4xl mb-3 opacity-50"></i>
<p>No projects found. Save your work to see it here!</p>
</div>
<div v-else class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div v-for="project in projects" :key="project.id" class="group p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 hover:border-indigo-500 dark:hover:border-indigo-500 transition-all cursor-pointer relative" @click="selectProject(project)">
<div class="flex items-start justify-between mb-2">
<div class="flex-1 min-w-0">
<h3 class="font-bold text-gray-900 dark:text-gray-100 truncate pr-2">{{ project.name }}</h3>
<h3 class="font-bold text-gray-900 dark:text-gray-100 truncate pr-2">{{ project.name }}</h3>
</div>
<div class="opacity-0 group-hover:opacity-100 transition-opacity">
<button @click.stop="deleteProject(project.id)" class="text-gray-400 hover:text-red-500 p-1" title="Delete Project">
<i class="fas fa-trash"></i>
</button>
<button @click.stop="deleteProject(project.id)" class="text-gray-400 hover:text-red-500 p-1" title="Delete Project">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 flex flex-col gap-1">
<span><i class="fas fa-clock mr-1"></i> Updated: {{ formatDate(project.updated) }}</span>
<span><i class="fas fa-layer-group mr-1"></i> Layers: {{ project.data?.layers?.length || 0 }}</span>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 flex flex-col gap-1">
<span><i class="fas fa-clock mr-1"></i> Updated: {{ formatDate(project.updated) }}</span>
<span><i class="fas fa-layer-group mr-1"></i> Layers: {{ project.data?.layers?.length || 0 }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 flex justify-end shrink-0">
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 flex justify-end shrink-0">
<button @click="close" class="btn btn-secondary text-sm">Close</button>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { onMounted, toRefs, watch } from 'vue';
import { useProjectStore, type Project } from '@/stores/useProjectStore';
import { onMounted, toRefs, watch } from 'vue';
import { useProjectStore, type Project } from '@/stores/useProjectStore';
const props = defineProps<{
isOpen: boolean;
}>();
const props = defineProps<{
isOpen: boolean;
}>();
const emit = defineEmits<{
(e: 'close'): void;
(e: 'open-project', project: Project): void;
}>();
const emit = defineEmits<{
(e: 'close'): void;
(e: 'open-project', project: Project): void;
}>();
const projectStore = useProjectStore();
const { projects, isLoading: loading } = toRefs(projectStore);
const projectStore = useProjectStore();
const { projects, isLoading: loading } = toRefs(projectStore);
watch(() => props.isOpen, (isOpen) => {
if (isOpen) {
projectStore.fetchProjects();
}
});
watch(
() => props.isOpen,
isOpen => {
if (isOpen) {
projectStore.fetchProjects();
}
}
);
onMounted(() => {
if (props.isOpen) {
projectStore.fetchProjects();
}
});
const close = () => emit('close');
const selectProject = (project: Project) => {
emit('open-project', project);
close();
};
const deleteProject = async (id: string) => {
await projectStore.deleteProject(id);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
onMounted(() => {
if (props.isOpen) {
projectStore.fetchProjects();
}
});
};
const close = () => emit('close');
const selectProject = (project: Project) => {
emit('open-project', project);
close();
};
const deleteProject = async (id: string) => {
await projectStore.deleteProject(id);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
</script>

View File

@@ -1,64 +1,57 @@
<template>
<Teleport to="body">
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" @click.self="close">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl p-6 w-full max-w-sm border border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-gray-100">Save Project</h2>
<form @submit.prevent="handleSubmit">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Project Name</label>
<input
v-model="projectName"
type="text"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
placeholder="My Awesome Spritesheet"
ref="inputRef"
/>
</div>
<div class="flex justify-end gap-3">
<button type="button" @click="close" class="btn btn-secondary">Cancel</button>
<button type="submit" class="btn btn-primary" :disabled="!projectName.trim()">Save</button>
</div>
</form>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl p-6 w-full max-w-sm border border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-gray-100">Save Project</h2>
<form @submit.prevent="handleSubmit">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Project Name</label>
<input v-model="projectName" type="text" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" placeholder="My Awesome Spritesheet" ref="inputRef" />
</div>
<div class="flex justify-end gap-3">
<button type="button" @click="close" class="btn btn-secondary">Cancel</button>
<button type="submit" class="btn btn-primary" :disabled="!projectName.trim()">Save</button>
</div>
</form>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue';
import { ref, watch, nextTick } from 'vue';
const props = defineProps<{
isOpen: boolean;
initialName?: string;
}>();
const props = defineProps<{
isOpen: boolean;
initialName?: string;
}>();
const emit = defineEmits<{
(e: 'close'): void;
(e: 'save', name: string): void;
}>();
const emit = defineEmits<{
(e: 'close'): void;
(e: 'save', name: string): void;
}>();
const projectName = ref('');
const inputRef = ref<HTMLInputElement | null>(null);
const projectName = ref('');
const inputRef = ref<HTMLInputElement | null>(null);
watch(
() => props.isOpen,
(isOpen) => {
if (isOpen) {
projectName.value = props.initialName || '';
nextTick(() => {
inputRef.value?.focus();
inputRef.value?.select();
});
watch(
() => props.isOpen,
isOpen => {
if (isOpen) {
projectName.value = props.initialName || '';
nextTick(() => {
inputRef.value?.focus();
inputRef.value?.select();
});
}
}
}
);
);
const close = () => emit('close');
const handleSubmit = () => {
const close = () => emit('close');
const handleSubmit = () => {
if (projectName.value.trim()) {
emit('save', projectName.value);
close();
emit('save', projectName.value);
close();
}
};
};
</script>