[FEAT] Format code
This commit is contained in:
@@ -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 = '';
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { RouterLink } from 'vue-router';
|
||||
</script>
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { RouterLink } from 'vue-router';
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user