refactor(todo/web): remove edit mode, rename pages, add inline editing & drag reorder

- Remove edit mode entirely (toolbar, context, Layout pill in PillNav)
- Remove onAddColumn/add-sheet from all layouts (FokusLayout, Grid, Kanban)
- Rename SecondaryPage → TodoPage, secondaryPage → page (i18n keys, CSS)
- ViewColumnHeader: always-on inline editing (click name, click color dot)
- TodoPage: contenteditable title (invisible, no input styling)
- TodoPage: drag handle bar for reorder via HTML5 drag & drop
- FokusLayout: add matching drag handle to board sheets
- Clean up dead spotlight actions (/today, /upcoming, /kanban)
- Remove /statistics reference from QuickInputBar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-01 20:31:36 +02:00
parent 990ade352f
commit 933715c7d9
14 changed files with 603 additions and 640 deletions

View file

@ -16,7 +16,6 @@
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
onColumnDelete?: (colIdx: number) => void;
onColumnClose?: (colIdx: number) => void;
onAddColumn?: () => void;
trailing?: Snippet;
}
@ -28,7 +27,6 @@
onColumnMove,
onColumnDelete,
onColumnClose,
onAddColumn,
trailing,
}: Props = $props();
@ -105,7 +103,6 @@
{onColumnMove}
{onColumnDelete}
{onColumnClose}
{onAddColumn}
{trailing}
/>
{:else if activeLayout === 'grid'}
@ -119,7 +116,6 @@
{onColumnColorChange}
{onColumnMove}
{onColumnDelete}
{onAddColumn}
/>
{:else}
<KanbanLayout
@ -132,6 +128,5 @@
{onColumnColorChange}
{onColumnMove}
{onColumnDelete}
{onAddColumn}
/>
{/if}

View file

@ -9,7 +9,7 @@
import ViewColumnHeader from './ViewColumnHeader.svelte';
import { tasksStore } from '$lib/stores/tasks.svelte';
import { todoSettings } from '$lib/stores/settings.svelte';
import { X } from '@manacore/shared-icons';
import { X, DotsSixVertical } from '@manacore/shared-icons';
interface Props {
columns: GroupedColumn[];
@ -22,7 +22,6 @@
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
onColumnDelete?: (colIdx: number) => void;
onColumnClose?: (colIdx: number) => void;
onAddColumn?: () => void;
trailing?: Snippet;
}
@ -37,7 +36,6 @@
onColumnMove,
onColumnDelete,
onColumnClose,
onAddColumn,
trailing,
}: Props = $props();
@ -135,6 +133,11 @@
{#each columns as column, i (column.id)}
{@const tasks = localTasksByColumn[column.id] || column.tasks}
<div class="fokus-sheet" class:sheet-completed={column.name === 'Erledigt'}>
<div class="drag-handle-bar">
<span class="drag-handle">
<DotsSixVertical size={14} />
</span>
</div>
<div class="sheet-header-row">
<ViewColumnHeader
name={column.name}
@ -206,16 +209,7 @@
</div>
{/each}
{#if onAddColumn}
<div class="fokus-sheet add-sheet">
<button class="add-sheet-btn" onclick={onAddColumn}>
<span class="add-sheet-icon">+</span>
<span class="add-sheet-label">Neues Board</span>
</button>
</div>
{/if}
<!-- Trailing content (Neue Seite, secondary pages) -->
<!-- Trailing content (pages) -->
{#if trailing}
{@render trailing()}
{/if}
@ -248,6 +242,36 @@
display: none;
}
.drag-handle-bar {
display: flex;
justify-content: center;
padding: 0.25rem 0 0;
}
.drag-handle {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 14px;
color: #d1d5db;
cursor: grab;
border-radius: 0.25rem;
transition: color 0.15s;
}
.drag-handle:hover {
color: #9ca3af;
}
.drag-handle:active {
cursor: grabbing;
}
:global(.dark) .drag-handle {
color: #3f3b38;
}
:global(.dark) .drag-handle:hover {
color: #6b7280;
}
.fokus-sheet {
flex: 0 0 auto;
width: var(--sheet-width, min(840px, 85vw));
@ -337,42 +361,6 @@
background: color-mix(in srgb, var(--color-primary) 4%, transparent);
}
/* Add sheet */
.add-sheet {
border: 2px dashed color-mix(in srgb, var(--color-primary) 30%, transparent) !important;
background: color-mix(in srgb, var(--color-primary) 2%, transparent) !important;
box-shadow: none !important;
}
.add-sheet:hover {
border-color: var(--color-primary) !important;
background: color-mix(in srgb, var(--color-primary) 6%, transparent) !important;
}
.add-sheet-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
height: 100%;
min-height: 200px;
background: transparent;
border: none;
cursor: pointer;
}
.add-sheet-icon {
font-size: 2rem;
font-weight: 300;
color: var(--color-primary);
line-height: 1;
}
.add-sheet-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-primary);
}
/* Heute erledigt section */
.completed-today {
padding: 0.75rem 1rem 1rem;

View file

@ -13,7 +13,6 @@
onColumnColorChange?: (colIdx: number, color: string) => void;
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
onColumnDelete?: (colIdx: number) => void;
onAddColumn?: () => void;
}
let {
@ -26,7 +25,6 @@
onColumnColorChange,
onColumnMove,
onColumnDelete,
onAddColumn,
}: Props = $props();
</script>
@ -48,15 +46,6 @@
/>
</div>
{/each}
{#if onAddColumn}
<div class="grid-cell">
<button class="add-column-card" onclick={onAddColumn}>
<span class="add-icon">+</span>
<span class="add-label">Neues Board</span>
</button>
</div>
{/if}
</div>
<style>

View file

@ -13,7 +13,6 @@
onColumnColorChange?: (colIdx: number, color: string) => void;
onColumnMove?: (colIdx: number, dir: -1 | 1) => void;
onColumnDelete?: (colIdx: number) => void;
onAddColumn?: () => void;
}
let {
@ -26,7 +25,6 @@
onColumnColorChange,
onColumnMove,
onColumnDelete,
onAddColumn,
}: Props = $props();
</script>
@ -48,15 +46,6 @@
/>
</div>
{/each}
{#if onAddColumn}
<div class="kanban-column-wrapper">
<button class="add-column-card" onclick={onAddColumn}>
<span class="add-column-icon">+</span>
<span class="add-column-label">Neues Board</span>
</button>
</div>
{/if}
</div>
<style>

View file

@ -1,5 +1,4 @@
<script lang="ts">
import { getContext } from 'svelte';
import { ArrowLeft, ArrowRight, Trash } from '@manacore/shared-icons';
interface Props {
@ -26,11 +25,9 @@
onDelete,
}: Props = $props();
const editModeCtx: { readonly active: boolean } | undefined = getContext('editMode');
let editMode = $derived(editModeCtx?.active ?? false);
let editable = $derived(editMode && !!onRename);
let showColorPicker = $state(false);
let isEditingName = $state(false);
let editInputEl = $state<HTMLInputElement | null>(null);
const PRESET_COLORS = [
'#EF4444',
@ -50,6 +47,25 @@
'#6B7280',
'#334155',
];
function startEditing() {
if (!onRename) return;
isEditingName = true;
requestAnimationFrame(() => {
editInputEl?.select();
});
}
function finishEditing() {
isEditingName = false;
}
function handleNameKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' || e.key === 'Escape') {
e.preventDefault();
finishEditing();
}
}
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
@ -57,13 +73,12 @@
<div class="picker-backdrop" onclick={() => (showColorPicker = false)}></div>
{/if}
<div class="column-header" class:editing={editable}>
{#if editable}
<!-- Edit mode: same layout, color dot is clickable, name is input -->
<div class="header-left">
<div class="column-header">
<div class="header-left">
{#if onColorChange}
<div class="color-dot-wrapper">
<button
class="color-dot editable"
class="color-dot clickable"
style="background-color: {color}"
onclick={() => (showColorPicker = !showColorPicker)}
title="Farbe ändern"
@ -78,7 +93,7 @@
class:active={color === c}
style="background-color: {c}"
onclick={() => {
onColorChange?.(c);
onColorChange(c);
showColorPicker = false;
}}
></button>
@ -89,56 +104,41 @@
<input
type="color"
value={color}
oninput={(e) => onColorChange?.(e.currentTarget.value)}
oninput={(e) => onColorChange(e.currentTarget.value)}
class="custom-color-input"
/>
</label>
</div>
{/if}
</div>
{:else}
<span class="color-dot" style="background-color: {color}"></span>
{/if}
{#if isEditingName && onRename}
<input
bind:this={editInputEl}
class="name-input"
type="text"
value={name}
oninput={(e) => onRename?.(e.currentTarget.value)}
oninput={(e) => onRename(e.currentTarget.value)}
onblur={finishEditing}
onkeydown={handleNameKeydown}
/>
</div>
<div class="edit-actions">
<button
class="act-btn"
onclick={() => onMove?.(-1)}
disabled={columnIndex === 0}
title="Nach links"
{:else}
<span
class="column-name"
class:editable={!!onRename}
role={onRename ? 'button' : undefined}
tabindex={onRename ? 0 : undefined}
onclick={startEditing}
onkeydown={(e) => {
if (e.key === 'Enter') startEditing();
}}>{name}</span
>
<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>
{: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}
{/if}
</div>
<span class="task-count">{taskCount}</span>
</div>
<style>
@ -164,6 +164,27 @@
flex-shrink: 0;
}
.color-dot-wrapper {
position: relative;
flex-shrink: 0;
}
.color-dot.clickable {
width: 0.75rem;
height: 0.75rem;
cursor: pointer;
border: none;
padding: 0;
transition: all 0.15s;
}
.color-dot.clickable:hover {
transform: scale(1.25);
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
}
:global(.dark) .color-dot.clickable:hover {
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.15);
}
.column-name {
font-size: 0.875rem;
font-weight: 600;
@ -172,11 +193,42 @@
overflow: hidden;
text-overflow: ellipsis;
}
.column-name.editable {
cursor: text;
border-radius: 0.125rem;
padding: 0.0625rem 0.25rem;
margin: -0.0625rem -0.25rem;
}
.column-name.editable:hover {
background: rgba(0, 0, 0, 0.04);
}
:global(.dark) .column-name.editable:hover {
background: rgba(255, 255, 255, 0.06);
}
:global(.dark) .column-name {
color: #f3f4f6;
}
.name-input {
flex: 1;
font-size: 0.875rem;
font-weight: 600;
color: #374151;
background: transparent;
border: none;
border-bottom: 1px solid rgba(139, 92, 246, 0.4);
padding: 0.125rem 0;
outline: none;
min-width: 0;
}
.name-input:focus {
border-bottom-color: #8b5cf6;
}
:global(.dark) .name-input {
color: #f3f4f6;
}
.task-count {
font-size: 0.75rem;
font-weight: 500;
@ -192,80 +244,6 @@
color: #6b7280;
}
/* ── Edit mode ────────────────────────────────────────── */
.color-dot-wrapper {
position: relative;
flex-shrink: 0;
}
.color-dot.editable {
width: 0.875rem;
height: 0.875rem;
cursor: pointer;
border: none;
padding: 0;
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.5);
transition: all 0.15s;
}
.color-dot.editable:hover {
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.7);
transform: scale(1.15);
}
.name-input {
flex: 1;
font-size: 0.875rem;
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;
margin-left: 0.375rem;
}
.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;
}
/* ── Color Picker Popup ──────────────────────────────── */
.picker-backdrop {

View file

@ -3,21 +3,45 @@
import { isToday, isPast, startOfDay, addDays, subHours, format } from 'date-fns';
import { t } from 'svelte-i18n';
import type { Task } from '@todo/shared';
import { X, Circle, Minus } from '@manacore/shared-icons';
import { X, Circle, Minus, DotsSixVertical } from '@manacore/shared-icons';
import KanbanTaskCard from '../kanban/KanbanTaskCard.svelte';
import { tasksStore } from '$lib/stores/tasks.svelte';
import { todoSettings } from '$lib/stores/settings.svelte';
interface Props {
pageId: string;
title?: string;
onClose: () => void;
onMinimize?: () => void;
onRename?: (name: string) => void;
}
let { pageId, onClose, onMinimize }: Props = $props();
let { pageId, title: customTitle, onClose, onMinimize, onRename }: Props = $props();
const tasksCtx: { readonly value: Task[] } = getContext('tasks');
let titleEl = $state<HTMLSpanElement | null>(null);
let isTitleFocused = $state(false);
// Set initial text content without reactive binding (avoids cursor jump)
$effect(() => {
if (titleEl && !isTitleFocused) {
titleEl.textContent = displayTitle;
}
});
function handleTitleInput() {
const text = titleEl?.textContent?.trim() ?? '';
if (text && onRename) onRename(text);
}
function handleTitleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
(e.target as HTMLElement).blur();
}
}
const PAGE_META: Record<string, { title: string; color: string }> = {
todo: { title: 'To Do', color: '#6B7280' },
completed: { title: 'Erledigt', color: '#22C55E' },
@ -29,7 +53,8 @@
'no-date': { title: 'Ohne Datum', color: '#6B7280' },
};
let meta = $derived(PAGE_META[pageId] ?? { title: pageId, color: '#6B7280' });
let pageMeta = $derived(PAGE_META[pageId] ?? { title: pageId, color: '#6B7280' });
let displayTitle = $derived(customTitle ?? pageMeta.title);
let filteredTasks = $derived.by(() => {
const tasks = tasksCtx.value;
@ -127,9 +152,9 @@
const time = format(date, 'HH:mm');
if (pageId === 'completed') {
const dateStr = format(date, 'dd.MM.');
return $t('secondaryPage.completedAtDateTime', { values: { date: dateStr, time } });
return $t('page.completedAtDateTime', { values: { date: dateStr, time } });
}
return $t('secondaryPage.completedAtTime', { values: { time } });
return $t('page.completedAtTime', { values: { time } });
}
let newTaskTitle = $state('');
@ -150,11 +175,27 @@
}
</script>
<div class="secondary-page" style="width: {sheetWidth}">
<div class="page-header">
<div class="todo-page" style="width: {sheetWidth}">
<div class="drag-handle-bar">
<span class="drag-handle">
<DotsSixVertical size={14} />
</span>
</div>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="page-header" ondragstart={(e) => e.preventDefault()}>
<div class="header-left">
<span class="color-dot" style="background-color: {meta.color}"></span>
<span class="page-title">{meta.title}</span>
<span class="color-dot" style="background-color: {pageMeta.color}"></span>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span
bind:this={titleEl}
class="page-title"
contenteditable={!!onRename}
oninput={handleTitleInput}
onkeydown={handleTitleKeydown}
onfocus={() => (isTitleFocused = true)}
onblur={() => (isTitleFocused = false)}
></span>
<span class="task-count">{filteredTasks.length}</span>
</div>
<div class="header-actions">
@ -169,7 +210,8 @@
</div>
</div>
<div class="page-body">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="page-body" ondragstart={(e) => e.preventDefault()}>
{#if pageId === 'completed'}
{#each filteredTasks as task (task.id)}
<div class="task-card-wrapper completed-task">
@ -198,7 +240,7 @@
{#if recentlyCompleted.length > 0}
<div class="completed-section">
<span class="completed-label">{$t('secondaryPage.recentlyCompleted')}</span>
<span class="completed-label">{$t('page.recentlyCompleted')}</span>
{#each recentlyCompleted as task (task.id)}
<div class="task-card-wrapper completed-task">
<KanbanTaskCard
@ -223,7 +265,7 @@
bind:this={inputEl}
bind:value={newTaskTitle}
class="inline-create-input"
placeholder={$t('secondaryPage.newTaskPlaceholder')}
placeholder={$t('page.newTaskPlaceholder')}
onkeydown={(e) => {
if (e.key === 'Enter') handleInlineCreate();
}}
@ -234,7 +276,7 @@
</div>
<style>
.secondary-page {
.todo-page {
flex: 0 0 auto;
min-height: 60vh;
background: #fffef5;
@ -246,13 +288,43 @@
flex-direction: column;
animation: fadeIn 0.25s ease-out;
}
:global(.dark) .secondary-page {
:global(.dark) .todo-page {
background-color: #252220;
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.06);
}
.drag-handle-bar {
display: flex;
justify-content: center;
padding: 0.25rem 0 0;
}
.drag-handle {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 14px;
color: #d1d5db;
cursor: grab;
border-radius: 0.25rem;
transition: color 0.15s;
}
.drag-handle:hover {
color: #9ca3af;
}
.drag-handle:active {
cursor: grabbing;
}
:global(.dark) .drag-handle {
color: #3f3b38;
}
:global(.dark) .drag-handle:hover {
color: #6b7280;
}
@keyframes fadeIn {
from {
opacity: 0;
@ -288,6 +360,11 @@
font-size: 0.875rem;
font-weight: 600;
color: #374151;
outline: none;
border-radius: 0.125rem;
}
.page-title[contenteditable='true'] {
cursor: text;
}
:global(.dark) .page-title {
color: #f3f4f6;

View file

@ -225,7 +225,7 @@
"taskOptions": "Aufgaben-Optionen",
"switchView": "Ansicht wechseln"
},
"secondaryPage": {
"page": {
"recentlyCompleted": "Kürzlich erledigt",
"completedAtTime": "{time} Uhr",
"completedAtDateTime": "{date}, {time} Uhr",

View file

@ -225,7 +225,7 @@
"taskOptions": "Task options",
"switchView": "Switch view"
},
"secondaryPage": {
"page": {
"recentlyCompleted": "Recently completed",
"completedAtTime": "{time}",
"completedAtDateTime": "{date}, {time}",

View file

@ -225,7 +225,7 @@
"taskOptions": "Opciones de tareas",
"switchView": "Cambiar vista"
},
"secondaryPage": {
"page": {
"recentlyCompleted": "Completadas recientemente",
"completedAtTime": "{time}",
"completedAtDateTime": "{date}, {time}",

View file

@ -225,7 +225,7 @@
"taskOptions": "Options des tâches",
"switchView": "Changer de vue"
},
"secondaryPage": {
"page": {
"recentlyCompleted": "Récemment terminées",
"completedAtTime": "{time} h",
"completedAtDateTime": "{date}, {time} h",

View file

@ -225,7 +225,7 @@
"taskOptions": "Opzioni attività",
"switchView": "Cambia vista"
},
"secondaryPage": {
"page": {
"recentlyCompleted": "Completate di recente",
"completedAtTime": "ore {time}",
"completedAtDateTime": "{date}, ore {time}",

View file

@ -0,0 +1,51 @@
/**
* Minimized pages context layout owns the state, page reads/writes via context.
*
* Layout calls `createMinimizedPagesContext()` + `setContext`.
* Page calls `getContext('minimizedPages')` to get the same object.
*/
import type { MinimizedPage } from '@manacore/shared-ui';
export const PAGE_META: Record<string, { title: string; color: string }> = {
todo: { title: 'To Do', color: '#6B7280' },
completed: { title: 'Erledigt', color: '#22C55E' },
today: { title: 'Heute', color: '#F59E0B' },
overdue: { title: 'Überfällig', color: '#EF4444' },
all: { title: 'Alle Aufgaben', color: '#3B82F6' },
'high-priority': { title: 'Hohe Priorität', color: '#EF4444' },
'this-week': { title: 'Diese Woche', color: '#8B5CF6' },
'no-date': { title: 'Ohne Datum', color: '#6B7280' },
};
export interface MinimizedPagesContext {
readonly pages: MinimizedPage[];
readonly hasPages: boolean;
/** Called by page to sync its openPages state */
sync(openPages: { id: string; minimized: boolean }[]): void;
/** Called by page on unmount */
clear(): void;
}
export function createMinimizedPagesContext(): MinimizedPagesContext {
let pages = $state<MinimizedPage[]>([]);
return {
get pages() {
return pages;
},
get hasPages() {
return pages.length > 0;
},
sync(openPages: { id: string; minimized: boolean }[]) {
pages = openPages
.filter((p) => p.minimized)
.map((p) => {
const meta = PAGE_META[p.id] ?? { title: p.id, color: '#6B7280' };
return { id: p.id, title: meta.title, color: meta.color };
});
},
clear() {
pages = [];
},
};
}

View file

@ -3,7 +3,13 @@
import { page } from '$app/stores';
import { setContext } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation, QuickInputBar, ImmersiveModeToggle } from '@manacore/shared-ui';
import {
PillNavigation,
QuickInputBar,
ImmersiveModeToggle,
BottomStack,
MinimizedTabs,
} from '@manacore/shared-ui';
import {
SplitPaneContainer,
setSplitPanelContext,
@ -54,6 +60,7 @@
import { useAllTasks, useAllBoardViews } from '$lib/data/task-queries';
import SyncIndicator from '$lib/components/SyncIndicator.svelte';
import { List, X } from '@manacore/shared-icons';
import { createMinimizedPagesContext } from '$lib/stores/minimized-pages.svelte';
// Live queries — auto-update when IndexedDB changes (local writes, sync, other tabs)
const allTasks = useAllTasks();
@ -86,20 +93,6 @@
},
});
// Edit mode state — shared between layout (PillNav button) and page (editor)
let editMode = $state(false);
setContext('editMode', {
get active() {
return editMode;
},
toggle() {
editMode = !editMode;
},
set(val: boolean) {
editMode = val;
},
});
// Guest welcome modal state
let showGuestWelcome = $state(false);
@ -187,6 +180,10 @@
// FilterStrip visibility (toggle via Filter button in PillNav)
let isFilterStripVisible = $derived(!todoSettings.filterStripCollapsed);
// Minimized page tabs add extra height to the bottom bar stack
let hasMinimizedTabs = $derived(minimizedPagesStore.hasPages);
const MINIMIZED_TABS_HEIGHT = 36; // px
// Use theme store's isDark directly
let isDark = $derived(theme.isDark);
@ -242,7 +239,7 @@
// Keep navRoutes for keyboard shortcuts (Ctrl+1-3)
const viewRoutes: Record<string, string> = { fokus: '/' };
// Tags and Layout stay as standalone pills (toggle behavior, not navigation)
// Tags pill (toggle behavior, not navigation)
let baseNavItems = $derived<PillNavItem[]>([
{
href: '/',
@ -251,19 +248,6 @@
onClick: handleTagStripToggle,
active: isFilterStripVisible,
},
...($page.url.pathname === '/' || $page.url.pathname === ''
? [
{
href: '/',
label: editMode ? 'Fertig' : 'Layout',
icon: editMode ? 'check' : 'grid',
onClick: () => {
editMode = !editMode;
},
active: editMode,
},
]
: []),
]);
// Navigation items filtered by visibility settings (with fallback for guest mode)
@ -341,25 +325,18 @@
category: 'Erstellen',
onExecute: () => goto('/'),
},
{ id: 'today', label: 'Heute', category: 'Navigation', onExecute: () => goto('/today') },
{
id: 'upcoming',
label: 'Demnächst',
category: 'Navigation',
onExecute: () => goto('/upcoming'),
},
{
id: 'kanban',
label: 'Kanban Board',
category: 'Navigation',
onExecute: () => goto('/kanban'),
},
{
id: 'settings',
label: 'Einstellungen',
category: 'Navigation',
onExecute: () => goto('/settings'),
},
{
id: 'tags',
label: 'Tags verwalten',
category: 'Navigation',
onExecute: () => goto('/tags'),
},
];
async function handleAuthReady() {
@ -476,8 +453,54 @@
{/if}
{/if}
<!-- Minimized Page Tabs (between PillNav and QuickInputBar) -->
{#if hasMinimizedTabs}
<div
class="minimized-tabs-bar"
style="--tabs-bottom: {(() => {
let offset = 16;
if (!isPillNavCollapsed) offset += 68;
if (!isPillNavCollapsed && isFilterStripVisible) offset += 50;
return `${offset}px`;
})()}"
>
<div class="minimized-tabs-inner">
{#each minimizedPagesStore.pages as pg (pg.id)}
<div
class="minimized-tab"
role="button"
tabindex="0"
onclick={() => {
window.dispatchEvent(new CustomEvent('restore-page', { detail: pg.id }));
}}
>
<span class="minimized-tab-dot" style="background-color: {pg.color}"></span>
<span class="minimized-tab-title">{pg.title}</span>
<button
class="minimized-tab-close"
onclick={(e) => {
e.stopPropagation();
window.dispatchEvent(new CustomEvent('remove-page', { detail: pg.id }));
}}
title="Schließen"
>
<X size={10} />
</button>
</div>
{/each}
<button
class="minimized-tab-add"
onclick={() => window.dispatchEvent(new CustomEvent('toggle-page-picker'))}
title="Neue Seite hinzufügen"
>
<Plus size={14} />
</button>
</div>
</div>
{/if}
<!-- Global Quick Input Bar -->
{#if $page.url.pathname === '/' || $page.url.pathname === '/statistics'}
{#if $page.url.pathname === '/'}
<QuickInputBar
onSearch={handleSearch}
onSelect={handleSelect}
@ -492,13 +515,26 @@
locale={$locale || 'de'}
appIcon="todo"
hasFabRight={true}
bottomOffset={isPillNavCollapsed ? '16px' : isFilterStripVisible ? '180px' : '110px'}
bottomOffset={(() => {
let offset = 16;
if (!isPillNavCollapsed) offset += 68;
if (!isPillNavCollapsed && isFilterStripVisible) offset += 50;
if (hasMinimizedTabs) offset += MINIMIZED_TABS_HEIGHT;
return `${offset}px`;
})()}
/>
{/if}
<!-- FAB to toggle PillNav visibility -->
<button
class="pillnav-fab"
style="--fab-bottom: {(() => {
let offset = 20;
if (!isPillNavCollapsed) offset += 68;
if (!isPillNavCollapsed && isFilterStripVisible) offset += 50;
if (hasMinimizedTabs) offset += MINIMIZED_TABS_HEIGHT;
return `${offset}px`;
})()}"
onclick={handlePillNavToggle}
title={isPillNavCollapsed ? 'Navigation anzeigen' : 'Navigation ausblenden'}
aria-label={isPillNavCollapsed ? 'Navigation anzeigen' : 'Navigation ausblenden'}
@ -506,10 +542,10 @@
>
{#if isPillNavCollapsed}
<!-- Menu icon -->
<List size={20} class="fab-icon" />
<List size="48" weight="bold" />
{:else}
<!-- Close icon -->
<X size={20} class="fab-icon" />
<X size="48" weight="bold" />
{/if}
</button>
{/if}
@ -635,13 +671,14 @@
}
}
/* FAB to toggle PillNav */
/* FAB to toggle PillNav — sits right next to the centered QuickInputBar */
.pillnav-fab {
position: fixed;
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
right: 1rem;
width: 54px;
height: 54px;
bottom: calc(var(--fab-bottom, 16px) + env(safe-area-inset-bottom, 0px));
/* Anchor to center, then offset by half of InputBar max-width (350px) + gap */
left: calc(50% + 350px + 0.75rem);
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--color-surface-elevated-2);
border: 1px solid var(--color-border);
@ -654,6 +691,14 @@
transition: all 0.2s ease;
}
/* On narrower screens, FAB sits at the right edge of the padded input area */
@media (max-width: 900px) {
.pillnav-fab {
left: auto;
right: 1rem;
}
}
.pillnav-fab:hover {
transform: scale(1.05);
}
@ -662,9 +707,112 @@
transform: scale(0.95);
}
.fab-icon {
width: 24px;
height: 24px;
.pillnav-fab :global(svg) {
color: var(--color-foreground);
}
/* ── Minimized Page Tabs Bar ─────────────────────────── */
.minimized-tabs-bar {
position: fixed;
bottom: calc(var(--tabs-bottom, 16px) + env(safe-area-inset-bottom, 0px));
left: 50%;
transform: translateX(-50%);
z-index: 1001;
}
.minimized-tabs-inner {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.3rem 0.5rem;
background: var(--color-surface-elevated, #fffef5);
border: 1px solid var(--color-border, rgba(0, 0, 0, 0.12));
border-radius: 0.625rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
overflow-x: auto;
scrollbar-width: none;
}
:global(.dark) .minimized-tabs-inner {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
}
.minimized-tabs-inner::-webkit-scrollbar {
display: none;
}
.minimized-tab {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
background: transparent;
border: none;
border-radius: 0.3rem;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
flex-shrink: 0;
font-family: inherit;
}
.minimized-tab:hover {
background: rgba(0, 0, 0, 0.05);
}
:global(.dark) .minimized-tab:hover {
background: rgba(255, 255, 255, 0.08);
}
.minimized-tab-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 9999px;
flex-shrink: 0;
}
.minimized-tab-title {
font-size: 0.75rem;
font-weight: 500;
color: var(--color-muted-foreground, #6b7280);
}
.minimized-tab-close {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border: none;
background: transparent;
color: var(--color-muted-foreground, #d1d5db);
border-radius: 0.125rem;
cursor: pointer;
padding: 0;
transition: all 0.15s;
opacity: 0.5;
}
.minimized-tab-close:hover {
opacity: 1;
background: rgba(0, 0, 0, 0.06);
}
:global(.dark) .minimized-tab-close:hover {
background: rgba(255, 255, 255, 0.08);
}
.minimized-tab-add {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 0.3rem;
border: none;
background: transparent;
color: var(--color-muted-foreground, #9ca3af);
cursor: pointer;
flex-shrink: 0;
transition: all 0.15s;
opacity: 0.6;
}
.minimized-tab-add:hover {
opacity: 1;
color: var(--color-primary, #8b5cf6);
}
</style>

View file

@ -1,34 +1,59 @@
<script lang="ts">
import { getContext } from 'svelte';
import { getContext, onDestroy } from 'svelte';
import type { LocalBoardView } from '$lib/data/local-store';
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 { Plus, Minus, X } from '@manacore/shared-icons';
import { Plus } from '@manacore/shared-icons';
import PagePicker from '$lib/components/pages/PagePicker.svelte';
import SecondaryPage from '$lib/components/pages/SecondaryPage.svelte';
import TodoPage from '$lib/components/pages/TodoPage.svelte';
import { minimizedPagesStore } from '$lib/stores/minimized-pages.svelte';
// Get active view + edit mode from layout context
// Get active view from layout context
const activeViewCtx: { readonly value: LocalBoardView | null } = getContext('activeView');
const editModeCtx: { readonly active: boolean; toggle(): void; set(val: boolean): void } =
getContext('editMode');
let editMode = $derived(editModeCtx.active);
let activeView = $derived(activeViewCtx.value);
let pageTitle = $derived(activeView?.name ?? 'Aufgaben');
// ── Secondary Pages ─────────────────────────────────────
// ── Pages ───────────────────────────────────────────────
let showPagePicker = $state(false);
let openPages = $state<{ id: string; minimized: boolean }[]>([]);
let openPages = $state<{ id: string; minimized: boolean; customTitle?: string }[]>([
{ id: 'todo', minimized: false },
]);
let expandedPages = $derived(openPages.filter((p) => !p.minimized));
let minimizedPages = $derived(openPages.filter((p) => p.minimized));
// Sync minimized pages to shared store so layout can render tabs
$effect(() => {
minimizedPagesStore.set(openPages);
});
onDestroy(() => minimizedPagesStore.clear());
// Listen for events from layout's minimized tab bar
function onRestorePage(e: Event) {
handleRestorePage((e as CustomEvent).detail);
}
function onRemovePage(e: Event) {
handleRemovePage((e as CustomEvent).detail);
}
function onTogglePagePicker() {
togglePagePicker();
}
$effect(() => {
window.addEventListener('restore-page', onRestorePage);
window.addEventListener('remove-page', onRemovePage);
window.addEventListener('toggle-page-picker', onTogglePagePicker);
return () => {
window.removeEventListener('restore-page', onRestorePage);
window.removeEventListener('remove-page', onRemovePage);
window.removeEventListener('toggle-page-picker', onTogglePagePicker);
};
});
function handleAddPage(pageId: string) {
if (!openPages.some((p) => p.id === pageId)) {
openPages = [...openPages, { id: pageId, minimized: false }];
} else {
// Restore if minimized
openPages = openPages.map((p) => (p.id === pageId ? { ...p, minimized: false } : p));
}
showPagePicker = false;
@ -46,6 +71,44 @@
openPages = openPages.map((p) => (p.id === pageId ? { ...p, minimized: false } : p));
}
function handleRenamePage(pageId: string, name: string) {
openPages = openPages.map((p) => (p.id === pageId ? { ...p, customTitle: name } : p));
}
// ── Page drag reorder ───────────────────────────────────
let dragPageId = $state<string | null>(null);
function handlePageDragStart(e: DragEvent, pageId: string) {
dragPageId = pageId;
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', pageId);
}
}
function handlePageDragOver(e: DragEvent) {
if (!dragPageId) return;
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
}
function handlePageDrop(e: DragEvent, targetPageId: string) {
e.preventDefault();
if (!dragPageId || dragPageId === targetPageId) return;
const fromIdx = openPages.findIndex((p) => p.id === dragPageId);
const toIdx = openPages.findIndex((p) => p.id === targetPageId);
if (fromIdx === -1 || toIdx === -1) return;
const pages = [...openPages];
const [moved] = pages.splice(fromIdx, 1);
pages.splice(toIdx, 0, moved);
openPages = pages;
dragPageId = null;
}
function handlePageDragEnd() {
dragPageId = null;
}
function togglePagePicker() {
showPagePicker = !showPagePicker;
}
@ -56,33 +119,7 @@
updateView({ columns });
}
// ── Edit helpers ────────────────────────────────────────
const GROUPBY_OPTIONS = [
{ value: 'status', label: 'Status' },
{ value: 'priority', label: 'Priorität' },
{ value: 'dueDate', label: 'Fälligkeit' },
{ value: 'custom', label: 'Benutzerdefiniert' },
];
const WIDTH_OPTIONS: { value: PageWidth; label: string }[] = [
{ value: 'narrow', label: 'S' },
{ value: 'medium', label: 'M' },
{ value: 'wide', label: 'L' },
{ value: 'full', label: 'XL' },
];
const COLUMN_COLORS = [
'#EF4444',
'#F59E0B',
'#22C55E',
'#3B82F6',
'#8B5CF6',
'#EC4899',
'#14B8A6',
'#F97316',
'#6B7280',
];
// ── Column helpers ──────────────────────────────────────
async function updateView(data: Partial<LocalBoardView>) {
if (!activeView) return;
@ -102,17 +139,6 @@
updateView({ columns });
}
function addColumn() {
if (!activeView) return;
const newCol = {
id: `col-${crypto.randomUUID().slice(0, 8)}`,
name: 'Neue Spalte',
color: COLUMN_COLORS[activeView.columns.length % COLUMN_COLORS.length],
match: { type: 'custom' as const, value: `custom-${Date.now()}` },
};
updateView({ columns: [...$state.snapshot(activeView.columns), newCol] });
}
function moveColumn(colIdx: number, dir: -1 | 1) {
if (!activeView) return;
const cols = $state.snapshot(activeView.columns);
@ -126,17 +152,6 @@
activeView?.groupBy === 'status' || activeView?.groupBy === 'custom'
);
const PAGE_META: Record<string, { title: string; color: string }> = {
todo: { title: 'To Do', color: '#6B7280' },
completed: { title: 'Erledigt', color: '#22C55E' },
today: { title: 'Heute', color: '#F59E0B' },
overdue: { title: 'Überfällig', color: '#EF4444' },
all: { title: 'Alle Aufgaben', color: '#3B82F6' },
'high-priority': { title: 'Hohe Priorität', color: '#EF4444' },
'this-week': { title: 'Diese Woche', color: '#8B5CF6' },
'no-date': { title: 'Ohne Datum', color: '#6B7280' },
};
let pagePickerEl = $state<HTMLDivElement | null>(null);
$effect(() => {
@ -144,95 +159,13 @@
pagePickerEl.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
}
});
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape' && editMode) {
editModeCtx.set(false);
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
<svelte:head>
<title>{pageTitle} - Todo</title>
</svelte:head>
<div class="board-page" class:edit-mode={editMode}>
<!-- Edit Toolbar -->
{#if editMode && activeView}
<div class="edit-toolbar">
<input
class="edit-name-input"
type="text"
value={activeView.name}
oninput={(e) => updateView({ name: e.currentTarget.value })}
placeholder="View-Name"
/>
<div class="edit-group">
<span class="edit-label">Gruppierung</span>
<div class="edit-pills">
{#each GROUPBY_OPTIONS as opt}
<button
class="edit-pill"
class:active={activeView.groupBy === opt.value}
onclick={() => updateView({ groupBy: opt.value as LocalBoardView['groupBy'] })}
>
{opt.label}
</button>
{/each}
</div>
</div>
<div class="edit-group">
<span class="edit-label">Breite</span>
<div class="edit-pills">
{#each WIDTH_OPTIONS as opt}
<button
class="edit-pill"
class:active={todoSettings.pageWidth === opt.value}
onclick={() => todoSettings.set('pageWidth', opt.value)}
>
{opt.label}
</button>
{/each}
</div>
</div>
</div>
{/if}
<!-- Minimized Page Tabs -->
{#if minimizedPages.length > 0}
<div class="minimized-tabs">
{#each minimizedPages as page (page.id)}
{@const meta = PAGE_META[page.id] ?? { title: page.id, color: '#6B7280' }}
<div
class="minimized-tab"
role="button"
tabindex="0"
onclick={() => handleRestorePage(page.id)}
>
<span class="minimized-tab-dot" style="background-color: {meta.color}"></span>
<span class="minimized-tab-title">{meta.title}</span>
<button
class="minimized-tab-close"
onclick={(e) => {
e.stopPropagation();
handleRemovePage(page.id);
}}
title="Schließen"
>
<X size={10} />
</button>
</div>
{/each}
<button class="minimized-tab-add" onclick={togglePagePicker} title="Neue Seite hinzufügen">
<Plus size={14} />
</button>
</div>
{/if}
<div class="board-page">
<!-- Board Content -->
{#if activeView}
<BoardViewRenderer
@ -243,37 +176,43 @@
onColumnMove={columnsEditable ? moveColumn : undefined}
onColumnDelete={columnsEditable ? removeColumn : undefined}
onColumnClose={handleColumnClose}
onAddColumn={columnsEditable && editMode ? addColumn : undefined}
>
{#snippet trailing()}
<!-- Secondary Pages -->
<!-- Pages -->
{#each expandedPages as page (page.id)}
<SecondaryPage
pageId={page.id}
onClose={() => handleRemovePage(page.id)}
onMinimize={() => handleMinimizePage(page.id)}
/>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="page-drag-wrapper"
class:dragging={dragPageId === page.id}
draggable="true"
ondragstart={(e) => handlePageDragStart(e, page.id)}
ondragover={handlePageDragOver}
ondrop={(e) => handlePageDrop(e, page.id)}
ondragend={handlePageDragEnd}
>
<TodoPage
pageId={page.id}
title={page.customTitle}
onClose={() => handleRemovePage(page.id)}
onMinimize={() => handleMinimizePage(page.id)}
onRename={(name) => handleRenamePage(page.id, name)}
/>
</div>
{/each}
<!-- Neue Seite button (always last in track) -->
{#if !editMode}
{#if showPagePicker}
<div bind:this={pagePickerEl}>
<PagePicker
onSelect={handleAddPage}
onClose={() => (showPagePicker = false)}
activePageIds={openPages.map((p) => p.id)}
/>
</div>
{:else}
<button
class="neue-seite-card"
onclick={togglePagePicker}
title="Neue Seite hinzufügen"
>
<Plus size={18} />
</button>
{/if}
<!-- Page picker -->
{#if showPagePicker}
<div bind:this={pagePickerEl}>
<PagePicker
onSelect={handleAddPage}
onClose={() => (showPagePicker = false)}
activePageIds={openPages.map((p) => p.id)}
/>
</div>
{:else}
<button class="neue-seite-card" onclick={togglePagePicker} title="Neue Seite hinzufügen">
<Plus size={18} />
</button>
{/if}
{/snippet}
</BoardViewRenderer>
@ -291,6 +230,14 @@
flex-direction: column;
}
.page-drag-wrapper {
flex: 0 0 auto;
transition: opacity 0.15s;
}
.page-drag-wrapper.dragging {
opacity: 0.4;
}
.neue-seite-card {
flex: 0 0 auto;
width: 48px;
@ -320,209 +267,10 @@
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 8%, transparent);
}
/* ── Minimized Tabs ──────────────────────────────────── */
.minimized-tabs {
display: flex;
justify-content: center;
gap: 0.375rem;
padding: 0.5rem 1.5rem 0.25rem;
overflow-x: auto;
scrollbar-width: none;
}
.minimized-tabs::-webkit-scrollbar {
display: none;
}
.minimized-tab {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.4rem 0.75rem 0.4rem 0.75rem;
background: #fffef5;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
flex-shrink: 0;
}
.minimized-tab:hover {
background: #fffdf0;
border-color: rgba(0, 0, 0, 0.12);
}
:global(.dark) .minimized-tab {
background: #252220;
border-color: rgba(255, 255, 255, 0.08);
}
:global(.dark) .minimized-tab:hover {
background: #2a2725;
border-color: rgba(255, 255, 255, 0.14);
}
.minimized-tab-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 9999px;
flex-shrink: 0;
}
.minimized-tab-title {
font-size: 0.75rem;
font-weight: 500;
color: #6b7280;
}
:global(.dark) .minimized-tab-title {
color: #9ca3af;
}
.minimized-tab-close {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border: none;
background: transparent;
color: #d1d5db;
border-radius: 0.125rem;
cursor: pointer;
padding: 0;
transition: all 0.15s;
}
.minimized-tab-close:hover {
color: #6b7280;
background: rgba(0, 0, 0, 0.06);
}
:global(.dark) .minimized-tab-close {
color: #4b5563;
}
:global(.dark) .minimized-tab-close:hover {
color: #9ca3af;
background: rgba(255, 255, 255, 0.08);
}
.minimized-tab-add {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 0.375rem;
border: 1px dashed rgba(0, 0, 0, 0.15);
background: transparent;
color: #9ca3af;
cursor: pointer;
flex-shrink: 0;
transition: all 0.15s;
}
.minimized-tab-add:hover {
border-color: var(--color-primary, #8b5cf6);
color: var(--color-primary, #8b5cf6);
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 4%, transparent);
}
:global(.dark) .minimized-tab-add {
border-color: rgba(255, 255, 255, 0.1);
color: #4b5563;
}
:global(.dark) .minimized-tab-add:hover {
border-color: var(--color-primary, #8b5cf6);
color: var(--color-primary, #8b5cf6);
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 8%, transparent);
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
/* ── Edit Toolbar ─────────────────────────────────────── */
.edit-toolbar {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 0.75rem 1.5rem;
background: rgba(139, 92, 246, 0.06);
border-bottom: 1px solid rgba(139, 92, 246, 0.15);
flex-shrink: 0;
overflow-x: auto;
scrollbar-width: none;
}
.edit-toolbar::-webkit-scrollbar {
display: none;
}
:global(.dark) .edit-toolbar {
background: rgba(139, 92, 246, 0.1);
border-bottom-color: rgba(139, 92, 246, 0.2);
}
.edit-name-input {
font-size: 1rem;
font-weight: 600;
color: #374151;
background: transparent;
border: none;
border-bottom: 2px solid rgba(139, 92, 246, 0.3);
padding: 0.25rem 0;
outline: none;
min-width: 120px;
max-width: 200px;
}
.edit-name-input:focus {
border-bottom-color: #8b5cf6;
}
:global(.dark) .edit-name-input {
color: #f3f4f6;
}
.edit-group {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.edit-label {
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6b7280;
white-space: nowrap;
}
:global(.dark) .edit-label {
color: #9ca3af;
}
.edit-pills {
display: flex;
gap: 0.25rem;
}
.edit-pill {
padding: 0.25rem 0.625rem;
font-size: 0.75rem;
font-weight: 500;
border-radius: 9999px;
border: 1px solid rgba(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 0.8);
color: #374151;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
:global(.dark) .edit-pill {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.1);
color: #d1d5db;
}
.edit-pill:hover {
border-color: rgba(139, 92, 246, 0.4);
}
.edit-pill.active {
background: #8b5cf6;
border-color: #8b5cf6;
color: white;
}
</style>