Merge dev into main (v0.31.0)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 28s

This commit is contained in:
sandrodach committed 2026-11-01 11:02:29 +01:00
commit f2c92c4bec
11 files changed
+175 -33

No files matched your search

+7 -1
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mindsift-frontend",
"private": true,
"version": "0.30.0",
"version": "0.31.0",
"license": "Unlicense",
"type": "module",
"scripts": {
@@ -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<string, string> = { 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<ManageColumnsModalProps> = (props) =>
props.isOpen ? <ManageColumnsModalContent {...props} /> : null;
@@ -56,6 +71,44 @@ const ManageColumnsModalContent: React.FC<ManageColumnsModalProps> = ({
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<HTMLDivElement>(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<ManageColumnsModalProps> = ({
);
};
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,12 +231,10 @@ const ManageColumnsModalContent: React.FC<ManageColumnsModalProps> = ({
{/* Columns List */}
<div className="space-y-2">
<label className="block text-xs font-semibold text-main">Aktive Spalten ({columns.length}):</label>
<div className="space-y-1.5 max-h-60 overflow-y-auto pr-1">
<div className="space-y-1.5 max-h-[45vh] overflow-y-auto pr-1" onScroll={() => setColorMenu(null)}>
{columns.map((col, idx) => (
<div
key={col.id}
className="p-2 bg-canvas border border-border rounded-card flex items-center gap-2"
>
<div key={col.id} className="p-2 bg-canvas border border-border rounded-card space-y-2">
<div className="flex items-center gap-2">
<div className="flex flex-col gap-0.5">
<button
type="button"
@@ -199,6 +256,19 @@ const ManageColumnsModalContent: React.FC<ManageColumnsModalProps> = ({
</button>
</div>
<button
type="button"
data-color-trigger
onClick={(e) => openColorMenu(col.id, e.currentTarget)}
className="w-7 h-7 flex items-center justify-center rounded-btn border border-border bg-panel hover:border-strong flex-shrink-0"
title="Farbe der Spalte"
aria-label={`Farbe der Spalte ${col.name}`}
aria-haspopup="menu"
aria-expanded={colorMenu?.id === col.id}
>
<span className={`w-3 h-3 rounded-full ${columnDotClass(col)}`} />
</button>
<input
type="text"
value={col.name}
@@ -232,6 +302,7 @@ const ManageColumnsModalContent: React.FC<ManageColumnsModalProps> = ({
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
))}
</div>
</div>
@@ -303,6 +374,42 @@ const ManageColumnsModalContent: React.FC<ManageColumnsModalProps> = ({
</button>
</div>
</div>
{colorMenu &&
(() => {
const col = columns.find((c) => c.id === colorMenu.id);
if (!col) return null;
return (
<div
ref={colorMenuRef}
role="menu"
aria-label={`Farbe der Spalte ${col.name}`}
style={{ left: colorMenu.left, top: colorMenu.top, bottom: colorMenu.bottom }}
className="fixed z-[60] w-44 p-1 bg-panel border border-border rounded-card shadow-panel"
onClick={(e) => e.stopPropagation()}
>
{COLUMN_COLORS.map((c) => {
const active = columnColorId(col) === c.id;
return (
<button
key={c.id}
type="button"
role="menuitemradio"
aria-checked={active}
onClick={() => handleColor(col.id, c.id)}
className={`w-full h-11 sm:h-8 px-2 rounded-btn text-xs flex items-center gap-2 text-left transition-colors ${
active ? 'bg-accent/10 text-main' : 'text-muted hover:text-main hover:bg-panel-hover'
}`}
>
<span className={`w-3 h-3 rounded-full flex-shrink-0 ${c.cls}`} />
<span className="flex-1">{c.label}</span>
{active && <Check className="w-3 h-3 text-accent stroke-[3]" />}
</button>
);
})}
</div>
);
})()}
</div>
);
};
@@ -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<TaskDetailModalProps & { task: Thought }>
: 'bg-canvas border-border text-muted hover:text-main hover:border-strong'
}`}
>
<span
className={`w-2 h-2 rounded-full ${
col.isDone
? 'bg-status-ok'
: col.id === 'in_progress'
? 'bg-accent'
: col.id === 'waiting'
? 'bg-status-warn'
: 'bg-faint'
}`}
/>
<span className={`w-2 h-2 rounded-full ${columnDotClass(col)}`} />
<span>{col.name}</span>
{isActive && <Check className="w-3 h-3 text-accent stroke-[3] ml-0.5" />}
</button>
@@ -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');
});
});
+11
View File
@@ -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;
+4
View File
@@ -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 */
+5
View File
@@ -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'),