From 08081e6c1d78db9c281a9b3119cdddc0682b87b6 Mon Sep 17 00:00:00 2001 From: Sandro Date: Sun, 1 Nov 2026 10:52:16 +0100 Subject: [PATCH] feat(kanban): choose a colour per column "Spalten anpassen" gets a colour dot per column that opens the choice: grey, anthracite (theme text colour), green, yellow, blue, red, purple or the accent. columnDotClass reads the column's colour (server defaults emerald/amber count as green/yellow; columns without one keep the colour of their role) and drives the Kanban header, the status dot in the list view and the task detail. Blue and purple come from new theme variables --color-info / --color-purple with fallbacks for light and dark themes. Docs and changelog updated; v0.31.0. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 +- Doku/modules/tasks.md | 2 +- README.md | 2 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- .../features/triage/ManageColumnsModal.tsx | 227 +++++++++++++----- .../src/features/triage/TaskDetailModal.tsx | 13 +- .../triage/__tests__/columnDotClass.test.ts | 18 ++ frontend/src/index.css | 11 + frontend/src/themes/theme-template.css | 4 + frontend/tailwind.config.js | 5 + 11 files changed, 219 insertions(+), 77 deletions(-) create mode 100644 frontend/src/features/triage/__tests__/columnDotClass.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 71436a6..babee67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.31.0] - 2026-11-01 + +### Added +- Kanban: Die Farbe jeder Spalte lässt sich einstellen. In „Spalten anpassen“ öffnet der Farbpunkt vor dem Namen die Auswahl: Grau, Anthrazit, Grün, Gelb, Blau, Rot, Lila oder die Akzentfarbe des Themes. Die Farbe erscheint im Spaltenkopf, als Statuspunkt in der Listenansicht und im Detail-Fenster. Blau und Lila passen sich hellen und dunklen Themes an. + ## [0.30.0] - 2026-11-01 ### Added @@ -577,7 +582,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V - Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken. - Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung. -[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.30.0...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.31.0...dev +[0.31.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.30.0...v0.31.0 [0.30.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.29.0...v0.30.0 [0.29.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.28.0...v0.29.0 [0.28.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.27.1...v0.28.0 diff --git a/Doku/modules/tasks.md b/Doku/modules/tasks.md index 25d484c..9a1e2f0 100644 --- a/Doku/modules/tasks.md +++ b/Doku/modules/tasks.md @@ -9,7 +9,7 @@ Eine Aufgabe ist ein `thought` ([database.md](../database.md)) mit `kanbanStatus ## Ansichten - **Liste** mit Filtern (offen, erledigt, Papierkorb), Gruppierung (z. B. nach Status oder Fälligkeit), Sortierung und Hashtag-Filter. -- **Kanban:** Spalten pro Liste frei konfigurierbar (Name, Farbe, „Erledigt“-Spalte). Standard: *Zu erledigen*, *In Bearbeitung*, *Wartet auf andere*, *Erledigt*. Eine Karte in eine „Erledigt“-Spalte zu ziehen setzt `isCompleted`. +- **Kanban:** Spalten pro Liste frei konfigurierbar (Name, Farbe, „Erledigt“-Spalte). Farben in „Spalten anpassen“ über den Punkt vor dem Namen: Grau, Anthrazit (Textfarbe des Themes), Grün, Gelb, Blau, Rot, Lila, Akzentfarbe (`columns[].color` = `gray`/`slate`/`green`/`yellow`/`blue`/`red`/`purple`/`accent`; ältere Werte `emerald`/`amber` gelten als Grün/Gelb, Spalten ohne Farbe behalten die ihrer Rolle). Der Punkt erscheint im Spaltenkopf, vor dem Status in der Listenansicht und im Detail-Fenster ([ManageColumnsModal.tsx](../../frontend/src/features/triage/ManageColumnsModal.tsx), `columnDotClass`). Blau und Lila kommen aus `--color-info` / `--color-purple`; Themes können sie setzen, sonst gelten Standardwerte aus `index.css`. Standard: *Zu erledigen*, *In Bearbeitung*, *Wartet auf andere*, *Erledigt*. Eine Karte in eine „Erledigt“-Spalte zu ziehen setzt `isCompleted`. - Drag & Drop sortiert in der Liste und zwischen Spalten; ein Klick öffnet das Detail-Fenster (Titel, Status, Priorität, Fälligkeit mit optionaler Uhrzeit und Schnellwahl, Liste, Hashtags, Unteraufgaben mit Fortschrittsbalken, Notizen). - Aufgaben lassen sich in eine Notiz umwandeln („Als Notiz vertiefen“). Aufgaben aus Notiz-Checkboxen (`enhanced-task`) bleiben verknüpft: Abhaken wirkt in beide Richtungen (`SyncDocBlockAndTask`), „In Notiz öffnen“ springt zum Block. diff --git a/README.md b/README.md index 45be90a..60a60c6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#) [![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-schnellstart) [![SQLite](https://img.shields.io/badge/Daten-SQLite-003b57?style=flat-square&logo=sqlite&logoColor=white)](#-deine-daten-gehören-dir) -[![Version](https://img.shields.io/badge/Version-v0.30.0-16a34a?style=flat-square)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/Version-v0.31.0-16a34a?style=flat-square)](CHANGELOG.md) **MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ab18d4b..423961c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.30.0", + "version": "0.31.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.30.0", + "version": "0.31.0", "license": "Unlicense", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index aa1b9bc..3697a20 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.30.0", + "version": "0.31.0", "license": "Unlicense", "type": "module", "scripts": { diff --git a/frontend/src/features/triage/ManageColumnsModal.tsx b/frontend/src/features/triage/ManageColumnsModal.tsx index b3035c6..b3a7459 100644 --- a/frontend/src/features/triage/ManageColumnsModal.tsx +++ b/frontend/src/features/triage/ManageColumnsModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { X, Plus, @@ -28,17 +28,32 @@ export const DEFAULT_KANBAN_COLUMNS: KanbanColumn[] = [ ]; // Wrapper and content split so the hooks always run in the same number (see TaskDetailModal) -// Dot colour of a status column (Kanban header, list view); fixed by column role for now +// Colours a column can get; all come from the theme (blue and purple via --color-info / --color-purple) +export const COLUMN_COLORS: { id: string; label: string; cls: string }[] = [ + { id: 'gray', label: 'Grau', cls: 'bg-faint' }, + { id: 'slate', label: 'Anthrazit', cls: 'bg-main' }, + { id: 'green', label: 'Grün', cls: 'bg-status-ok' }, + { id: 'yellow', label: 'Gelb', cls: 'bg-status-warn' }, + { id: 'blue', label: 'Blau', cls: 'bg-tone-blue' }, + { id: 'red', label: 'Rot', cls: 'bg-status-err' }, + { id: 'purple', label: 'Lila', cls: 'bg-tone-purple' }, + { id: 'accent', label: 'Akzentfarbe', cls: 'bg-accent' }, +]; +// Names the server's default columns use +const COLOR_ALIASES: Record = { emerald: 'green', amber: 'yellow' }; + +const columnColorId = (col: KanbanColumn): string => { + if (col.color) return COLOR_ALIASES[col.color] || col.color; + // Columns without a colour (older boards) keep the colour of their role + if (col.isDone) return 'green'; + if (col.id === 'in_progress') return 'accent'; + if (col.id === 'waiting') return 'yellow'; + return 'gray'; +}; + +// Dot colour of a status column (Kanban header, list view, task detail) export const columnDotClass = (col: KanbanColumn | undefined): string => - !col - ? 'bg-faint' - : col.isDone - ? 'bg-status-ok' - : col.id === 'in_progress' - ? 'bg-accent' - : col.id === 'waiting' - ? 'bg-status-warn' - : 'bg-faint'; + (col && COLUMN_COLORS.find((c) => c.id === columnColorId(col))?.cls) || 'bg-faint'; export const ManageColumnsModal: React.FC = (props) => props.isOpen ? : null; @@ -56,6 +71,44 @@ const ManageColumnsModalContent: React.FC = ({ const [newColName, setNewColName] = useState(''); const [newColIsDone, setNewColIsDone] = useState(false); const [saving, setSaving] = useState(false); + // Colour menu: opens at the column's dot like a context menu (fixed, so the scrolling list cannot clip it) + const [colorMenu, setColorMenu] = useState<{ id: string; left: number; top?: number; bottom?: number } | null>(null); + const colorMenuRef = useRef(null); + + useEffect(() => { + if (!colorMenu) return; + const close = (e: Event) => { + const target = e.target as HTMLElement; + if (colorMenuRef.current?.contains(target) || target.closest?.('[data-color-trigger]')) return; + setColorMenu(null); + }; + const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setColorMenu(null); + const dismiss = () => setColorMenu(null); + document.addEventListener('pointerdown', close); + document.addEventListener('keydown', onKey); + window.addEventListener('resize', dismiss); + return () => { + document.removeEventListener('pointerdown', close); + document.removeEventListener('keydown', onKey); + window.removeEventListener('resize', dismiss); + }; + }, [colorMenu]); + + const openColorMenu = (id: string, trigger: HTMLElement) => { + if (colorMenu?.id === id) { + setColorMenu(null); + return; + } + const rect = trigger.getBoundingClientRect(); + const menuHeight = COLUMN_COLORS.length * 36 + 12; + const left = Math.min(rect.left, window.innerWidth - 200); + // Below the dot if there is room, else above it + setColorMenu( + rect.bottom + menuHeight + 8 < window.innerHeight + ? { id, left, top: rect.bottom + 4 } + : { id, left, bottom: window.innerHeight - rect.top + 4 } + ); + }; useEffect(() => { setColumns(initialColumns && initialColumns.length > 0 ? initialColumns : DEFAULT_KANBAN_COLUMNS); @@ -78,6 +131,12 @@ const ManageColumnsModalContent: React.FC = ({ ); }; + const handleColor = (id: string, color: string) => { + triggerHaptic(10); + setColumns((prev) => prev.map((c) => (c.id === id ? { ...c, color } : c))); + setColorMenu(null); + }; + const handleToggleDone = (id: string) => { triggerHaptic(10); setColumns((prev) => @@ -172,65 +231,77 @@ const ManageColumnsModalContent: React.FC = ({ {/* Columns List */}
-
+
setColorMenu(null)}> {columns.map((col, idx) => ( -
-
+
+
+
+ + +
+ + + handleRename(col.id, e.target.value)} + className="flex-1 h-7 px-2 text-xs input-field" + placeholder="Spaltenname" + /> + + {/* IsDone Toggle */} + +
- - handleRename(col.id, e.target.value)} - className="flex-1 h-7 px-2 text-xs input-field" - placeholder="Spaltenname" - /> - - {/* IsDone Toggle */} - - -
))}
@@ -303,6 +374,42 @@ const ManageColumnsModalContent: React.FC = ({
+ + {colorMenu && + (() => { + const col = columns.find((c) => c.id === colorMenu.id); + if (!col) return null; + return ( +
e.stopPropagation()} + > + {COLUMN_COLORS.map((c) => { + const active = columnColorId(col) === c.id; + return ( + + ); + })} +
+ ); + })()}
); }; diff --git a/frontend/src/features/triage/TaskDetailModal.tsx b/frontend/src/features/triage/TaskDetailModal.tsx index 73ef9cd..e5a65c5 100644 --- a/frontend/src/features/triage/TaskDetailModal.tsx +++ b/frontend/src/features/triage/TaskDetailModal.tsx @@ -20,6 +20,7 @@ import { import { Thought, TaskList, TaskListMember, KanbanColumn, User, Recurrence } from '../../lib/types'; import { UserAvatar } from '../../components/UserAvatar'; import { OriginBadge, originDetails } from '../../components/OriginBadge'; +import { columnDotClass } from './ManageColumnsModal'; import { UserAutocomplete } from '../UserAutocomplete'; import { api, triggerHaptic } from '../../lib/api'; import { MarkdownViewer } from '../../lib/markdown'; @@ -409,17 +410,7 @@ const TaskDetailModalContent: React.FC : 'bg-canvas border-border text-muted hover:text-main hover:border-strong' }`} > - + {col.name} {isActive && } diff --git a/frontend/src/features/triage/__tests__/columnDotClass.test.ts b/frontend/src/features/triage/__tests__/columnDotClass.test.ts new file mode 100644 index 0000000..7684634 --- /dev/null +++ b/frontend/src/features/triage/__tests__/columnDotClass.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from 'vitest'; +import { columnDotClass } from '../ManageColumnsModal'; + +describe('columnDotClass', () => { + it('uses the chosen colour, also the names of the server defaults', () => { + expect(columnDotClass({ id: 'x', name: 'Review', color: 'purple' })).toBe('bg-tone-purple'); + expect(columnDotClass({ id: 'x', name: 'Blockiert', color: 'red' })).toBe('bg-status-err'); + expect(columnDotClass({ id: 'waiting', name: 'Wartet', color: 'amber' })).toBe('bg-status-warn'); + expect(columnDotClass({ id: 'done', name: 'Erledigt', color: 'emerald', isDone: true })).toBe('bg-status-ok'); + }); + + it('keeps the colour of the role for columns without one', () => { + expect(columnDotClass({ id: 'in_progress', name: 'In Arbeit' })).toBe('bg-accent'); + expect(columnDotClass({ id: 'x', name: 'Fertig', isDone: true })).toBe('bg-status-ok'); + expect(columnDotClass({ id: 'x', name: 'Neu' })).toBe('bg-faint'); + expect(columnDotClass(undefined)).toBe('bg-faint'); + }); +}); diff --git a/frontend/src/index.css b/frontend/src/index.css index 0cfbd23..bc3a91f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -431,6 +431,17 @@ body { --code-variable: #9cdcfe; } + /* Extra tones (Kanban column colours). Fallbacks like the code tokens: a theme's own values win. */ + :where(:root) { + --color-info: #60a5fa; + --color-purple: #a78bfa; + } + + :where(:root:not(.dark)) { + --color-info: #2563eb; + --color-purple: #7c3aed; + } + :where(:root:not(.dark)) { --code-keyword: #0000ff; --code-string: #a31515; diff --git a/frontend/src/themes/theme-template.css b/frontend/src/themes/theme-template.css index 1b2e884..b43a019 100644 --- a/frontend/src/themes/theme-template.css +++ b/frontend/src/themes/theme-template.css @@ -49,6 +49,10 @@ --bg-warning: rgba(245, 158, 11, 0.12); --border-warning: rgba(245, 158, 11, 0.38); + /* Optional: Blau und Lila für Kanban-Spalten (ohne Angabe gelten Standardwerte) */ + --color-info: #60a5fa; + --color-purple: #a78bfa; + /* --- 6. RUNDUNGEN (Geometrie) --- */ --radius-card: 0px; /* Karten, Panels, Container (0px = eckig) */ --radius-btn: 2px; /* Buttons & Badges */ diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index ca3a6bc..b722fd1 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -59,6 +59,11 @@ export default { info: v('--accent-hover'), danger: v('--color-danger'), }, + // Extra tones for Kanban columns; fallbacks in index.css, themes may set their own + tone: { + blue: v('--color-info'), + purple: v('--color-purple'), + }, border: v('--border-color'), // Aliases for names used in components (e.g. avatar initials), mapped to theme colours primary: v('--accent-primary'),