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'),