Spaces:
Sleeping
Sleeping
| <template> | |
| <div | |
| class="premium-glass rounded-2xl overflow-hidden animate-fade-in-up transition-all" | |
| :class="{ 'drop-zone-active': isDragOver }" | |
| @dragenter.prevent="onDragEnter" | |
| @dragover.prevent="onDragOver" | |
| @dragleave="onDragLeave" | |
| @drop.prevent="onDrop" | |
| > | |
| <!-- Group Header --> | |
| <div | |
| class="flex items-center gap-3 px-5 py-4 border-b border-white/40 bg-white/20" | |
| > | |
| <!-- Collapse toggle --> | |
| <button | |
| @click="emit('toggle-collapse', group.id)" | |
| class="w-6 h-6 flex items-center justify-center text-slate-400 hover:text-slate-600 transition-colors shrink-0" | |
| > | |
| <UIcon | |
| :name=" | |
| group.collapsed ? 'i-lucide-chevron-right' : 'i-lucide-chevron-down' | |
| " | |
| class="w-4 h-4 transition-transform duration-200" | |
| /> | |
| </button> | |
| <!-- Folder icon --> | |
| <UIcon | |
| name="i-lucide-folder-open" | |
| class="w-4 h-4 text-green-500 shrink-0" | |
| /> | |
| <!-- Editable group name --> | |
| <div class="flex-1 min-w-0"> | |
| <input | |
| v-if="isEditing" | |
| ref="nameInput" | |
| v-model="editName" | |
| class="w-full bg-transparent text-slate-900 font-semibold text-sm outline-none border-b border-green-400 pb-0.5 focus:border-green-500" | |
| @blur="commitRename" | |
| @keydown.enter.prevent="commitRename" | |
| @keydown.escape.prevent="cancelRename" | |
| /> | |
| <button | |
| v-else | |
| @click="startEditing" | |
| class="text-slate-800 font-semibold text-sm truncate hover:text-green-600 transition-colors text-left w-full" | |
| :title="group.name" | |
| > | |
| {{ group.name }} | |
| </button> | |
| </div> | |
| <!-- Project count badge --> | |
| <span | |
| class="text-xs font-medium bg-slate-100 text-slate-500 px-2 py-0.5 rounded-full shrink-0" | |
| > | |
| {{ group.projectIds.length }} | |
| </span> | |
| <!-- Delete group button --> | |
| <button | |
| @click="onDeleteGroup" | |
| class="w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-red-500 hover:bg-red-50 transition-all shrink-0" | |
| title="Delete group" | |
| > | |
| <UIcon name="i-lucide-trash-2" class="w-3.5 h-3.5" /> | |
| </button> | |
| </div> | |
| <!-- Drop zone / project grid --> | |
| <div v-show="!group.collapsed" class="p-4 transition-all"> | |
| <!-- Empty drop zone --> | |
| <div | |
| v-if="projects.length === 0" | |
| class="rounded-xl border-2 border-dashed border-slate-200 flex flex-col items-center justify-center py-8 text-slate-400 text-sm gap-2 transition-colors" | |
| :class=" | |
| isDragOver ? 'border-green-400 bg-green-50/40 text-green-600' : '' | |
| " | |
| > | |
| <UIcon | |
| :name="isDragOver ? 'i-lucide-package-plus' : 'i-lucide-package'" | |
| class="w-6 h-6" | |
| /> | |
| <span>{{ isDragOver ? "Drop to add" : "Drag projects here" }}</span> | |
| </div> | |
| <!-- Project cards grid --> | |
| <div | |
| v-else | |
| class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4" | |
| :class=" | |
| isDragOver && projects.length > 0 | |
| ? 'ring-2 ring-green-400 ring-offset-2 rounded-xl' | |
| : '' | |
| " | |
| > | |
| <ProjectCard | |
| v-for="project in projects" | |
| :key="project.id" | |
| :project="project" | |
| :duration="durations?.[project.id]" | |
| @delete="(p) => emit('delete-project', p)" | |
| @continue="(p) => emit('continue-project', p)" | |
| @dragstart="emit('project-dragstart', $event)" | |
| @dragend="emit('project-dragend')" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script setup lang="ts"> | |
| import { ref, nextTick } from "vue"; | |
| import type { ProjectGroup } from "~/stores/groups"; | |
| const props = defineProps<{ | |
| group: ProjectGroup; | |
| projects: any[]; | |
| durations?: Record<string, string>; | |
| }>(); | |
| const emit = defineEmits<{ | |
| "toggle-collapse": [groupId: string]; | |
| rename: [groupId: string, name: string]; | |
| delete: [groupId: string]; | |
| drop: [projectId: string, groupId: string]; | |
| "delete-project": [project: any]; | |
| "continue-project": [project: any]; | |
| "project-dragstart": [projectId: string]; | |
| "project-dragend": []; | |
| }>(); | |
| // ββ Drag-and-drop (counter avoids false dragleave from child elements) ββ | |
| const isDragOver = ref(false); | |
| let dragEnterCount = 0; | |
| function onDragEnter(e: DragEvent) { | |
| dragEnterCount++; | |
| isDragOver.value = true; | |
| if (e.dataTransfer) e.dataTransfer.dropEffect = "move"; | |
| // Auto-expand collapsed group so user can see where they're dropping | |
| if (props.group.collapsed) { | |
| emit("toggle-collapse", props.group.id); | |
| } | |
| } | |
| function onDragOver(e: DragEvent) { | |
| isDragOver.value = true; | |
| if (e.dataTransfer) e.dataTransfer.dropEffect = "move"; | |
| } | |
| function onDragLeave(_e: DragEvent) { | |
| dragEnterCount = Math.max(0, dragEnterCount - 1); | |
| if (dragEnterCount === 0) { | |
| isDragOver.value = false; | |
| } | |
| } | |
| function onDrop(e: DragEvent) { | |
| dragEnterCount = 0; | |
| isDragOver.value = false; | |
| const projectId = e.dataTransfer?.getData("text/plain"); | |
| if (projectId) { | |
| emit("drop", projectId, props.group.id); | |
| } | |
| } | |
| // ββ Inline editing ββ | |
| const isEditing = ref(false); | |
| const editName = ref(""); | |
| const nameInput = ref<HTMLInputElement | null>(null); | |
| function startEditing() { | |
| editName.value = props.group.name; | |
| isEditing.value = true; | |
| nextTick(() => { | |
| nameInput.value?.select(); | |
| }); | |
| } | |
| function commitRename() { | |
| if (editName.value.trim()) { | |
| emit("rename", props.group.id, editName.value.trim()); | |
| } | |
| isEditing.value = false; | |
| } | |
| function cancelRename() { | |
| isEditing.value = false; | |
| } | |
| // ββ Delete ββ | |
| function onDeleteGroup() { | |
| emit("delete", props.group.id); | |
| } | |
| </script> | |