[FEAT] Streamline UI
This commit is contained in:
@@ -85,5 +85,3 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,32 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Copy to Frame Modal -->
|
<!-- 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">
|
<CopyToFrameModal :is-open="showCopyToFrameModal" :layers="props.layers" :initial-layer-id="copyTargetLayerId" @close="closeCopyToFrameModal" @copy="confirmCopyToFrame" />
|
||||||
<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>
|
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
<div class="h-full w-full flex flex-col p-4">
|
<div class="h-full w-full flex flex-col p-4">
|
||||||
@@ -206,6 +181,7 @@
|
|||||||
import { useFileDrop } from '@/composables/useFileDrop';
|
import { useFileDrop } from '@/composables/useFileDrop';
|
||||||
import { useGridMetrics } from '@/composables/useGridMetrics';
|
import { useGridMetrics } from '@/composables/useGridMetrics';
|
||||||
import { useBackgroundStyles } from '@/composables/useBackgroundStyles';
|
import { useBackgroundStyles } from '@/composables/useBackgroundStyles';
|
||||||
|
import CopyToFrameModal from './utilities/CopyToFrameModal.vue';
|
||||||
|
|
||||||
import type { Layer } from '@/types/sprites';
|
import type { Layer } from '@/types/sprites';
|
||||||
|
|
||||||
@@ -302,15 +278,9 @@
|
|||||||
|
|
||||||
// Copy to frame modal state
|
// Copy to frame modal state
|
||||||
const showCopyToFrameModal = ref(false);
|
const showCopyToFrameModal = ref(false);
|
||||||
const copyTargetFrame = ref(0);
|
|
||||||
const copyTargetLayerId = ref(props.activeLayerId);
|
const copyTargetLayerId = ref(props.activeLayerId);
|
||||||
const copySpriteId = ref<string | null>(null);
|
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
|
// Clear selection when toggling multi-select mode
|
||||||
watch(
|
watch(
|
||||||
() => props.isMultiSelectMode,
|
() => props.isMultiSelectMode,
|
||||||
@@ -556,7 +526,6 @@
|
|||||||
if (contextMenuSpriteId.value) {
|
if (contextMenuSpriteId.value) {
|
||||||
copySpriteId.value = contextMenuSpriteId.value;
|
copySpriteId.value = contextMenuSpriteId.value;
|
||||||
copyTargetLayerId.value = props.activeLayerId;
|
copyTargetLayerId.value = props.activeLayerId;
|
||||||
copyTargetFrame.value = 0;
|
|
||||||
showCopyToFrameModal.value = true;
|
showCopyToFrameModal.value = true;
|
||||||
showContextMenu.value = false;
|
showContextMenu.value = false;
|
||||||
}
|
}
|
||||||
@@ -567,9 +536,9 @@
|
|||||||
copySpriteId.value = null;
|
copySpriteId.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmCopyToFrame = () => {
|
const confirmCopyToFrame = (targetLayerId: string, targetFrameIndex: number) => {
|
||||||
if (copySpriteId.value) {
|
if (copySpriteId.value) {
|
||||||
emit('copySpriteToFrame', copySpriteId.value, copyTargetLayerId.value, copyTargetFrame.value);
|
emit('copySpriteToFrame', copySpriteId.value, targetLayerId, targetFrameIndex);
|
||||||
closeCopyToFrameModal();
|
closeCopyToFrameModal();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,32 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Copy to Frame Modal -->
|
<!-- 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">
|
<CopyToFrameModal :is-open="showCopyToFrameModal" :layers="props.layers" :initial-layer-id="copyTargetLayerId" @close="closeCopyToFrameModal" @copy="confirmCopyToFrame" />
|
||||||
<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>
|
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
<div class="spritesheet-preview w-full h-full" @click="hideContextMenu">
|
<div class="spritesheet-preview w-full h-full" @click="hideContextMenu">
|
||||||
@@ -308,6 +283,7 @@
|
|||||||
import { useAnimationFrames } from '@/composables/useAnimationFrames';
|
import { useAnimationFrames } from '@/composables/useAnimationFrames';
|
||||||
import { useGridMetrics } from '@/composables/useGridMetrics';
|
import { useGridMetrics } from '@/composables/useGridMetrics';
|
||||||
import { useBackgroundStyles } from '@/composables/useBackgroundStyles';
|
import { useBackgroundStyles } from '@/composables/useBackgroundStyles';
|
||||||
|
import CopyToFrameModal from './utilities/CopyToFrameModal.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
layers: Layer[];
|
layers: Layer[];
|
||||||
@@ -373,14 +349,8 @@
|
|||||||
|
|
||||||
// Copy to frame modal state
|
// Copy to frame modal state
|
||||||
const showCopyToFrameModal = ref(false);
|
const showCopyToFrameModal = ref(false);
|
||||||
const copyTargetFrame = ref(0);
|
|
||||||
const copyTargetLayerId = ref(props.activeLayerId);
|
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
|
// Drag and drop for new sprites
|
||||||
const onDragOver = () => {
|
const onDragOver = () => {
|
||||||
isDragOver.value = true;
|
isDragOver.value = true;
|
||||||
@@ -713,7 +683,6 @@
|
|||||||
const openCopyToFrameModal = () => {
|
const openCopyToFrameModal = () => {
|
||||||
if (contextMenuSpriteId.value) {
|
if (contextMenuSpriteId.value) {
|
||||||
copyTargetLayerId.value = contextMenuLayerId.value || props.activeLayerId;
|
copyTargetLayerId.value = contextMenuLayerId.value || props.activeLayerId;
|
||||||
copyTargetFrame.value = 0;
|
|
||||||
showCopyToFrameModal.value = true;
|
showCopyToFrameModal.value = true;
|
||||||
showContextMenu.value = false;
|
showContextMenu.value = false;
|
||||||
}
|
}
|
||||||
@@ -723,9 +692,9 @@
|
|||||||
showCopyToFrameModal.value = false;
|
showCopyToFrameModal.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmCopyToFrame = () => {
|
const confirmCopyToFrame = (targetLayerId: string, targetFrameIndex: number) => {
|
||||||
if (contextMenuSpriteId.value) {
|
if (contextMenuSpriteId.value) {
|
||||||
emit('copySpriteToFrame', contextMenuSpriteId.value, copyTargetLayerId.value, copyTargetFrame.value);
|
emit('copySpriteToFrame', contextMenuSpriteId.value, targetLayerId, targetFrameIndex);
|
||||||
closeCopyToFrameModal();
|
closeCopyToFrameModal();
|
||||||
contextMenuSpriteId.value = null;
|
contextMenuSpriteId.value = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Modal :is-open="isOpen" @close="close" :title="isLogin ? 'Welcome Back' : 'Create Account'" :initialWidth="450">
|
||||||
<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="space-y-4">
|
||||||
<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">
|
<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">
|
<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' }}
|
{{ isLogin ? 'Sign in to access your projects' : 'Join to save and manage your spritesheets' }}
|
||||||
</p>
|
</p>
|
||||||
@@ -46,13 +40,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useAuthStore } from '@/stores/useAuthStore';
|
import { useAuthStore } from '@/stores/useAuthStore';
|
||||||
|
import Modal from '@/components/utilities/Modal.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Modal :is-open="isOpen" @close="close" title="New project" :initialWidth="400">
|
||||||
<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="px-2 pt-2 pb-1">
|
||||||
<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">
|
<form @submit.prevent="handleSubmit">
|
||||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -38,12 +36,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import Modal from '@/components/utilities/Modal.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Modal :is-open="isOpen" @close="close" title="My Projects" :initialWidth="900" :initialHeight="800" noPadding>
|
||||||
<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="flex flex-col h-full bg-white dark:bg-gray-900">
|
||||||
<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">
|
<!-- Search Header -->
|
||||||
<!-- 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="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="mb-4">
|
||||||
<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>
|
<p class="text-sm text-gray-500 dark:text-gray-400">Manage your saved sprite sheets</p>
|
||||||
</div>
|
</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>
|
|
||||||
|
|
||||||
<!-- Search Bar -->
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
|
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
|
||||||
<input
|
<input
|
||||||
@@ -121,14 +113,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, toRefs, watch, ref, computed } from 'vue';
|
import { onMounted, toRefs, watch, ref, computed } from 'vue';
|
||||||
import { useProjectStore, type Project } from '@/stores/useProjectStore';
|
import { useProjectStore, type Project } from '@/stores/useProjectStore';
|
||||||
import { useProjectManager } from '@/composables/useProjectManager';
|
import { useProjectManager } from '@/composables/useProjectManager';
|
||||||
|
import Modal from '@/components/utilities/Modal.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Modal :is-open="isOpen" @close="close" title="Save project" :initialWidth="400">
|
||||||
<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="px-2 pt-2 pb-1">
|
||||||
<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">
|
<form @submit.prevent="handleSubmit">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Project Name</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Project Name</label>
|
||||||
@@ -14,12 +12,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, nextTick } from 'vue';
|
import { ref, watch, nextTick } from 'vue';
|
||||||
|
import Modal from '@/components/utilities/Modal.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isOpen: boolean;
|
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`,
|
width: isFullScreen ? '100%' : `${size.width}px`,
|
||||||
height: isFullScreen ? '100%' : `${size.height}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 }"
|
:class="{ 'rounded-none border-0': isFullScreen, 'select-none': isDragging }"
|
||||||
>
|
>
|
||||||
<!-- Header with drag handle -->
|
<!-- 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">
|
<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-xl sm:text-2xl font-semibold text-gray-900 dark:text-gray-100 truncate pr-2">{{ title }}</h3>
|
<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-2">
|
<div class="flex items-center space-x-1">
|
||||||
<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">
|
<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">
|
||||||
<img src="@/assets/images/fullscreen-icon.svg" class="w-4 h-4 dark:invert" alt="Fullscreen" :class="{ 'rotate-180': isFullScreen }" />
|
<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>
|
||||||
<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">
|
<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">
|
||||||
<img src="@/assets/images/close-icon.svg" class="w-5 h-5 dark:invert" alt="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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Body -->
|
<!-- 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>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Resize handle -->
|
<!-- Resize handle -->
|
||||||
<div v-if="!isFullScreen && !isMobile" class="absolute bottom-0 right-0 w-8 h-8 cursor-se-resize" @mousedown="startResize" @touchstart="startResize">
|
<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-8 h-8 text-gray-400 dark:text-gray-500" viewBox="0 0 24 24" fill="currentColor">
|
<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 22H16V20H18V22ZM22 14H20V12H22V14ZM18 18H16V16H18V18ZM14 22H12V20H14V22Z" />
|
<path d="M22 22H20V20H22V22ZM22 18H20V16H22V18ZM18 22H16V20H18V22Z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,6 +57,7 @@
|
|||||||
title: string;
|
title: string;
|
||||||
initialWidth?: number;
|
initialWidth?: number;
|
||||||
initialHeight?: number;
|
initialHeight?: number;
|
||||||
|
noPadding?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
Reference in New Issue
Block a user