feat(todo): inline column editing in board headers, remove separate editor

Move column editing (color, name, reorder, delete) directly into each
column/sheet header. In edit mode, ViewColumnHeader shows color dots,
name input, and action buttons inline. Add column button in toolbar.

- ViewColumnHeader: edit mode with color picker, name input, move/delete
- Props flow: page → BoardViewRenderer → Layout → ViewColumn → Header
- Remove separate column-editor bar from page
- Narrower Fokus sheet widths (360/480/640/840px)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-31 13:43:47 +02:00
parent 3b54d4d48e
commit 1926c6b1f2
7 changed files with 336 additions and 186 deletions

View file

@ -11,9 +11,20 @@
interface Props {
view: LocalBoardView;
layoutOverride?: 'kanban' | 'grid' | 'fokus';
onColumnRename?: (colIdx: number, name: string) => void;
onColumnColorChange?: (colIdx: number, color: string) => void;
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
onColumnDelete?: (colIdx: number) => void;
}
let { view, layoutOverride }: Props = $props();
let {
view,
layoutOverride,
onColumnRename,
onColumnColorChange,
onColumnMove,
onColumnDelete,
}: Props = $props();
let activeLayout = $derived(layoutOverride || view.layout);
@ -74,6 +85,10 @@
onTaskToggle={handleTaskToggle}
onTaskDelete={handleTaskDelete}
onTaskUpdate={handleTaskUpdate}
{onColumnRename}
{onColumnColorChange}
{onColumnMove}
{onColumnDelete}
/>
{:else if activeLayout === 'grid'}
<GridLayout
@ -82,6 +97,10 @@
onTaskToggle={handleTaskToggle}
onTaskDelete={handleTaskDelete}
onTaskUpdate={handleTaskUpdate}
{onColumnRename}
{onColumnColorChange}
{onColumnMove}
{onColumnDelete}
/>
{:else}
<KanbanLayout
@ -90,5 +109,9 @@
onTaskToggle={handleTaskToggle}
onTaskDelete={handleTaskDelete}
onTaskUpdate={handleTaskUpdate}
{onColumnRename}
{onColumnColorChange}
{onColumnMove}
{onColumnDelete}
/>
{/if}

View file

@ -14,15 +14,29 @@
onTaskToggle: (task: Task) => void;
onTaskDelete: (taskId: string) => void;
onTaskUpdate: (taskId: string, data: Partial<Task>) => void;
onColumnRename?: (colIdx: number, name: string) => void;
onColumnColorChange?: (colIdx: number, color: string) => void;
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
onColumnDelete?: (colIdx: number) => void;
}
let { columns, onTaskDrop, onTaskToggle, onTaskDelete, onTaskUpdate }: Props = $props();
let {
columns,
onTaskDrop,
onTaskToggle,
onTaskDelete,
onTaskUpdate,
onColumnRename,
onColumnColorChange,
onColumnMove,
onColumnDelete,
}: Props = $props();
const PAGE_WIDTH_MAP: Record<string, string> = {
narrow: 'min(640px, 85vw)',
medium: 'min(840px, 85vw)',
wide: 'min(1024px, 92vw)',
full: '92vw',
narrow: 'min(360px, 85vw)',
medium: 'min(480px, 85vw)',
wide: 'min(640px, 90vw)',
full: 'min(840px, 95vw)',
};
let sheetWidth = $derived(PAGE_WIDTH_MAP[todoSettings.pageWidth] || PAGE_WIDTH_MAP.medium);
@ -106,10 +120,20 @@
bind:this={scrollContainer}
onscroll={handleScroll}
>
{#each columns as column (column.id)}
{#each columns as column, i (column.id)}
{@const tasks = localTasksByColumn[column.id] || column.tasks}
<div class="fokus-sheet" class:sheet-completed={column.name === 'Erledigt'}>
<ViewColumnHeader name={column.name} color={column.color} taskCount={tasks.length} />
<ViewColumnHeader
name={column.name}
color={column.color}
taskCount={tasks.length}
columnIndex={i}
totalColumns={columns.length}
onRename={onColumnRename ? (name) => onColumnRename(i, name) : undefined}
onColorChange={onColumnColorChange ? (c) => onColumnColorChange(i, c) : undefined}
onMove={onColumnMove ? (dir) => onColumnMove(i, dir) : undefined}
onDelete={onColumnDelete ? () => onColumnDelete(i) : undefined}
/>
<div
class="sheet-content"

View file

@ -9,20 +9,40 @@
onTaskToggle: (task: Task) => void;
onTaskDelete: (taskId: string) => void;
onTaskUpdate: (taskId: string, data: Partial<Task>) => void;
onColumnRename?: (colIdx: number, name: string) => void;
onColumnColorChange?: (colIdx: number, color: string) => void;
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
onColumnDelete?: (colIdx: number) => void;
}
let { columns, onTaskDrop, onTaskToggle, onTaskDelete, onTaskUpdate }: Props = $props();
let {
columns,
onTaskDrop,
onTaskToggle,
onTaskDelete,
onTaskUpdate,
onColumnRename,
onColumnColorChange,
onColumnMove,
onColumnDelete,
}: Props = $props();
</script>
<div class="grid-layout">
{#each columns as column (column.id)}
{#each columns as column, i (column.id)}
<div class="grid-cell">
<ViewColumn
{column}
columnIndex={i}
totalColumns={columns.length}
{onTaskDrop}
{onTaskToggle}
{onTaskDelete}
{onTaskUpdate}
onColumnRename={onColumnRename ? (name) => onColumnRename(i, name) : undefined}
onColumnColorChange={onColumnColorChange ? (c) => onColumnColorChange(i, c) : undefined}
onColumnMove={onColumnMove ? (dir) => onColumnMove(i, dir) : undefined}
onColumnDelete={onColumnDelete ? () => onColumnDelete(i) : undefined}
/>
</div>
{/each}

View file

@ -9,20 +9,40 @@
onTaskToggle: (task: Task) => void;
onTaskDelete: (taskId: string) => void;
onTaskUpdate: (taskId: string, data: Partial<Task>) => void;
onColumnRename?: (colIdx: number, name: string) => void;
onColumnColorChange?: (colIdx: number, color: string) => void;
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
onColumnDelete?: (colIdx: number) => void;
}
let { columns, onTaskDrop, onTaskToggle, onTaskDelete, onTaskUpdate }: Props = $props();
let {
columns,
onTaskDrop,
onTaskToggle,
onTaskDelete,
onTaskUpdate,
onColumnRename,
onColumnColorChange,
onColumnMove,
onColumnDelete,
}: Props = $props();
</script>
<div class="kanban-layout">
{#each columns as column (column.id)}
{#each columns as column, i (column.id)}
<div class="kanban-column-wrapper">
<ViewColumn
{column}
columnIndex={i}
totalColumns={columns.length}
{onTaskDrop}
{onTaskToggle}
{onTaskDelete}
{onTaskUpdate}
onColumnRename={onColumnRename ? (name) => onColumnRename(i, name) : undefined}
onColumnColorChange={onColumnColorChange ? (c) => onColumnColorChange(i, c) : undefined}
onColumnMove={onColumnMove ? (dir) => onColumnMove(i, dir) : undefined}
onColumnDelete={onColumnDelete ? () => onColumnDelete(i) : undefined}
/>
</div>
{/each}

View file

@ -9,13 +9,31 @@
interface Props {
column: GroupedColumn;
columnIndex?: number;
totalColumns?: number;
onTaskDrop: (taskId: string, columnId: string) => void;
onTaskToggle: (task: Task) => void;
onTaskDelete: (taskId: string) => void;
onTaskUpdate: (taskId: string, data: Partial<Task>) => void;
onColumnRename?: (name: string) => void;
onColumnColorChange?: (color: string) => void;
onColumnMove?: (dir: -1 | 1) => void;
onColumnDelete?: () => void;
}
let { column, onTaskDrop, onTaskToggle, onTaskDelete, onTaskUpdate }: Props = $props();
let {
column,
columnIndex = 0,
totalColumns = 1,
onTaskDrop,
onTaskToggle,
onTaskDelete,
onTaskUpdate,
onColumnRename,
onColumnColorChange,
onColumnMove,
onColumnDelete,
}: Props = $props();
// Local tasks state for drag and drop
let localTasks = $state<Task[]>([]);
@ -102,7 +120,17 @@
<div class="view-column flex flex-col">
<!-- Header -->
<ViewColumnHeader name={column.name} color={column.color} taskCount={localTasks.length} />
<ViewColumnHeader
name={column.name}
color={column.color}
taskCount={localTasks.length}
{columnIndex}
{totalColumns}
onRename={onColumnRename}
onColorChange={onColumnColorChange}
onMove={onColumnMove}
onDelete={onColumnDelete}
/>
<!-- Tasks list with drag and drop -->
<div

View file

@ -1,19 +1,105 @@
<script lang="ts">
import { getContext } from 'svelte';
import { ArrowLeft, ArrowRight, Trash } from '@manacore/shared-icons';
interface Props {
name: string;
color: string;
taskCount: number;
columnIndex?: number;
totalColumns?: number;
onRename?: (name: string) => void;
onColorChange?: (color: string) => void;
onMove?: (dir: -1 | 1) => void;
onDelete?: () => void;
}
let { name, color, taskCount }: Props = $props();
let {
name,
color,
taskCount,
columnIndex = 0,
totalColumns = 1,
onRename,
onColorChange,
onMove,
onDelete,
}: Props = $props();
const editModeCtx: { readonly active: boolean } | undefined = getContext('editMode');
let editMode = $derived(editModeCtx?.active ?? false);
let editable = $derived(editMode && !!onRename);
const COLORS = [
'#EF4444',
'#F59E0B',
'#22C55E',
'#3B82F6',
'#8B5CF6',
'#EC4899',
'#14B8A6',
'#F97316',
'#6B7280',
];
</script>
<div class="column-header">
<div class="header-left">
<span class="color-dot" style="background-color: {color}"></span>
<span class="column-name">{name}</span>
</div>
<span class="task-count">{taskCount}</span>
<div class="column-header" class:editing={editable}>
{#if editable}
<!-- Edit mode: color dots + name input + actions -->
<div class="edit-header">
<div class="color-row">
{#each COLORS as c}
<button
class="color-pick"
class:active={color === c}
style="background-color: {c}"
onclick={() => onColorChange?.(c)}
></button>
{/each}
</div>
<div class="edit-row">
<input
class="name-input"
type="text"
value={name}
oninput={(e) => onRename?.(e.currentTarget.value)}
/>
<div class="edit-actions">
<button
class="act-btn"
onclick={() => onMove?.(-1)}
disabled={columnIndex === 0}
title="Nach links"
>
<ArrowLeft size={12} />
</button>
<button
class="act-btn"
onclick={() => onMove?.(1)}
disabled={columnIndex >= totalColumns - 1}
title="Nach rechts"
>
<ArrowRight size={12} />
</button>
<button
class="act-btn del-btn"
onclick={() => onDelete?.()}
disabled={totalColumns <= 1}
title="Spalte löschen"
>
<Trash size={12} />
</button>
</div>
</div>
</div>
{:else}
<!-- Normal mode -->
<div class="header-left">
<span class="color-dot" style="background-color: {color}"></span>
<span class="column-name">{name}</span>
</div>
<span class="task-count">{taskCount}</span>
{/if}
</div>
<style>
@ -24,6 +110,10 @@
padding: 0.75rem 1rem;
}
.column-header.editing {
padding: 0.5rem 0.75rem;
}
.header-left {
display: flex;
align-items: center;
@ -65,4 +155,93 @@
background: rgba(255, 255, 255, 0.1);
color: #6b7280;
}
/* ── Edit mode ────────────────────────────────────────── */
.edit-header {
display: flex;
flex-direction: column;
gap: 0.375rem;
width: 100%;
}
.color-row {
display: flex;
gap: 0.2rem;
}
.color-pick {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.15s;
padding: 0;
}
.color-pick:hover {
transform: scale(1.25);
}
.color-pick.active {
border-color: white;
box-shadow: 0 0 0 1.5px currentColor;
transform: scale(1.15);
}
.edit-row {
display: flex;
align-items: center;
gap: 0.375rem;
}
.name-input {
flex: 1;
font-size: 0.8125rem;
font-weight: 600;
color: #374151;
background: transparent;
border: none;
border-bottom: 1px solid rgba(139, 92, 246, 0.3);
padding: 0.125rem 0;
outline: none;
min-width: 0;
}
.name-input:focus {
border-bottom-color: #8b5cf6;
}
:global(.dark) .name-input {
color: #f3f4f6;
}
.edit-actions {
display: flex;
gap: 0.125rem;
flex-shrink: 0;
}
.act-btn {
padding: 0.2rem;
border-radius: 0.25rem;
color: #9ca3af;
cursor: pointer;
transition: all 0.15s;
background: transparent;
border: none;
line-height: 0;
}
.act-btn:hover:not(:disabled) {
color: #374151;
background: rgba(0, 0, 0, 0.06);
}
.act-btn:disabled {
opacity: 0.25;
cursor: not-allowed;
}
:global(.dark) .act-btn:hover:not(:disabled) {
color: #f3f4f6;
background: rgba(255, 255, 255, 0.1);
}
.del-btn:hover:not(:disabled) {
color: #ef4444 !important;
background: rgba(239, 68, 68, 0.1) !important;
}
</style>

View file

@ -4,7 +4,7 @@
import { BoardViewRenderer } from '$lib/components/board-views';
import { todoSettings, type PageWidth } from '$lib/stores/settings.svelte';
import { boardViewsStore } from '$lib/stores/board-views.svelte';
import { ArrowLeft, ArrowRight, Plus, Trash } from '@manacore/shared-icons';
import { Plus } from '@manacore/shared-icons';
// Get active view + edit mode from layout context
const activeViewCtx: { readonly value: LocalBoardView | null } = getContext('activeView');
@ -148,57 +148,11 @@
</div>
</div>
<!-- Column Editor -->
{#if columnsEditable}
<div class="column-editor">
{#each activeView.columns as col, i (col.id)}
<div class="col-edit-card">
<div class="col-colors">
{#each COLUMN_COLORS as c}
<button
class="col-color-dot"
class:active={col.color === c}
style="background-color: {c}"
onclick={() => updateColumn(i, { color: c })}
></button>
{/each}
</div>
<input
class="col-name-input"
type="text"
value={col.name}
oninput={(e) => updateColumn(i, { name: e.currentTarget.value })}
/>
<div class="col-actions">
<button
class="col-action-btn"
onclick={() => moveColumn(i, -1)}
disabled={i === 0}
title="Nach links"
>
<ArrowLeft size={14} />
</button>
<button
class="col-action-btn"
onclick={() => moveColumn(i, 1)}
disabled={i === activeView.columns.length - 1}
title="Nach rechts"
>
<ArrowRight size={14} />
</button>
<button
class="col-action-btn col-delete-btn"
onclick={() => removeColumn(i)}
disabled={activeView.columns.length <= 1}
title="Spalte löschen"
>
<Trash size={14} />
</button>
</div>
</div>
{/each}
<div class="add-col-bar">
<button class="col-add-btn" onclick={addColumn} title="Spalte hinzufügen">
<Plus size={16} />
<Plus size={14} />
<span>Spalte</span>
</button>
</div>
{:else}
@ -208,7 +162,14 @@
<!-- Board Content -->
{#if activeView}
<BoardViewRenderer view={activeView} {layoutOverride} />
<BoardViewRenderer
view={activeView}
{layoutOverride}
onColumnRename={columnsEditable ? (i, name) => updateColumn(i, { name }) : undefined}
onColumnColorChange={columnsEditable ? (i, color) => updateColumn(i, { color }) : undefined}
onColumnMove={columnsEditable ? moveColumn : undefined}
onColumnDelete={columnsEditable ? removeColumn : undefined}
/>
{:else}
<div class="empty-state">
<p class="text-muted-foreground">Views werden geladen...</p>
@ -319,132 +280,27 @@
color: white;
}
/* ── Column Editor ───────────────────────────────────── */
.column-editor {
/* ── Add Column Bar ───────────────────────────────────── */
.add-col-bar {
display: flex;
gap: 0.75rem;
padding: 0.75rem 1.5rem;
overflow-x: auto;
scrollbar-width: none;
padding: 0.5rem 1.5rem;
flex-shrink: 0;
background: rgba(139, 92, 246, 0.03);
border-bottom: 1px solid rgba(139, 92, 246, 0.08);
}
.column-editor::-webkit-scrollbar {
display: none;
}
:global(.dark) .column-editor {
background: rgba(139, 92, 246, 0.05);
border-bottom-color: rgba(139, 92, 246, 0.12);
}
.col-edit-card {
display: flex;
flex-direction: column;
gap: 0.375rem;
padding: 0.5rem 0.75rem;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 0.5rem;
min-width: 140px;
flex-shrink: 0;
}
:global(.dark) .col-edit-card {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.1);
}
.col-colors {
display: flex;
gap: 0.25rem;
}
.col-color-dot {
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.15s;
}
.col-color-dot:hover {
transform: scale(1.2);
}
.col-color-dot.active {
border-color: white;
box-shadow: 0 0 0 2px currentColor;
transform: scale(1.15);
}
.col-name-input {
font-size: 0.8125rem;
font-weight: 600;
color: #374151;
background: transparent;
border: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 0.125rem 0;
outline: none;
width: 100%;
}
.col-name-input:focus {
border-bottom-color: #8b5cf6;
}
:global(.dark) .col-name-input {
color: #f3f4f6;
border-bottom-color: rgba(255, 255, 255, 0.1);
}
.col-actions {
display: flex;
gap: 0.25rem;
justify-content: flex-end;
}
.col-action-btn {
padding: 0.25rem;
border-radius: 0.25rem;
color: #6b7280;
cursor: pointer;
transition: all 0.15s;
background: transparent;
border: none;
}
.col-action-btn:hover:not(:disabled) {
color: #374151;
background: rgba(0, 0, 0, 0.05);
}
.col-action-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
:global(.dark) .col-action-btn {
color: #9ca3af;
}
:global(.dark) .col-action-btn:hover:not(:disabled) {
color: #f3f4f6;
background: rgba(255, 255, 255, 0.1);
}
.col-delete-btn:hover:not(:disabled) {
color: #ef4444 !important;
background: rgba(239, 68, 68, 0.1) !important;
}
.col-add-btn {
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: 40px;
border-radius: 0.5rem;
border: 2px dashed rgba(139, 92, 246, 0.3);
gap: 0.375rem;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
border-radius: 9999px;
border: 1px dashed rgba(139, 92, 246, 0.4);
color: #8b5cf6;
background: transparent;
cursor: pointer;
transition: all 0.15s;
flex-shrink: 0;
align-self: center;
}
.col-add-btn:hover {
border-color: #8b5cf6;