diff --git a/src/routes/cardsboard/Card.svelte b/src/routes/cardsboard/Card.svelte index fb80e67..dd7df8b 100644 --- a/src/routes/cardsboard/Card.svelte +++ b/src/routes/cardsboard/Card.svelte @@ -186,6 +186,126 @@ }; }); + // ============================================================================ + // KEYBOARD NAVIGATION: Global listener for Enter/Space on focused card + // Workaround for dndzone blocking keyboard events + // ============================================================================ + $effect(() => { + const handleGlobalKeyDown = (event: KeyboardEvent) => { + // Check if this card is currently focused + const activeElement = document.activeElement; + if (!activeElement) return; + + const cardElement = activeElement.closest(`[data-card-id="${card.id}"]`); + if (!cardElement) return; + + // Check if we're in an input/textarea + const target = event.target as HTMLElement; + const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable; + if (isInput) return; + + // Open dialog on Enter or Space + if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') { + event.preventDefault(); + event.stopPropagation(); + console.log('⌨️ Opening card dialog via global keyboard listener:', card.id); + isDialogOpen = true; + return; + } + + // Arrow key navigation for moving cards + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) { + event.preventDefault(); + event.stopPropagation(); + + // Get all columns and find current card position + const columns = boardStore.uiData; + let currentColumnIndex = -1; + let currentCardIndex = -1; + let currentColumn = null; + + for (let i = 0; i < columns.length; i++) { + const col = columns[i]; + const cardIndex = col.items.findIndex(item => String(item.id) === String(card.id)); + if (cardIndex !== -1) { + currentColumnIndex = i; + currentCardIndex = cardIndex; + currentColumn = col; + break; + } + } + + if (currentColumnIndex === -1 || !currentColumn) return; + + // Helper function to restore focus with multiple attempts + const restoreFocus = (cardId: string, maxAttempts = 10) => { + let attempts = 0; + const tryFocus = () => { + const movedCard = document.querySelector(`[data-card-id="${cardId}"]`) as HTMLElement; + if (movedCard && movedCard !== document.activeElement) { + movedCard.focus(); + console.log(`⌨️ Card refocused (attempt ${attempts + 1})`); + } else if (!movedCard && attempts < maxAttempts) { + attempts++; + requestAnimationFrame(tryFocus); + } + }; + requestAnimationFrame(() => requestAnimationFrame(tryFocus)); + }; + + // Handle Up/Down - move within same column + if (event.key === 'ArrowUp' && currentCardIndex > 0) { + // Move card up (swap with previous card) + const newItems = [...currentColumn.items]; + [newItems[currentCardIndex - 1], newItems[currentCardIndex]] = + [newItems[currentCardIndex], newItems[currentCardIndex - 1]]; + + const updatedColumn = { ...currentColumn, items: newItems }; + const newColumns = [...columns]; + newColumns[currentColumnIndex] = updatedColumn; + + boardStore.syncBoardState(newColumns); + console.log('⌨️ Card moved up'); + restoreFocus(String(card.id)); + } + else if (event.key === 'ArrowDown' && currentCardIndex < currentColumn.items.length - 1) { + // Move card down (swap with next card) + const newItems = [...currentColumn.items]; + [newItems[currentCardIndex], newItems[currentCardIndex + 1]] = + [newItems[currentCardIndex + 1], newItems[currentCardIndex]]; + + const updatedColumn = { ...currentColumn, items: newItems }; + const newColumns = [...columns]; + newColumns[currentColumnIndex] = updatedColumn; + + boardStore.syncBoardState(newColumns); + console.log('⌨️ Card moved down'); + restoreFocus(String(card.id)); + } + // Handle Left - move to previous column + else if (event.key === 'ArrowLeft' && currentColumnIndex > 0) { + const targetColumn = columns[currentColumnIndex - 1]; + boardStore.moveCard(String(card.id), currentColumn.id, targetColumn.id); + console.log('⌨️ Card moved to previous column'); + restoreFocus(String(card.id)); + } + // Handle Right - move to next column + else if (event.key === 'ArrowRight' && currentColumnIndex < columns.length - 1) { + const targetColumn = columns[currentColumnIndex + 1]; + boardStore.moveCard(String(card.id), currentColumn.id, targetColumn.id); + console.log('⌨️ Card moved to next column'); + restoreFocus(String(card.id)); + } + } + }; + + window.addEventListener('keydown', handleGlobalKeyDown, true); // Use capture phase + + return () => { + window.removeEventListener('keydown', handleGlobalKeyDown, true); + }; + }); + // ============================================================================ // PROP-UPDATE-GUIDE.md Schritt 3: $effect für UI-Synchronisation // ============================================================================ @@ -381,6 +501,7 @@ data-card-id={card.id} data-card-root style="border-bottom: 5px solid {getCardColor(localColor)};" + tabindex={0} ontouchstart={handleTouchStart} ontouchend={handleTouchEnd} ontouchmove={handleTouchMove} @@ -411,6 +532,22 @@ // Klick auf Card öffnet direkt CardDetailsDialog isDialogOpen = true; }} + onkeydown={(e) => { + // Open card dialog with Enter or Space key + if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') { + // Check if we're not in an input/textarea + const target = e.target as HTMLElement; + const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA'; + if (isInput) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + console.log('⌨️ Opening card dialog via keyboard'); + isDialogOpen = true; + } + }} >
@@ -480,6 +617,7 @@
{:else if card.link} - {/if} diff --git a/src/routes/cardsboard/Column.svelte b/src/routes/cardsboard/Column.svelte index b09b756..55ba06e 100644 --- a/src/routes/cardsboard/Column.svelte +++ b/src/routes/cardsboard/Column.svelte @@ -14,7 +14,9 @@ import { toast } from "svelte-sonner"; import LinkAddPopover from '$lib/components/LinkAddPopover.svelte'; import TrashIcon from '@lucide/svelte/icons/trash'; - import SquarePlusIcon from '@lucide/svelte/icons/square-plus'; + import SquarePlusIcon from '@lucide/svelte/icons/square-plus'; + import ArrowLeftRightIcon from '@lucide/svelte/icons/arrow-left-right'; + import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'; const flipDurationMs = 150; // Sicherer Flip-Wrapper: Vermeidet Fehler bei ungültigen Größen (NaN-Werte) @@ -90,6 +92,7 @@ let editName = $state(name); let selectedColor = $state(color || 'slate'); let popoverOpen = $state(false); + let showMoveOptions = $state(false); // State für Inline-Editing des Column-Titels let isEditingTitle = $state(false); @@ -98,6 +101,35 @@ // Global popover state management - ensures only one popover is open at a time const popoverId = `column-popover-${columnId}`; + // ============================================================================ + // COLUMN MOVEMENT: Reorder columns in the board + // ============================================================================ + function moveColumnTo(targetIndex: number) { + // Get current columns + const currentColumns = boardStore.uiData; + + // Find current column's index + const currentIndex = currentColumns.findIndex(col => col.id === columnId); + if (currentIndex === -1) return; + + // Create new array with reordered columns + const reorderedColumns = [...currentColumns]; + const [movedColumn] = reorderedColumns.splice(currentIndex, 1); + reorderedColumns.splice(targetIndex, 0, movedColumn); + + // Sync to store + boardStore.syncBoardState(reorderedColumns); + + // Close popover + popoverOpen = false; + + // Show success feedback + const targetPosition = targetIndex === 0 ? 'erste Position' : + targetIndex === currentColumns.length - 1 ? 'letzte Position' : + `Position ${targetIndex + 1}`; + toast.success(`Spalte verschoben an ${targetPosition}`); + } + const colorOptions = [ { value: 'slate', label: 'Slate', cssVar: '--color-slate' }, { value: 'blue', label: 'Blau', cssVar: '--color-blue' }, @@ -335,13 +367,21 @@ flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; + padding-top: 0.5rem; + padding-left: 0.5rem; padding-right: 0.5rem; min-height: 50px; border-radius: var(--radius-md); padding-bottom: 10px; + display: flex; + flex-direction: column; + } + + .cards-dnd-area { + flex: 0 0 auto; } - /* Add-Card-Button: Sticky am unteren Rand wenn gescrollt wird */ + /* Add-Card-Button: Inside scrollable area but outside dndzone */ .add-card-button { border-radius: var(--radius-md); /* border: 2px dotted var(--accent); */ @@ -355,11 +395,6 @@ flex-shrink: 0; align-items: center; justify-content: center; - - /* Sticky: Klebt am unteren Rand wenn Container scrollbar ist */ - position: sticky; - bottom: -10px; - z-index: 5; } .add-card-button:hover { @@ -466,8 +501,8 @@ - - - + + + - - - + + {#if showMoveOptions} +
+ {#each boardStore.uiData as _, idx} + {@const allColumns = boardStore.uiData} + {@const currentIndex = allColumns.findIndex(col => col.id === columnId)} + + {#if idx === 0} + + {#if currentIndex !== 0} + + {/if} + {/if} + + {#if idx > 0 && idx <= allColumns.length - 1} + {@const leftColumn = allColumns[idx - 1]} + {@const rightColumn = allColumns[idx]} + {@const isCurrentInBetween = currentIndex === idx - 1 || currentIndex === idx} + + {#if !isCurrentInBetween} + {@const adjustedIdx = idx > currentIndex ? idx - 1 : idx} + + {/if} + {/if} + + {#if idx === allColumns.length - 1} + + {#if currentIndex !== allColumns.length - 1} + + {/if} + {/if} + {/each} +
+ {/if} + + + + + @@ -549,29 +665,37 @@
-
- {#each items as item (item.id)} -
- -
- {/each} + +
+ +
+ {#each items as item (item.id)} +
+ +
+ {/each} +
- + {#if !readOnly}