[FEAT] Streamline UI
This commit is contained in:
@@ -85,5 +85,3 @@
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -33,32 +33,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Copy to Frame Modal -->
|
||||
<div v-if="showCopyToFrameModal" class="fixed inset-0 bg-black/50 z-[60] flex items-center justify-center" @click.self="closeCopyToFrameModal">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl p-6 min-w-[320px] max-w-md" @click.stop>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 mb-4">Copy sprite to frame</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Target frame</label>
|
||||
<select v-model.number="copyTargetFrame" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500">
|
||||
<option v-for="i in maxFrameCount" :key="i" :value="i - 1">Frame {{ i }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Target layer</label>
|
||||
<select v-model="copyTargetLayerId" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500">
|
||||
<option v-for="layer in props.layers" :key="layer.id" :value="layer.id">{{ layer.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button @click="closeCopyToFrameModal" class="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">Cancel</button>
|
||||
<button @click="confirmCopyToFrame" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CopyToFrameModal :is-open="showCopyToFrameModal" :layers="props.layers" :initial-layer-id="copyTargetLayerId" @close="closeCopyToFrameModal" @copy="confirmCopyToFrame" />
|
||||
</Teleport>
|
||||
|
||||
<div class="h-full w-full flex flex-col p-4">
|
||||
@@ -206,6 +181,7 @@
|
||||
import { useFileDrop } from '@/composables/useFileDrop';
|
||||
import { useGridMetrics } from '@/composables/useGridMetrics';
|
||||
import { useBackgroundStyles } from '@/composables/useBackgroundStyles';
|
||||
import CopyToFrameModal from './utilities/CopyToFrameModal.vue';
|
||||
|
||||
import type { Layer } from '@/types/sprites';
|
||||
|
||||
@@ -302,15 +278,9 @@
|
||||
|
||||
// Copy to frame modal state
|
||||
const showCopyToFrameModal = ref(false);
|
||||
const copyTargetFrame = ref(0);
|
||||
const copyTargetLayerId = ref(props.activeLayerId);
|
||||
const copySpriteId = ref<string | null>(null);
|
||||
|
||||
const maxFrameCount = computed(() => {
|
||||
const maxLen = Math.max(1, ...props.layers.map(l => l.sprites.length));
|
||||
return maxLen + 1; // Allow copying to one frame beyond current max
|
||||
});
|
||||
|
||||
// Clear selection when toggling multi-select mode
|
||||
watch(
|
||||
() => props.isMultiSelectMode,
|
||||
@@ -556,7 +526,6 @@
|
||||
if (contextMenuSpriteId.value) {
|
||||
copySpriteId.value = contextMenuSpriteId.value;
|
||||
copyTargetLayerId.value = props.activeLayerId;
|
||||
copyTargetFrame.value = 0;
|
||||
showCopyToFrameModal.value = true;
|
||||
showContextMenu.value = false;
|
||||
}
|
||||
@@ -567,9 +536,9 @@
|
||||
copySpriteId.value = null;
|
||||
};
|
||||
|
||||
const confirmCopyToFrame = () => {
|
||||
const confirmCopyToFrame = (targetLayerId: string, targetFrameIndex: number) => {
|
||||
if (copySpriteId.value) {
|
||||
emit('copySpriteToFrame', copySpriteId.value, copyTargetLayerId.value, copyTargetFrame.value);
|
||||
emit('copySpriteToFrame', copySpriteId.value, targetLayerId, targetFrameIndex);
|
||||
closeCopyToFrameModal();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -24,32 +24,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Copy to Frame Modal -->
|
||||
<div v-if="showCopyToFrameModal" class="fixed inset-0 bg-black/50 z-[60] flex items-center justify-center" @click.self="closeCopyToFrameModal">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl p-6 min-w-[320px] max-w-md" @click.stop>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 mb-4">Copy Sprite to Frame</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Target Frame</label>
|
||||
<select v-model.number="copyTargetFrame" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500">
|
||||
<option v-for="i in maxFrameCount" :key="i" :value="i - 1">Frame {{ i }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Target Layer</label>
|
||||
<select v-model="copyTargetLayerId" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500">
|
||||
<option v-for="layer in props.layers" :key="layer.id" :value="layer.id">{{ layer.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button @click="closeCopyToFrameModal" class="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">Cancel</button>
|
||||
<button @click="confirmCopyToFrame" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CopyToFrameModal :is-open="showCopyToFrameModal" :layers="props.layers" :initial-layer-id="copyTargetLayerId" @close="closeCopyToFrameModal" @copy="confirmCopyToFrame" />
|
||||
</Teleport>
|
||||
|
||||
<div class="spritesheet-preview w-full h-full" @click="hideContextMenu">
|
||||
@@ -308,6 +283,7 @@
|
||||
import { useAnimationFrames } from '@/composables/useAnimationFrames';
|
||||
import { useGridMetrics } from '@/composables/useGridMetrics';
|
||||
import { useBackgroundStyles } from '@/composables/useBackgroundStyles';
|
||||
import CopyToFrameModal from './utilities/CopyToFrameModal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
layers: Layer[];
|
||||
@@ -373,14 +349,8 @@
|
||||
|
||||
// Copy to frame modal state
|
||||
const showCopyToFrameModal = ref(false);
|
||||
const copyTargetFrame = ref(0);
|
||||
const copyTargetLayerId = ref(props.activeLayerId);
|
||||
|
||||
const maxFrameCount = computed(() => {
|
||||
const maxLen = Math.max(1, ...props.layers.map(l => l.sprites.length));
|
||||
return maxLen + 1; // Allow copying to one frame beyond current max
|
||||
});
|
||||
|
||||
// Drag and drop for new sprites
|
||||
const onDragOver = () => {
|
||||
isDragOver.value = true;
|
||||
@@ -713,7 +683,6 @@
|
||||
const openCopyToFrameModal = () => {
|
||||
if (contextMenuSpriteId.value) {
|
||||
copyTargetLayerId.value = contextMenuLayerId.value || props.activeLayerId;
|
||||
copyTargetFrame.value = 0;
|
||||
showCopyToFrameModal.value = true;
|
||||
showContextMenu.value = false;
|
||||
}
|
||||
@@ -723,9 +692,9 @@
|
||||
showCopyToFrameModal.value = false;
|
||||
};
|
||||
|
||||
const confirmCopyToFrame = () => {
|
||||
const confirmCopyToFrame = (targetLayerId: string, targetFrameIndex: number) => {
|
||||
if (contextMenuSpriteId.value) {
|
||||
emit('copySpriteToFrame', contextMenuSpriteId.value, copyTargetLayerId.value, copyTargetFrame.value);
|
||||
emit('copySpriteToFrame', contextMenuSpriteId.value, targetLayerId, targetFrameIndex);
|
||||
closeCopyToFrameModal();
|
||||
contextMenuSpriteId.value = null;
|
||||
}
|
||||
|
||||
@@ -1,58 +1,52 @@
|
||||
<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>
|
||||
<Modal :is-open="isOpen" @close="close" :title="isLogin ? 'Welcome Back' : 'Create Account'" :initialWidth="450">
|
||||
<div class="space-y-4">
|
||||
<div class="text-center mb-6">
|
||||
<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="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 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>
|
||||
</Teleport>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useAuthStore } from '@/stores/useAuthStore';
|
||||
import Modal from '@/components/utilities/Modal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -1,49 +1,47 @@
|
||||
<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">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" />
|
||||
<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" />
|
||||
<span class="absolute right-3 top-2 text-gray-400 text-xs">px</span>
|
||||
</div>
|
||||
<Modal :is-open="isOpen" @close="close" title="New project" :initialWidth="400">
|
||||
<div class="px-2 pt-2 pb-1">
|
||||
<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" />
|
||||
<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" />
|
||||
<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>
|
||||
<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" />
|
||||
</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" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<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" />
|
||||
</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" />
|
||||
</div>
|
||||
</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="!isValid">Create project</button>
|
||||
</div>
|
||||
</form>
|
||||
</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="!isValid">Create project</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Teleport>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import Modal from '@/components/utilities/Modal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -1,134 +1,126 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4" @click.self="close">
|
||||
<div class="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-full max-w-4xl overflow-hidden border border-gray-200 dark:border-gray-800 flex flex-col h-[85vh] animate-fade-in-up">
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-5 border-b border-gray-100 dark:border-gray-800 flex flex-col gap-4 bg-gray-50/50 dark:bg-gray-800/20">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100">My projects</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Manage your saved sprite sheets</p>
|
||||
</div>
|
||||
<button @click="close" class="w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<Modal :is-open="isOpen" @close="close" title="My Projects" :initialWidth="900" :initialHeight="800" noPadding>
|
||||
<div class="flex flex-col h-full bg-white dark:bg-gray-900">
|
||||
<!-- Search Header -->
|
||||
<div class="px-6 py-4 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/20">
|
||||
<div class="mb-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Manage your saved sprite sheets</p>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="relative">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search projects..."
|
||||
class="w-full pl-10 pr-4 py-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 dark:focus:border-indigo-500 outline-none transition-all text-sm"
|
||||
/>
|
||||
<div class="relative">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search projects..."
|
||||
class="w-full pl-10 pr-4 py-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 dark:focus:border-indigo-500 outline-none transition-all text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar p-6 bg-gray-50 dark:bg-black/20">
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center py-12">
|
||||
<div class="w-10 h-10 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mb-4"></div>
|
||||
<p class="text-gray-500 font-medium">Loading projects...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="projects.length === 0" class="flex flex-col items-center justify-center py-12 text-center h-full">
|
||||
<div class="w-20 h-20 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
|
||||
<i class="fas fa-folder-open text-3xl text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 mb-2">No projects yet</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 max-w-xs mb-6">Create your first project or load an example to get started with Sprite Sheet Generator.</p>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button @click="loadExample" class="btn btn-secondary flex items-center gap-2"><i class="fas fa-lightbulb"></i> Load Example</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar p-6 bg-gray-50 dark:bg-black/20">
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center py-12">
|
||||
<div class="w-10 h-10 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mb-4"></div>
|
||||
<p class="text-gray-500 font-medium">Loading projects...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="projects.length === 0" class="flex flex-col items-center justify-center py-12 text-center h-full">
|
||||
<div class="w-20 h-20 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
|
||||
<i class="fas fa-folder-open text-3xl text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 mb-2">No projects yet</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 max-w-xs mb-6">Create your first project or load an example to get started with Sprite Sheet Generator.</p>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button @click="loadExample" class="btn btn-secondary flex items-center gap-2"><i class="fas fa-lightbulb"></i> Load Example</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Search Results -->
|
||||
<div v-else-if="filteredProjects.length === 0" class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div class="w-16 h-16 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
|
||||
<i class="fas fa-search text-2xl text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<h3 class="text-md font-bold text-gray-900 dark:text-gray-100 mb-1">No matches found</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm">No projects match "{{ searchQuery }}"</p>
|
||||
</div>
|
||||
|
||||
<!-- Project List -->
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="project in filteredProjects"
|
||||
:key="project.id"
|
||||
class="group bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 hover:border-indigo-500 dark:hover:border-indigo-500 hover:shadow-md dark:hover:shadow-indigo-500/10 transition-all cursor-pointer overflow-hidden flex flex-col sm:flex-row h-auto sm:h-32"
|
||||
@click="selectProject(project)"
|
||||
>
|
||||
<!-- Left: Preview Stripe -->
|
||||
<div class="w-full sm:w-48 bg-gray-100 dark:bg-black/30 flex items-center justify-center relative overflow-hidden border-b sm:border-b-0 sm:border-r border-gray-100 dark:border-gray-700/50 p-2">
|
||||
<div class="absolute inset-0 bg-checkerboard opacity-50"></div>
|
||||
|
||||
<!-- Sprite Stack for Preview -->
|
||||
<div v-if="getProjectSprites(project, 1).length > 0" class="relative z-10 flex -space-x-4">
|
||||
<div
|
||||
v-for="(sprite, idx) in getProjectSprites(project, 3)"
|
||||
:key="idx"
|
||||
class="w-16 h-16 rounded-lg bg-white dark:bg-gray-700 border-2 border-white dark:border-gray-600 shadow-lg flex items-center justify-center overflow-hidden transform transition-transform group-hover:scale-105 group-hover:first:-rotate-6 group-hover:last:rotate-6"
|
||||
:style="{ zIndex: 10 - idx }"
|
||||
>
|
||||
<img :src="sprite.base64" class="w-full h-full object-contain" style="image-rendering: pixelated" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fallback Icon -->
|
||||
<div v-else class="relative z-10 w-16 h-16 rounded-lg bg-white dark:bg-gray-700 border-2 border-white dark:border-gray-600 shadow-lg flex items-center justify-center text-indigo-300 dark:text-indigo-400/50 font-bold text-2xl">
|
||||
{{ project.name.charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- content -->
|
||||
<div class="flex-1 p-4 flex flex-col justify-center min-w-0">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 class="font-bold text-gray-900 dark:text-gray-100 truncate text-lg mb-1 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">{{ project.name }}</h3>
|
||||
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span class="flex items-center gap-1"><i class="fas fa-layer-group opacity-70"></i> {{ project.data?.layers?.length || 0 }} layers</span>
|
||||
<span class="flex items-center gap-1"><i class="fas fa-border-all opacity-70"></i> {{ project.data?.columns }} columns</span>
|
||||
<span class="flex items-center gap-1"><i class="fas fa-images opacity-70"></i> {{ getSpriteCount(project) }} sprites</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click.stop="deleteProject(project.id)" class="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 p-2 rounded-lg transition-all" title="Delete Project">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto pt-3 flex items-center justify-between">
|
||||
<span class="text-xs text-gray-400"> Updated {{ formatDate(project.updated) }} </span>
|
||||
<span class="text-xs font-bold text-indigo-600 dark:text-indigo-400 flex items-center gap-1 transform translate-x-2 opacity-0 group-hover:translate-x-0 group-hover:opacity-100 transition-all"> Open Project <i class="fas fa-arrow-right"></i> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- No Search Results -->
|
||||
<div v-else-if="filteredProjects.length === 0" class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div class="w-16 h-16 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
|
||||
<i class="fas fa-search text-2xl text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<h3 class="text-md font-bold text-gray-900 dark:text-gray-100 mb-1">No matches found</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm">No projects match "{{ searchQuery }}"</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 border-t border-gray-100 dark:border-gray-800 flex justify-between items-center bg-white dark:bg-gray-900">
|
||||
<p class="text-xs text-gray-400" v-if="projects.length > 0">Showing {{ filteredProjects.length }} of {{ projects.length }} project{{ projects.length !== 1 ? 's' : '' }}</p>
|
||||
<div class="flex gap-2 ml-auto">
|
||||
<button v-if="projects.length > 0" @click="loadExample" class="text-xs font-medium text-gray-500 hover:text-indigo-600 dark:hover:text-indigo-400 px-3 py-2">Load Example</button>
|
||||
<button @click="close" class="btn btn-secondary text-sm">Close</button>
|
||||
<!-- Project List -->
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="project in filteredProjects"
|
||||
:key="project.id"
|
||||
class="group bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 hover:border-indigo-500 dark:hover:border-indigo-500 hover:shadow-md dark:hover:shadow-indigo-500/10 transition-all cursor-pointer overflow-hidden flex flex-col sm:flex-row h-auto sm:h-32"
|
||||
@click="selectProject(project)"
|
||||
>
|
||||
<!-- Left: Preview Stripe -->
|
||||
<div class="w-full sm:w-48 bg-gray-100 dark:bg-black/30 flex items-center justify-center relative overflow-hidden border-b sm:border-b-0 sm:border-r border-gray-100 dark:border-gray-700/50 p-2">
|
||||
<div class="absolute inset-0 bg-checkerboard opacity-50"></div>
|
||||
|
||||
<!-- Sprite Stack for Preview -->
|
||||
<div v-if="getProjectSprites(project, 1).length > 0" class="relative z-10 flex -space-x-4">
|
||||
<div
|
||||
v-for="(sprite, idx) in getProjectSprites(project, 3)"
|
||||
:key="idx"
|
||||
class="w-16 h-16 rounded-lg bg-white dark:bg-gray-700 border-2 border-white dark:border-gray-600 shadow-lg flex items-center justify-center overflow-hidden transform transition-transform group-hover:scale-105 group-hover:first:-rotate-6 group-hover:last:rotate-6"
|
||||
:style="{ zIndex: 10 - idx }"
|
||||
>
|
||||
<img :src="sprite.base64" class="w-full h-full object-contain" style="image-rendering: pixelated" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fallback Icon -->
|
||||
<div v-else class="relative z-10 w-16 h-16 rounded-lg bg-white dark:bg-gray-700 border-2 border-white dark:border-gray-600 shadow-lg flex items-center justify-center text-indigo-300 dark:text-indigo-400/50 font-bold text-2xl">
|
||||
{{ project.name.charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- content -->
|
||||
<div class="flex-1 p-4 flex flex-col justify-center min-w-0">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 class="font-bold text-gray-900 dark:text-gray-100 truncate text-lg mb-1 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">{{ project.name }}</h3>
|
||||
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span class="flex items-center gap-1"><i class="fas fa-layer-group opacity-70"></i> {{ project.data?.layers?.length || 0 }} layers</span>
|
||||
<span class="flex items-center gap-1"><i class="fas fa-border-all opacity-70"></i> {{ project.data?.columns }} columns</span>
|
||||
<span class="flex items-center gap-1"><i class="fas fa-images opacity-70"></i> {{ getSpriteCount(project) }} sprites</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click.stop="deleteProject(project.id)" class="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 p-2 rounded-lg transition-all" title="Delete Project">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto pt-3 flex items-center justify-between">
|
||||
<span class="text-xs text-gray-400"> Updated {{ formatDate(project.updated) }} </span>
|
||||
<span class="text-xs font-bold text-indigo-600 dark:text-indigo-400 flex items-center gap-1 transform translate-x-2 opacity-0 group-hover:translate-x-0 group-hover:opacity-100 transition-all"> Open Project <i class="fas fa-arrow-right"></i> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 border-t border-gray-100 dark:border-gray-800 flex justify-between items-center bg-white dark:bg-gray-900">
|
||||
<p class="text-xs text-gray-400" v-if="projects.length > 0">Showing {{ filteredProjects.length }} of {{ projects.length }} project{{ projects.length !== 1 ? 's' : '' }}</p>
|
||||
<div class="flex gap-2 ml-auto">
|
||||
<button v-if="projects.length > 0" @click="loadExample" class="text-xs font-medium text-gray-500 hover:text-indigo-600 dark:hover:text-indigo-400 px-3 py-2">Load Example</button>
|
||||
<button @click="close" class="btn btn-secondary text-sm">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, toRefs, watch, ref, computed } from 'vue';
|
||||
import { useProjectStore, type Project } from '@/stores/useProjectStore';
|
||||
import { useProjectManager } from '@/composables/useProjectManager';
|
||||
import Modal from '@/components/utilities/Modal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
<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>
|
||||
<Modal :is-open="isOpen" @close="close" title="Save project" :initialWidth="400">
|
||||
<div class="px-2 pt-2 pb-1">
|
||||
<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>
|
||||
</Teleport>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue';
|
||||
import Modal from '@/components/utilities/Modal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
|
||||
72
src/components/utilities/CopyToFrameModal.vue
Normal file
72
src/components/utilities/CopyToFrameModal.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<Modal :is-open="isOpen" @close="close" title="Copy sprite to frame" :initialWidth="380" :initialHeight="320">
|
||||
<div class="p-4 flex flex-col h-full">
|
||||
<div class="space-y-4 flex-1">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Target frame</label>
|
||||
<select v-model.number="targetFrame" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500">
|
||||
<option v-for="i in maxFrameCount" :key="i" :value="i - 1">Frame {{ i }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Target layer</label>
|
||||
<select v-model="targetLayerId" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500">
|
||||
<option v-for="layer in layers" :key="layer.id" :value="layer.id">{{ layer.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-6 pt-4 border-t border-gray-100 dark:border-gray-700">
|
||||
<button @click="close" class="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors font-medium text-sm">Cancel</button>
|
||||
<button @click="confirm" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import Modal from './Modal.vue';
|
||||
import type { Layer } from '@/types/sprites';
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
layers: Layer[];
|
||||
initialLayerId?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'copy', targetLayerId: string, targetFrameIndex: number): void;
|
||||
}>();
|
||||
|
||||
const targetFrame = ref(0);
|
||||
const targetLayerId = ref('');
|
||||
|
||||
const maxFrameCount = computed(() => {
|
||||
const maxLen = Math.max(1, ...props.layers.map(l => l.sprites.length));
|
||||
return maxLen + 1; // Allow copying to one frame beyond current max
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
isOpen => {
|
||||
if (isOpen) {
|
||||
targetFrame.value = 0;
|
||||
targetLayerId.value = props.initialLayerId || (props.layers.length > 0 ? props.layers[0].id : '');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const close = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
if (targetLayerId.value) {
|
||||
emit('copy', targetLayerId.value, targetFrame.value);
|
||||
close();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -10,31 +10,39 @@
|
||||
width: isFullScreen ? '100%' : `${size.width}px`,
|
||||
height: isFullScreen ? '100%' : `${size.height}px`,
|
||||
}"
|
||||
class="bg-white dark:bg-gray-800 rounded-2xl border-2 border-gray-300 dark:border-gray-700 shadow-xl flex flex-col fixed z-50 transition-colors duration-300"
|
||||
class="bg-white dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700 shadow-2xl flex flex-col fixed z-50 transition-colors duration-300"
|
||||
:class="{ 'rounded-none border-0': isFullScreen, 'select-none': isDragging }"
|
||||
>
|
||||
<!-- Header with drag handle -->
|
||||
<div class="flex justify-between items-center p-4 border-b border-gray-200 dark:border-gray-700" :class="{ 'cursor-move': !isFullScreen && !isMobile }" @mousedown="startDrag" @touchstart="handleTouchStart">
|
||||
<h3 class="text-xl sm:text-2xl font-semibold text-gray-900 dark:text-gray-100 truncate pr-2">{{ title }}</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button @click="toggleFullScreen" @mousedown.stop @touchstart.stop class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors" data-rybbit-event="modal-fullscreen">
|
||||
<img src="@/assets/images/fullscreen-icon.svg" class="w-4 h-4 dark:invert" alt="Fullscreen" :class="{ 'rotate-180': isFullScreen }" />
|
||||
<div class="flex justify-between items-center px-5 py-4 border-b border-gray-100 dark:border-gray-700/50" :class="{ 'cursor-move': !isFullScreen && !isMobile }" @mousedown="startDrag" @touchstart="handleTouchStart">
|
||||
<h3 class="text-lg sm:text-lg font-bold text-gray-800 dark:text-gray-100 truncate pr-2">{{ title }}</h3>
|
||||
<div class="flex items-center space-x-1">
|
||||
<button @click="toggleFullScreen" @mousedown.stop @touchstart.stop class="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-all" data-rybbit-event="modal-fullscreen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" v-if="!isFullScreen">
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" v-else>
|
||||
<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" />
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="close" @mousedown.stop @touchstart.stop class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors" data-rybbit-event="modal-close">
|
||||
<img src="@/assets/images/close-icon.svg" class="w-5 h-5 dark:invert" alt="Close" />
|
||||
<button @click="close" @mousedown.stop @touchstart.stop class="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md transition-all" data-rybbit-event="modal-close">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="p-4 sm:p-6 flex-1 overflow-auto">
|
||||
<div :class="[noPadding ? '' : 'p-5 sm:p-6', 'flex-1 overflow-auto']">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<!-- Resize handle -->
|
||||
<div v-if="!isFullScreen && !isMobile" class="absolute bottom-0 right-0 w-8 h-8 cursor-se-resize" @mousedown="startResize" @touchstart="startResize">
|
||||
<svg class="w-8 h-8 text-gray-400 dark:text-gray-500" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M22 22H20V20H22V22ZM22 18H20V16H22V18ZM18 22H16V20H18V22ZM22 14H20V12H22V14ZM18 18H16V16H18V18ZM14 22H12V20H14V22Z" />
|
||||
<div v-if="!isFullScreen && !isMobile" class="absolute bottom-0 right-0 w-6 h-6 cursor-se-resize flex items-end justify-end p-1 opacity-50 hover:opacity-100 transition-opacity" @mousedown="startResize" @touchstart="startResize">
|
||||
<svg class="w-4 h-4 text-gray-400 dark:text-gray-500" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M22 22H20V20H22V22ZM22 18H20V16H22V18ZM18 22H16V20H18V22Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,6 +57,7 @@
|
||||
title: string;
|
||||
initialWidth?: number;
|
||||
initialHeight?: number;
|
||||
noPadding?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
Reference in New Issue
Block a user