Dev #1

Merged
sandrodach merged 20 commits from dev into main 2026-10-25 01:03:04 +02:00
18 changed files with 1455 additions and 804 deletions
Showing only changes of commit b3e651b3b9 - Show all commits

No files matched your search

+133 -119
View File
@@ -9,10 +9,16 @@
- [x] **CI/CD & Multi-Stage Docker Build (`Dockerfile`, `.gitea/workflows/build-and-publish.yaml`):** - [x] **CI/CD & Multi-Stage Docker Build (`Dockerfile`, `.gitea/workflows/build-and-publish.yaml`):**
- Alle Frontend-Checks (`tsc --noEmit`, `vite build`) und Backend-Tests (`go test -v ./...`) laufen direkt im Multi-Stage Container-Build ohne doppelte Vorab-Steps. - Alle Frontend-Checks (`tsc --noEmit`, `vite build`) und Backend-Tests (`go test -v ./...`) laufen direkt im Multi-Stage Container-Build ohne doppelte Vorab-Steps.
- Push auf `dev` baut und überschreibt `git.dsit.me/sandrodach/mindsift:dev`. - Push auf `dev` baut und überschreibt `git.dsit.me/sandrodach/mindsift:dev`.
- [x] **Monochromes, eckiges Desktop-Design & Zentrale CSS-Steuerung (`frontend/src/index.css`, `frontend/tailwind.config.js`):** - [x] **Single-File CSS Theming Engine & Design-System (`frontend/src/themes/`, `theme.ts`, `tailwind.config.js`):**
- Zentrale CSS-Variablen (`--bg-app`, `--bg-panel`, `--accent-primary`, `--border-color`, `--radius-card: 0px`) in `frontend/src/index.css`. - Jedes Theme ist als **genau eine einzige, eigenständige CSS-Datei** unter `frontend/src/themes/` definiert.
- Eckige Tabs, eckige Panels/Karten (`0px` Radius), nur die Schnell-Eingabe-Blase oben im Header und Profil-Avatare sind rund (`rounded-full`). - Alle Farben, Kontraste, Oberflächen, Rahmen, Statusfarben und Rundungen sind in 6 klaren Gruppen organisiert.
- Untere doppelte Navigationsleiste entfernt; Schnell-Notiz-Blase (`InstantCaptureBar`) sitzt ganz oben im Header über den Tabs. - 4 vorkonfigurierte Themes (Dark, Light, High Contrast, Soft) plus saubere `theme-template.css`-Vorlage für eigene Themes.
- Vollständige Trennung von Struktur und Styling: React-Komponenten enthalten keine hardcodierten Farbwerte mehr, sondern ausschließlich semantische Klassen und CSS-Variablen (`text-main`, `text-muted`, `bg-canvas`, `panel-surface`, `checkbox-square`, etc.).
- Schnellauswahl im Profil-Dropdown im Header sowie visueller Theme-Manager in den Benutzereinstellungen.
- [x] **Monochromes, eckiges Desktop-Design & Zentrale CSS-Steuerung:**
- Zentrale CSS-Variablen (`--bg-canvas`, `--bg-panel`, `--accent`, `--border-color`, `--radius-card`, `--radius-btn`, `--radius-input`) in den Theme-Dateien.
- Eckige Tabs, eckige Panels/Karten (`0px` Radius im Dark/Monochrome-Theme), nur die Schnell-Eingabe-Blase (`InstantCaptureBar`) oben im Header nutzt `--radius-pill`.
- Untere doppelte Navigationsleiste entfernt; Schnell-Notiz-Blase sitzt ganz oben im Header über den Tabs.
- Workspace links als eckiger, monochromer Datei-/Ordner-Baum (File Explorer Tree mit Unterordnern). - Workspace links als eckiger, monochromer Datei-/Ordner-Baum (File Explorer Tree mit Unterordnern).
- [x] **Quick Dump Notes + Triage Swipen + Direkt-Zuordnung (`InstantCaptureBar.tsx`, `TriageDeck.tsx`, `server.go`):** - [x] **Quick Dump Notes + Triage Swipen + Direkt-Zuordnung (`InstantCaptureBar.tsx`, `TriageDeck.tsx`, `server.go`):**
- **Quick Dump (Standard):** Text oben eingeben + `Enter` wirft den Gedanken sofort als Quick Dump (`status: 'inbox'`) ins Triage-Deck. - **Quick Dump (Standard):** Text oben eingeben + `Enter` wirft den Gedanken sofort als Quick Dump (`status: 'inbox'`) ins Triage-Deck.
@@ -28,7 +34,7 @@
- Öffentliche Selbstregistrierung ist standardmäßig deaktiviert (`allowRegistration: false`). Neue Benutzer können nur über einen **Einladungscode** (`MS-XXXXXXXX`, Tabelle `user_invites`) oder direkt durch einen Admin (`admin` / `team_admin`) angelegt werden. - Öffentliche Selbstregistrierung ist standardmäßig deaktiviert (`allowRegistration: false`). Neue Benutzer können nur über einen **Einladungscode** (`MS-XXXXXXXX`, Tabelle `user_invites`) oder direkt durch einen Admin (`admin` / `team_admin`) angelegt werden.
- [x] **Profile-Dropdown & Vollbild-Einstellungsseiten statt kleinem Modal (`App.tsx`, `UserSettingsView.tsx`, `AdminSettingsView.tsx`):** - [x] **Profile-Dropdown & Vollbild-Einstellungsseiten statt kleinem Modal (`App.tsx`, `UserSettingsView.tsx`, `AdminSettingsView.tsx`):**
- Klick auf den Benutzer oben rechts öffnet ein eckiges, monochromes Dropdown-Menü: - Klick auf den Benutzer oben rechts öffnet ein eckiges, monochromes Dropdown-Menü:
- **Kontoeinstellungen** (`UserSettingsView.tsx` — Vollbild-Ansicht für Profil & Passwort, Multi-Channel-Alarme, Private Webhooks & Daten-Export). - **Kontoeinstellungen** (`UserSettingsView.tsx` — Vollbild-Ansicht für Profil & Passwort, Darstellung & Themes, Multi-Channel-Alarme, Private Webhooks & Daten-Export).
- **Administration** (`AdminSettingsView.tsx` — nur sichtbar für `admin` und `team_admin`; Vollbild-Ansicht für Benutzer, Rollen, Einladungscodes, Organisationen & Teams, Server-Isolation und SMTP-Mailserver). - **Administration** (`AdminSettingsView.tsx` — nur sichtbar für `admin` und `team_admin`; Vollbild-Ansicht für Benutzer, Rollen, Einladungscodes, Organisationen & Teams, Server-Isolation und SMTP-Mailserver).
- **Abmelden** (bereinigt JWT-Token und führt zurück zum Login-Bildschirm). - **Abmelden** (bereinigt JWT-Token und führt zurück zum Login-Bildschirm).
- 3-stufiges Rollensystem: `admin` (Server-Admin), `team_admin` (Team-Admin mit Berechtigung zum Einladen und Verwalten von Teams) und `member` (Mitglied). - 3-stufiges Rollensystem: `admin` (Server-Admin), `team_admin` (Team-Admin mit Berechtigung zum Einladen und Verwalten von Teams) und `member` (Mitglied).
@@ -50,43 +56,85 @@
--- ---
## 🎨 1. Design-System & CSS-Architektur (`frontend/src/index.css`) ## 🎨 1. Single-File CSS Theming Engine & Design-System
Alle Farben, Abstände und Radien werden zentral in [`frontend/src/index.css`](file:///home/sandro/Dokumente/git/MindSift/frontend/src/index.css) über CSS-Variablen gesteuert: ### Philosophie: 1 Datei = 1 komplettes Theme
Anstatt Styling in Dutzenden Tailwind-Klassen über hunderte Komponenten zu verstreuen, steuert **eine einzige CSS-Datei** das komplette Erscheinungsbild der Anwendung.
Jedes Theme liegt unter `frontend/src/themes/` und gruppiert **6 standardisierte Bereiche**:
```css ```css
:root { :root[data-theme="dein-theme"] {
/* Monochromer Hintergrund (Dunkles Schiefer/Anthrazit) */ /* ==========================================================================
--bg-app: #121316; 1. AKZENTFARBE (Fokus, Primäraktionen, Highlights)
--bg-panel: #1a1c20; ========================================================================== */
--bg-panel-elevated: #22252a; --accent: #2563eb;
--bg-panel-hover: #282c33;
/* Einzige Akzentfarbe + hellere/gedimmte Variante */
--accent-primary: #2563eb;
--accent-hover: #3b82f6; --accent-hover: #3b82f6;
--accent-dim: rgba(37, 99, 235, 0.14); --accent-dim: rgba(37, 99, 235, 0.15);
--accent-border: rgba(59, 130, 246, 0.45); --accent-contrast: #ffffff;
/* Kanten & Linien */ /* ==========================================================================
--border-color: rgba(255, 255, 255, 0.09); 2. OBERFLÄCHEN & HINTERGRÜNDE
--border-subtle: rgba(255, 255, 255, 0.05); ========================================================================== */
--bg-canvas: #0e1013;
--bg-panel: #16181d;
--bg-panel-elevated: #1f2229;
--bg-input: #121418;
--bg-code: #0b0c0f;
--bg-overlay: rgba(0, 0, 0, 0.75);
/* Eckiges Layout (0px für Karten/Panels, minimal für Inputs) */ /* ==========================================================================
3. TEXT & KONTRASTE
========================================================================== */
--text-main: #f4f4f5;
--text-muted: #a1a1aa;
--text-faint: #71717a;
--text-on-accent: #ffffff;
/* ==========================================================================
4. RAHMEN, LINIEN & SCHATTEN
========================================================================== */
--border-color: rgba(255, 255, 255, 0.08);
--border-strong: rgba(255, 255, 255, 0.18);
--shadow-panel: 0 4px 20px -2px rgba(0, 0, 0, 0.45);
/* ==========================================================================
5. STATUS- & SIGNALFARBEN
========================================================================== */
--status-ok: #22c55e;
--status-err: #ef4444;
--status-warn: #eab308;
--status-info: #3b82f6;
/* ==========================================================================
6. RUNDUNGEN / GEOMETRIE
========================================================================== */
--radius-card: 0px; --radius-card: 0px;
--radius-btn: 2px; --radius-btn: 2px;
--radius-input: 2px; --radius-input: 2px;
--radius-checkbox: 2px;
--radius-pill: 9999px;
} }
``` ```
### Zentrale Komponenten-Klassen ### Verfügbare Themes
- `.panel-surface` / `.panel-elevated`: Eckige Container mit `1px solid var(--border-color)`.
- `.btn-action`: Monochromer, kompakter Button (`border-radius: var(--radius-btn)`). | Theme-ID | Datei | Charakter | Standard-Geometrie |
- `.btn-primary`: Akzent-Button auf `--accent-primary`. | :--- | :--- | :--- | :--- |
- `.input-field`: Minimal gerundetes (`2px`) Eingabefeld für Eigenschaften/Optionen. | `dark` | [`theme-dark.css`](file:///home/sandro/Dokumente/git/MindSift/frontend/src/themes/theme-dark.css) | Standard Monochrom / Dark Slate | Streng eckig (`0px` Cards, `2px` Buttons) |
- `.tab-btn` / `.tab-btn-active`: Eckige, monochrome Header-Tabs. | `light` | [`theme-light.css`](file:///home/sandro/Dokumente/git/MindSift/frontend/src/themes/theme-light.css) | Daylight Clean White / Minimalist Light | Modern sauber (`4px` Cards, `4px` Buttons) |
- `.badge-accent`: Kompaktes Akzent-Badge für Zähler und Tags. | `high-contrast` | [`theme-high-contrast.css`](file:///home/sandro/Dokumente/git/MindSift/frontend/src/themes/theme-high-contrast.css) | Reines Schwarz/Weiß mit maximaler Lesbarkeit (AAA Kontrast) | Streng eckig (`0px`, `2px` Rahmen) |
- **Ausnahme für Rundungen (`rounded-full`):** Nur die Schnell-Eingabe-Blase (`InstantCaptureBar`) im Header, der Profil-Avatar und Toggle-Switches sind rund. | `soft` | [`theme-soft.css`](file:///home/sandro/Dokumente/git/MindSift/frontend/src/themes/theme-soft.css) | Soft Rounded Slate mit warmem Indigo-Akzent | Weich gerundet (`12px` Cards, `8px` Buttons) |
| *Template* | [`theme-template.css`](file:///home/sandro/Dokumente/git/MindSift/frontend/src/themes/theme-template.css) | Dokumentierte Vorlage zur Erstellung eigener Themes | Voll anpassbar |
### Semantische Komponenten-Klassen (`frontend/src/index.css`)
- `.panel-surface` / `.panel-elevated`: Automatisch über `--radius-card`, `--bg-panel` und `--border-color` gesteuert.
- `.btn-primary` / `.btn-action`: Aktionen und Buttons mit `--radius-btn`, `--accent` und `--text-on-accent`.
- `.input-field`: Formularfelder mit `--radius-input`, `--bg-input`, `--border-color` und `--text-main`.
- `.checkbox-square`: Kontrollkästchen mit `--radius-checkbox` und `--accent`.
- `.badge-accent`, `.badge-neutral`, `.badge-ok`, `.badge-err`: Pillen und Zähler.
- `.alert-ok`, `.alert-err`: Hinweiskästen mit Signalfarben.
- `.capture-pill`: Die markante Schnell-Eingabeleiste im Header mit `--radius-pill`.
--- ---
@@ -98,105 +146,71 @@ MindSift speichert alle Daten lokal und hochperformant in SQLite (`/data/mindsif
1. **`users`** 1. **`users`**
- `id TEXT PRIMARY KEY`, `email TEXT UNIQUE`, `password_hash TEXT`, `display_name TEXT` - `id TEXT PRIMARY KEY`, `email TEXT UNIQUE`, `password_hash TEXT`, `display_name TEXT`
- `role TEXT DEFAULT 'member'` (`'admin'` | `'member'`) - `role TEXT DEFAULT 'member'` (`admin`, `team_admin`, `member`)
- `is_active INTEGER DEFAULT 1` - `is_active INTEGER DEFAULT 1`, `created_at DATETIME`, `updated_at DATETIME`
- `discord_webhook_url TEXT DEFAULT ''` 2. **`system_settings`** (Instanzweite globale Konfiguration, 1 Row mit `id = 'singleton'`)
- `notify_before_minutes INTEGER DEFAULT 15` - `sharing_mode TEXT DEFAULT 'teams_only'` (`strict_private`, `teams_only`, `open`)
- `created_at DATETIME`, `updated_at DATETIME` - `allow_registration INTEGER DEFAULT 0` (Öffentliche Registrierung erlaubt oder gesperrt)
- `instance_name TEXT DEFAULT 'MindSift'`
2. **`thoughts` (Quick Dumps, Triage & Aufgaben)** - `smtp_host TEXT`, `smtp_port INTEGER`, `smtp_user TEXT`, `smtp_pass TEXT`, `smtp_from TEXT`, `smtp_tls INTEGER`
- `id TEXT PRIMARY KEY`, `user_id TEXT`, `content TEXT`, `source TEXT`, `priority TEXT` 3. **`user_invites`** (Einladungscodes für neue Registrierungen)
- `status TEXT` (`'inbox'` | `'converted'` | `'discarded'`) - `id TEXT PRIMARY KEY`, `code TEXT UNIQUE` (`MS-XXXXXXXX`), `email TEXT` (optional gebunden)
- `converted_type TEXT` (`'task'` | `'document'` | `'list_item'`) - `role TEXT DEFAULT 'member'`, `team_id TEXT` (optional direkte Team-Zuweisung)
- `converted_ref_id TEXT` (Referenz auf Document oder ListItem) - `created_by TEXT`, `used_by TEXT`, `expires_at DATETIME`, `created_at DATETIME`
- `task_list_id TEXT` (FK auf `task_lists.id`) 4. **`organizations` & `organization_teams` & `team_members`**
- `kanban_status TEXT DEFAULT 'todo'` (`'todo'` | `'in_progress'` | `'done'`) - Strukturierung von Benutzern in Abteilungen, Familien oder Projekt-Teams.
- `notes TEXT DEFAULT ''` (zusätzliche Aufgabenbeschreibung/Notizen) - Bildet die Basis für den Sichtbarkeits-Modus `teams_only`.
- `due_date DATETIME` (Fälligkeitsdatum) 5. **`spaces` & `space_members`**
- `alarm_sent INTEGER DEFAULT 0` (Flag für Discord-Benachrichtigung) - Geteilte Kollaborations-Räume mit Einladungscode (`invite_code`), Echtzeit-WebSocket Sync und Rollen (`owner`, `editor`, `viewer`).
- `created_at DATETIME`, `updated_at DATETIME` 6. **`shared_lists` & `list_items`**
- Geteilte Einkaufs-, Pack- und Inventarlisten mit Kategorien, Mengen und Echtzeit-Abhaken.
3. **`task_lists` (Mehrere Aufgabenlisten)** 7. **`tasks` & `task_lists`**
- `id TEXT PRIMARY KEY`, `user_id TEXT`, `space_id TEXT` (optional für Team-Sharing) - Persönliche und geteilte Aufgabenlisten mit Kanban-Spalten (`todo`, `in_progress`, `done`), Fälligkeit, Priorität und Alarm-Optionen.
- `name TEXT NOT NULL`, `color TEXT DEFAULT '#2563eb'`, `icon TEXT DEFAULT 'list'` 8. **`quick_dumps` (Triage-Deck)**
- `is_default INTEGER DEFAULT 0`, `created_at DATETIME`, `updated_at DATETIME` - Schnelle Einfälle (`status = 'inbox'`, `'discarded'`, `'converted'`), Fälligkeitsdatum und Text-Notiz.
9. **`documents` (Workspace Markdown-Wiki)**
4. **`organizations` & `teams` & `team_members`** - Notizen und Dokumentationen mit Folder-Pfad (`folder_path`), Tags, Pinned-Status, JSON-Blocks und physischer `.md`-Spiegelung auf Disk.
- `organizations`: `id TEXT PRIMARY KEY`, `name TEXT`, `owner_id TEXT`, `created_at DATETIME`, `updated_at DATETIME` 10. **`user_alarms` & `user_webhooks`**
- `teams`: `id TEXT PRIMARY KEY`, `organization_id TEXT`, `name TEXT`, `created_at DATETIME` - Multi-Channel Alarme (Mail, Discord, Webhook) und ausgehende Event-Webhooks.
- `team_members`: `team_id TEXT`, `user_id TEXT`, `role TEXT DEFAULT 'member'`, `joined_at DATETIME`, `PRIMARY KEY (team_id, user_id)`
5. **`system_settings` (Admin-Konfiguration)**
- `key TEXT PRIMARY KEY`, `value TEXT`, `updated_at DATETIME`
- Wichtige Schlüssel:
- `sharing_isolation_mode`: `'open'` | `'teams_only'` | `'strict_private'`
- `allow_public_registration`: `'true'` | `'false'`
6. **`documents` & `document_blocks` (Workspace & Disk Sync)**
- `documents`: `id TEXT PRIMARY KEY`, `user_id TEXT`, `space_id TEXT`, `title TEXT`, `folder_path TEXT DEFAULT '/'`, `is_archived INTEGER`, `created_at DATETIME`, `updated_at DATETIME`
- `document_blocks`: `id TEXT PRIMARY KEY`, `document_id TEXT`, `block_type TEXT` (`paragraph`, `heading1`, `heading2`, `heading3`, `todo`, `bullet`, `callout`, `code`, `table`, `progress`), `content TEXT`, `order_index INTEGER`, `metadata TEXT`, `updated_at DATETIME`
7. **`spaces`, `space_members`, `shared_lists`, `list_items`, `api_keys`**
- Verwaltung geteilter Einkaufs-/Checklisten und privater Ingestion-Keys.
--- ---
## ⚙️ 3. State-Machine: Quick Dump, Direkt-Zuordnung & Triage ## ⚡ 3. Triage & Quick-Dump State Machine
``` ```mermaid
[ HEADER QUICK-CAPTURE BAR ] stateDiagram-v2
│ [*] --> InstantCapture: Gedanke eingetippt
┌───────────────────────┼───────────────────────┐ InstantCapture --> QuickDump_Inbox: Enter (Standard)
│ Standard (Enter) │ Direkt-Option gewählt │ InstantCapture --> TaskCreated: Ziel = 'task'
▼ ▼ ▼ InstantCapture --> ListItemCreated: Ziel = 'list_item'
status: 'inbox' status: 'converted' status: 'converted' InstantCapture --> DocCreated: Ziel = 'document'
(Quick Dump Deck) ├── target: 'task' ├── target: 'list_item'
│ └── target: 'document' └── Broadcast via WS
│
├──► Swipe Links / "Wegwerfen" (H / ←) ──► status: 'discarded'
├──► "Später" (Leertaste) ──► ans Ende der lokalen Queue
├──► Swipe Rechts / "Als Aufgabe" (L/→)──► status: 'converted' (type: 'task')
├──► "Mit Termin" (T) ──► status: 'converted' (type: 'task', dueDate)
├──► "Als Notiz" (D) ──► status: 'converted' (type: 'document' + Doc angelegt)
└──► "Auf Liste" ──► status: 'converted' (type: 'list_item' + Item angelegt)
```
### API-Endpunkt für Capture (`POST /api/v1/inbox`) QuickDump_Inbox --> TriageDeck: Stapel sichten
Akzeptiert: TriageDeck --> Discarded: Swipe links / H (Wegwerfen)
- `content`: Fließtext des Gedankens TriageDeck --> TaskCreated: Swipe rechts / L (Als Aufgabe)
- `priority`: `'low'` | `'medium'` | `'high'` TriageDeck --> TaskCreated: T (Termin-Aufgabe)
- `target`: `'inbox'` (Standard) | `'task'` | `'list_item'` | `'document'` TriageDeck --> DocCreated: D (In Notiz umwandeln)
- `taskListId`: ID der Aufgabenliste (wenn `target === 'task'`) TriageDeck --> ListItemCreated: Auf Liste schieben
- `targetListId`: ID der Zielliste (wenn `target === 'list_item'`) TriageDeck --> QuickDump_Inbox: Leertaste (Später - ans Ende)
- `dueDate`: Optionales ISO-Datum
Discarded --> [*]
TaskCreated --> [*]
DocCreated --> [*]
ListItemCreated --> [*]
```
--- ---
## 🔔 4. Discord-Alarm-Scheduler & Webhook-Architektur ## 👥 4. Server-Administration & Isolation Modes
1. **Hintergrund-Ticker im Go-Backend (`internal/api/server.go`):** Der Server-Administrator (`role = 'admin'`) legt unter `AdminSettingsView.tsx` global fest, wie Benutzer miteinander interagieren dürfen:
- Ein Hintergrund-Goroutine-Ticker läuft alle 60 Sekunden.
- Fragt `GetDueTasksForAlarm()` ab: Sucht nach offenen Aufgaben (`status = 'converted'`, `converted_type = 'task'`, `kanban_status != 'done'`), deren `due_date` in der Vorwarnzeit liegt (`due_date <= now + notifyBeforeMinutes`) und bei denen `alarm_sent == 0` ist.
- Sendet ein JSON-Embed an die in den Benutzer-Einstellungen hinterlegte `discord_webhook_url`:
```json
{
"username": "MindSift Reminder",
"embeds": [{
"title": "⏰ Aufgabe fällig: <Titel>",
"description": "<Notizen>",
"color": 2450411,
"fields": [
{"name": "Fällig am", "value": "<Datum>", "inline": true},
{"name": "Priorität", "value": "<Prio>", "inline": true}
]
}]
}
```
- Markiert den Gedanken in SQLite mit `MarkTaskAlarmSent(taskID)` als benachrichtigt.
2. **Eingehende Webhooks / Private API-Tokens:** 1. **`strict_private` (Strikt Privat):**
- Erreichbar unter `POST /api/v1/inbox` mit Header `X-API-Key: ms_...`. - Jeder Benutzer arbeitet komplett autark. Keine teamübergreifende Benutzersuche, keine geteilten Spaces.
- Ermöglicht direkte Einbindung in Shell-Skripte, Crons oder Drittsysteme via `curl`. 2. **`teams_only` (Teams isoliert — Empfohlener Standard):**
- Benutzer sehen im Adressbuch und bei geteilten Spaces ausschließlich Personen, mit denen sie mindestens in einem gemeinsamen Team sind.
3. **`open` (Offene Instanz):**
- Alle Benutzer können sich gegenseitig sehen, zu Spaces einladen und gemeinsam an Listen arbeiten.
--- ---
+67 -23
View File
@@ -10,9 +10,16 @@ import {
LogOut, LogOut,
ChevronDown, ChevronDown,
RotateCcw, RotateCcw,
Palette,
} from 'lucide-react'; } from 'lucide-react';
import { AuthStatus, DashboardStats, SharedList, Thought, User } from './lib/types'; import { AuthStatus, DashboardStats, SharedList, Thought, User } from './lib/types';
import { api, clearToken, getToken, triggerHaptic } from './lib/api'; import { api, clearToken, getToken, triggerHaptic } from './lib/api';
import {
AVAILABLE_THEMES,
initThemeOnBoot,
setActiveTheme as applyTheme,
ThemeId,
} from './lib/theme';
import { InstantCaptureBar } from './features/capture/InstantCaptureBar'; import { InstantCaptureBar } from './features/capture/InstantCaptureBar';
import { TriageDeck } from './features/triage/TriageDeck'; import { TriageDeck } from './features/triage/TriageDeck';
import { TasksView } from './features/triage/TasksView'; import { TasksView } from './features/triage/TasksView';
@@ -51,9 +58,9 @@ class ViewErrorBoundary extends React.Component<
render() { render() {
if (this.state.hasError) { if (this.state.hasError) {
return ( return (
<div className="p-6 bg-panel border border-border text-xs space-y-3 max-w-lg mx-auto my-8"> <div className="p-6 panel-surface text-xs space-y-3 max-w-lg mx-auto my-8">
<h3 className="text-sm font-bold text-white">Ansicht konnte nicht geladen werden</h3> <h3 className="text-sm font-bold text-main">Ansicht konnte nicht geladen werden</h3>
<p className="text-zinc-400">{this.state.errorText}</p> <p className="text-muted">{this.state.errorText}</p>
<button <button
onClick={() => { onClick={() => {
this.setState({ hasError: false, errorText: '' }); this.setState({ hasError: false, errorText: '' });
@@ -72,6 +79,7 @@ class ViewErrorBoundary extends React.Component<
} }
export const App: React.FC = () => { export const App: React.FC = () => {
const [activeTheme, setActiveThemeState] = useState<ThemeId>(() => initThemeOnBoot());
const [bootLoading, setBootLoading] = useState(true); const [bootLoading, setBootLoading] = useState(true);
const [authStatus, setAuthStatus] = useState<AuthStatus>({ const [authStatus, setAuthStatus] = useState<AuthStatus>({
needsSetup: false, needsSetup: false,
@@ -98,6 +106,12 @@ export const App: React.FC = () => {
const [showProfileMenu, setShowProfileMenu] = useState(false); const [showProfileMenu, setShowProfileMenu] = useState(false);
const dropdownRef = useRef<HTMLDivElement | null>(null); const dropdownRef = useRef<HTMLDivElement | null>(null);
const handleChangeTheme = (themeId: ThemeId) => {
triggerHaptic(10);
applyTheme(themeId);
setActiveThemeState(themeId);
};
const refreshAll = async () => { const refreshAll = async () => {
try { try {
const [meRes, deckRes, listsRes] = await Promise.all([ const [meRes, deckRes, listsRes] = await Promise.all([
@@ -228,7 +242,7 @@ export const App: React.FC = () => {
if (bootLoading) { if (bootLoading) {
return ( return (
<div className="min-h-screen bg-canvas text-zinc-400 flex items-center justify-center text-xs font-sans"> <div className="min-h-screen bg-canvas text-muted flex items-center justify-center text-xs font-sans">
Verbinde mit MindSift… Verbinde mit MindSift…
</div> </div>
); );
@@ -253,9 +267,9 @@ export const App: React.FC = () => {
: 'Mitglied'; : 'Mitglied';
return ( return (
<div className="min-h-screen bg-canvas text-white flex flex-col font-sans selection:bg-accent/30"> <div className="min-h-screen bg-canvas text-main flex flex-col font-sans selection:bg-accent/30">
{/* Top Header Bar */} {/* Top Header Bar */}
<header className="sticky top-0 z-30 bg-panel/95 backdrop-blur-md border-b border-border shadow-sm"> <header className="sticky top-0 z-30 bg-panel border-b border-border shadow-sm">
<div className="max-w-6xl mx-auto px-3 sm:px-4 py-2 flex items-center justify-between gap-3 sm:gap-6"> <div className="max-w-6xl mx-auto px-3 sm:px-4 py-2 flex items-center justify-between gap-3 sm:gap-6">
{/* Logo & Headline */} {/* Logo & Headline */}
<div <div
@@ -265,14 +279,14 @@ export const App: React.FC = () => {
}} }}
className="flex items-center gap-2.5 cursor-pointer group select-none flex-shrink-0" className="flex items-center gap-2.5 cursor-pointer group select-none flex-shrink-0"
> >
<div className="w-7 h-7 bg-accent text-white flex items-center justify-center"> <div className="w-7 h-7 bg-accent text-on-accent rounded-btn flex items-center justify-center">
<Brain className="w-4 h-4" /> <Brain className="w-4 h-4" />
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-bold text-sm sm:text-base text-white tracking-tight leading-none"> <span className="font-bold text-sm sm:text-base text-main tracking-tight leading-none">
{authStatus.instanceName || 'MindSift'} {authStatus.instanceName || 'MindSift'}
</span> </span>
<span className="text-[11px] text-zinc-400 mt-0.5 hidden sm:inline-block"> <span className="text-[11px] text-muted mt-0.5 hidden sm:inline-block">
Der Kopf frei in drei Sekunden Der Kopf frei in drei Sekunden
</span> </span>
</div> </div>
@@ -294,29 +308,57 @@ export const App: React.FC = () => {
triggerHaptic(10); triggerHaptic(10);
setShowProfileMenu((prev) => !prev); setShowProfileMenu((prev) => !prev);
}} }}
className="h-8 pl-1 pr-2.5 rounded-full bg-panel hover:bg-panel-elevated border border-border text-xs font-medium text-white flex items-center gap-2 transition-colors" className="h-8 pl-1 pr-2.5 capture-pill bg-panel hover:bg-panel-elevated border border-border text-xs font-medium text-main flex items-center gap-2 transition-colors"
title="Benutzermenü öffnen" title="Benutzermenü öffnen"
> >
<div className="w-6 h-6 rounded-full bg-accent text-white text-[10px] font-bold flex items-center justify-center"> <div className="w-6 h-6 capture-pill bg-accent text-on-accent text-[10px] font-bold flex items-center justify-center">
{user.displayName ? user.displayName.slice(0, 1).toUpperCase() : 'U'} {user.displayName ? user.displayName.slice(0, 1).toUpperCase() : 'U'}
</div> </div>
<span className="max-w-[120px] truncate hidden sm:inline"> <span className="max-w-[120px] truncate hidden sm:inline">
{user.displayName} {user.displayName}
</span> </span>
<ChevronDown className="w-3.5 h-3.5 text-zinc-400" /> <ChevronDown className="w-3.5 h-3.5 text-muted" />
</button> </button>
{showProfileMenu && ( {showProfileMenu && (
<div className="absolute right-0 top-full mt-1.5 w-60 bg-panel border border-border shadow-2xl z-50 divide-y divide-border text-xs"> <div className="absolute right-0 top-full mt-1.5 w-64 panel-surface shadow-panel z-50 divide-y divide-border text-xs">
{/* User Info Header */} {/* User Info Header */}
<div className="p-3 bg-canvas"> <div className="p-3 bg-canvas">
<p className="font-bold text-white truncate">{user.displayName}</p> <p className="font-bold text-main truncate">{user.displayName}</p>
<p className="text-[11px] text-zinc-400 truncate">{user.email}</p> <p className="text-[11px] text-muted truncate">{user.email}</p>
<span className="inline-block mt-1.5 px-1.5 py-0.5 bg-panel border border-border text-[10px] text-accent font-semibold"> <span className="inline-block mt-1.5 px-1.5 py-0.5 bg-panel border border-border rounded-btn text-[10px] text-accent font-semibold">
{roleLabel} {roleLabel}
</span> </span>
</div> </div>
{/* Theme Quick Switcher */}
<div className="p-2.5 space-y-1.5">
<span className="text-[10px] font-semibold text-muted uppercase tracking-wider flex items-center gap-1.5 px-1">
<Palette className="w-3 h-3 text-accent" />
<span>Design-Theme</span>
</span>
<div className="grid grid-cols-2 gap-1">
{AVAILABLE_THEMES.map((t) => (
<button
key={t.id}
type="button"
onClick={() => handleChangeTheme(t.id)}
className={`h-7 px-2 text-[11px] font-medium border rounded-btn flex items-center gap-1.5 transition-colors ${
activeTheme === t.id
? 'bg-panel-elevated border-accent text-main font-semibold'
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
<span
className="w-2.5 h-2.5 border border-strong flex-shrink-0"
style={{ backgroundColor: t.previewBg }}
/>
<span className="truncate">{t.name.replace('MindSift ', '').split(' ')[0]}</span>
</button>
))}
</div>
</div>
{/* Menu Items */} {/* Menu Items */}
<div className="py-1"> <div className="py-1">
<button <button
@@ -326,7 +368,7 @@ export const App: React.FC = () => {
setActiveTab('user_settings'); setActiveTab('user_settings');
}} }}
className={`w-full px-3.5 py-2.5 text-left flex items-center gap-2.5 hover:bg-panel-elevated transition-colors ${ className={`w-full px-3.5 py-2.5 text-left flex items-center gap-2.5 hover:bg-panel-elevated transition-colors ${
activeTab === 'user_settings' ? 'text-accent font-semibold' : 'text-zinc-200' activeTab === 'user_settings' ? 'text-accent font-semibold' : 'text-main'
}`} }`}
> >
<Settings className="w-4 h-4 text-accent" /> <Settings className="w-4 h-4 text-accent" />
@@ -343,7 +385,7 @@ export const App: React.FC = () => {
className={`w-full px-3.5 py-2.5 text-left flex items-center gap-2.5 hover:bg-panel-elevated transition-colors ${ className={`w-full px-3.5 py-2.5 text-left flex items-center gap-2.5 hover:bg-panel-elevated transition-colors ${
activeTab === 'admin_settings' activeTab === 'admin_settings'
? 'text-accent font-semibold' ? 'text-accent font-semibold'
: 'text-zinc-200' : 'text-main'
}`} }`}
> >
<Shield className="w-4 h-4 text-accent" /> <Shield className="w-4 h-4 text-accent" />
@@ -356,7 +398,7 @@ export const App: React.FC = () => {
<div className="py-1"> <div className="py-1">
<button <button
onClick={handleLogout} onClick={handleLogout}
className="w-full px-3.5 py-2.5 text-left flex items-center gap-2.5 text-red-400 hover:bg-red-950/30 transition-colors" className="w-full px-3.5 py-2.5 text-left flex items-center gap-2.5 text-status-err hover:bg-status-err-bg transition-colors"
> >
<LogOut className="w-4 h-4" /> <LogOut className="w-4 h-4" />
<span>Abmelden</span> <span>Abmelden</span>
@@ -367,7 +409,7 @@ export const App: React.FC = () => {
</div> </div>
</div> </div>
{/* Monochrome square tabs attached to header */} {/* Navigation tabs attached to header */}
<nav className="max-w-6xl mx-auto px-1 sm:px-2 overflow-x-auto no-scrollbar border-t border-border"> <nav className="max-w-6xl mx-auto px-1 sm:px-2 overflow-x-auto no-scrollbar border-t border-border">
<div className="flex items-stretch"> <div className="flex items-stretch">
<button <button
@@ -384,7 +426,7 @@ export const App: React.FC = () => {
className={ className={
activeTab === 'triage' activeTab === 'triage'
? 'badge-accent' ? 'badge-accent'
: 'px-1.5 py-0.5 text-[10px] bg-canvas text-zinc-400 border border-border' : 'badge-neutral'
} }
> >
{stats.inboxCount} {stats.inboxCount}
@@ -406,7 +448,7 @@ export const App: React.FC = () => {
className={ className={
activeTab === 'tasks' activeTab === 'tasks'
? 'badge-accent' ? 'badge-accent'
: 'px-1.5 py-0.5 text-[10px] bg-canvas text-zinc-400 border border-border' : 'badge-neutral'
} }
> >
{stats.tasksOpenCount} {stats.tasksOpenCount}
@@ -427,7 +469,7 @@ export const App: React.FC = () => {
className={ className={
activeTab === 'workspace' activeTab === 'workspace'
? 'badge-accent' ? 'badge-accent'
: 'px-1.5 py-0.5 text-[10px] bg-canvas text-zinc-400 border border-border' : 'badge-neutral'
} }
> >
{stats.docsCount} {stats.docsCount}
@@ -484,6 +526,8 @@ export const App: React.FC = () => {
{activeTab === 'user_settings' && ( {activeTab === 'user_settings' && (
<UserSettingsView <UserSettingsView
currentUser={user} currentUser={user}
activeTheme={activeTheme}
onThemeChange={handleChangeTheme}
onUserUpdated={(u) => { onUserUpdated={(u) => {
setUser(u); setUser(u);
refreshAll(); refreshAll();
+72 -69
View File
@@ -28,7 +28,9 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
const [setupEmail, setSetupEmail] = useState(''); const [setupEmail, setSetupEmail] = useState('');
const [setupPassword, setSetupPassword] = useState(''); const [setupPassword, setSetupPassword] = useState('');
const [instanceName, setInstanceName] = useState(authStatus.instanceName || 'MindSift'); const [instanceName, setInstanceName] = useState(authStatus.instanceName || 'MindSift');
const [sharingMode, setSharingMode] = useState<'open' | 'teams_only' | 'strict_private'>('teams_only'); const [sharingMode, setSharingMode] = useState<'open' | 'teams_only' | 'strict_private'>(
'teams_only'
);
const [allowRegistration, setAllowRegistration] = useState(false); const [allowRegistration, setAllowRegistration] = useState(false);
const [createSampleData, setCreateSampleData] = useState(false); const [createSampleData, setCreateSampleData] = useState(false);
@@ -101,52 +103,50 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
// ============================================================================ // ============================================================================
if (authStatus.needsSetup) { if (authStatus.needsSetup) {
return ( return (
<div className="min-h-screen bg-canvas text-white flex flex-col items-center justify-center p-4 font-sans"> <div className="min-h-screen bg-canvas text-main flex flex-col items-center justify-center p-4 font-sans">
<div className="w-full max-w-xl bg-panel border border-border shadow-2xl"> <div className="w-full max-w-xl panel-surface shadow-panel overflow-hidden">
{/* Top Brand Bar */} {/* Top Brand Bar */}
<div className="p-4 bg-canvas border-b border-border flex items-center justify-between"> <div className="p-4 bg-canvas border-b border-border flex items-center justify-between">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<div className="w-8 h-8 bg-accent text-white flex items-center justify-center"> <div className="w-8 h-8 bg-accent text-on-accent rounded-btn flex items-center justify-center">
<Brain className="w-4 h-4" /> <Brain className="w-4 h-4" />
</div> </div>
<div> <div>
<h1 className="text-base font-bold text-white tracking-tight leading-none"> <h1 className="text-base font-bold text-main tracking-tight leading-none">
MindSift Ersteinrichtung MindSift Ersteinrichtung
</h1> </h1>
<p className="text-[11px] text-zinc-400 mt-0.5"> <p className="text-[11px] text-muted mt-0.5">
Initiales Administrator-Konto & Server-Konfiguration Initiales Administrator-Konto & Server-Konfiguration
</p> </p>
</div> </div>
</div> </div>
<span className="px-2 py-0.5 bg-accent/15 border border-accent text-accent text-[10px] font-semibold uppercase tracking-wider"> <span className="badge-accent uppercase tracking-wider">First Boot</span>
First Boot
</span>
</div> </div>
<form onSubmit={handleCompleteSetup} className="p-5 space-y-5 text-xs"> <form onSubmit={handleCompleteSetup} className="p-5 space-y-5 text-xs">
<p className="text-zinc-300 leading-relaxed"> <p className="text-muted leading-relaxed">
Willkommen auf deiner neuen MindSift-Instanz. Es existiert noch kein Benutzerkonto in Willkommen auf deiner neuen MindSift-Instanz. Es existiert noch kein Benutzerkonto in
der Datenbank. Lege jetzt das erste <strong>Server-Admin-Konto</strong> an und bestimme der Datenbank. Lege jetzt das erste <strong className="text-main">Server-Admin-Konto</strong> an und
die Sicherheitseinstellungen. bestimme die Sicherheitseinstellungen.
</p> </p>
{error && ( {error && (
<div className="p-3 bg-red-950/40 border border-red-500 text-red-200 flex items-center gap-2"> <div className="alert-err flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" /> <AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
<span>{error}</span> <span>{error}</span>
</div> </div>
)} )}
{/* Section 1: Admin Account */} {/* Section 1: Admin Account */}
<div className="p-3.5 bg-canvas border border-border space-y-3"> <div className="p-3.5 bg-canvas border border-border space-y-3">
<h2 className="text-xs font-bold text-white uppercase tracking-wider flex items-center gap-1.5"> <h2 className="text-xs font-bold text-main uppercase tracking-wider flex items-center gap-1.5">
<Shield className="w-3.5 h-3.5 text-accent" /> <Shield className="w-3.5 h-3.5 text-accent" />
<span>1. Server-Administrator anlegen</span> <span>1. Server-Administrator anlegen</span>
</h2> </h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div> <div>
<label className="block text-zinc-400 mb-1">Dein Anzeigename *</label> <label className="block text-muted mb-1">Dein Anzeigename *</label>
<input <input
type="text" type="text"
value={setupName} value={setupName}
@@ -154,24 +154,24 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
placeholder="z.B. Admin / Dein Name" placeholder="z.B. Admin / Dein Name"
required required
autoFocus autoFocus
className="w-full h-8 px-2.5 input-field text-white" className="w-full h-8 px-2.5 input-field"
/> />
</div> </div>
<div> <div>
<label className="block text-zinc-400 mb-1">E-Mail-Adresse (Login) *</label> <label className="block text-muted mb-1">E-Mail-Adresse (Login) *</label>
<input <input
type="email" type="email"
value={setupEmail} value={setupEmail}
onChange={(e) => setSetupEmail(e.target.value)} onChange={(e) => setSetupEmail(e.target.value)}
placeholder="admin@deinedomain.de" placeholder="admin@deinedomain.de"
required required
className="w-full h-8 px-2.5 input-field text-white" className="w-full h-8 px-2.5 input-field"
/> />
</div> </div>
</div> </div>
<div> <div>
<label className="block text-zinc-400 mb-1">Administrator-Passwort *</label> <label className="block text-muted mb-1">Administrator-Passwort *</label>
<input <input
type="password" type="password"
value={setupPassword} value={setupPassword}
@@ -179,31 +179,31 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
placeholder="Mindestens 4 Zeichen" placeholder="Mindestens 4 Zeichen"
minLength={4} minLength={4}
required required
className="w-full h-8 px-2.5 input-field text-white" className="w-full h-8 px-2.5 input-field"
/> />
</div> </div>
</div> </div>
{/* Section 2: Server & Isolation */} {/* Section 2: Server & Isolation */}
<div className="p-3.5 bg-canvas border border-border space-y-3"> <div className="p-3.5 bg-canvas border border-border space-y-3">
<h2 className="text-xs font-bold text-white uppercase tracking-wider flex items-center gap-1.5"> <h2 className="text-xs font-bold text-main uppercase tracking-wider flex items-center gap-1.5">
<Server className="w-3.5 h-3.5 text-accent" /> <Server className="w-3.5 h-3.5 text-accent" />
<span>2. Instanz & Berechtigungen</span> <span>2. Instanz & Berechtigungen</span>
</h2> </h2>
<div> <div>
<label className="block text-zinc-400 mb-1">Name der Instanz</label> <label className="block text-muted mb-1">Name der Instanz</label>
<input <input
type="text" type="text"
value={instanceName} value={instanceName}
onChange={(e) => setInstanceName(e.target.value)} onChange={(e) => setInstanceName(e.target.value)}
placeholder="MindSift" placeholder="MindSift"
className="w-full h-8 px-2.5 input-field text-white" className="w-full h-8 px-2.5 input-field"
/> />
</div> </div>
<div> <div>
<label className="block text-zinc-400 mb-1.5 flex items-center gap-1"> <label className="block text-muted mb-1.5 flex items-center gap-1">
<Share2 className="w-3 h-3 text-accent" /> <Share2 className="w-3 h-3 text-accent" />
<span>Sichtbarkeit & Sharing-Isolation zwischen Benutzern</span> <span>Sichtbarkeit & Sharing-Isolation zwischen Benutzern</span>
</label> </label>
@@ -213,12 +213,12 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
onClick={() => setSharingMode('strict_private')} onClick={() => setSharingMode('strict_private')}
className={`p-2.5 text-left border transition-colors ${ className={`p-2.5 text-left border transition-colors ${
sharingMode === 'strict_private' sharingMode === 'strict_private'
? 'bg-panel border-accent text-white' ? 'bg-panel border-accent text-main'
: 'border-border text-zinc-400 hover:text-white' : 'border-border text-muted hover:text-main'
}`} }`}
> >
<p className="font-bold text-xs">Strikt Privat</p> <p className="font-bold text-xs">Strikt Privat</p>
<p className="text-[10px] text-zinc-400 mt-0.5"> <p className="text-[10px] text-muted mt-0.5">
Keiner sieht andere Nutzer. Komplett isoliert. Keiner sieht andere Nutzer. Komplett isoliert.
</p> </p>
</button> </button>
@@ -228,12 +228,12 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
onClick={() => setSharingMode('teams_only')} onClick={() => setSharingMode('teams_only')}
className={`p-2.5 text-left border transition-colors ${ className={`p-2.5 text-left border transition-colors ${
sharingMode === 'teams_only' sharingMode === 'teams_only'
? 'bg-panel border-accent text-white' ? 'bg-panel border-accent text-main'
: 'border-border text-zinc-400 hover:text-white' : 'border-border text-muted hover:text-main'
}`} }`}
> >
<p className="font-bold text-xs">Teams isoliert</p> <p className="font-bold text-xs">Teams isoliert</p>
<p className="text-[10px] text-zinc-400 mt-0.5"> <p className="text-[10px] text-muted mt-0.5">
Nutzer sehen nur Mitglieder ihrer eigenen Teams. Nutzer sehen nur Mitglieder ihrer eigenen Teams.
</p> </p>
</button> </button>
@@ -243,12 +243,12 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
onClick={() => setSharingMode('open')} onClick={() => setSharingMode('open')}
className={`p-2.5 text-left border transition-colors ${ className={`p-2.5 text-left border transition-colors ${
sharingMode === 'open' sharingMode === 'open'
? 'bg-panel border-accent text-white' ? 'bg-panel border-accent text-main'
: 'border-border text-zinc-400 hover:text-white' : 'border-border text-muted hover:text-main'
}`} }`}
> >
<p className="font-bold text-xs">Alle sehen alle</p> <p className="font-bold text-xs">Alle sehen alle</p>
<p className="text-[10px] text-zinc-400 mt-0.5"> <p className="text-[10px] text-muted mt-0.5">
Offene Zusammenarbeit auf dem gesamten Server. Offene Zusammenarbeit auf dem gesamten Server.
</p> </p>
</button> </button>
@@ -258,11 +258,12 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
<div className="pt-2 border-t border-border space-y-2"> <div className="pt-2 border-t border-border space-y-2">
<label className="flex items-center justify-between cursor-pointer select-none"> <label className="flex items-center justify-between cursor-pointer select-none">
<div> <div>
<span className="font-medium text-white block"> <span className="font-medium text-main block">
Öffentliche Selbstregistrierung erlauben Öffentliche Selbstregistrierung erlauben
</span> </span>
<span className="text-[11px] text-zinc-400"> <span className="text-[11px] text-muted">
Wenn deaktiviert, können neue Nutzer nur per Einladungscode oder durch einen Admin angelegt werden. Wenn deaktiviert, können neue Nutzer nur per Einladungscode oder durch einen
Admin angelegt werden.
</span> </span>
</div> </div>
<input <input
@@ -275,10 +276,10 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
<label className="flex items-center justify-between cursor-pointer select-none pt-1"> <label className="flex items-center justify-between cursor-pointer select-none pt-1">
<div> <div>
<span className="font-medium text-white block"> <span className="font-medium text-main block">
Start-Beispieldaten anlegen Start-Beispieldaten anlegen
</span> </span>
<span className="text-[11px] text-zinc-400"> <span className="text-[11px] text-muted">
Erstellt wenige Beispiel-Gedanken und ein Einführungsdokument zum Testen. Erstellt wenige Beispiel-Gedanken und ein Einführungsdokument zum Testen.
</span> </span>
</div> </div>
@@ -299,7 +300,11 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
className="btn-primary h-9 px-5 text-xs font-semibold flex items-center gap-2 disabled:opacity-50" className="btn-primary h-9 px-5 text-xs font-semibold flex items-center gap-2 disabled:opacity-50"
> >
<Check className="w-4 h-4" /> <Check className="w-4 h-4" />
<span>{submitting ? 'Richte Server ein…' : 'Ersteinrichtung abschließen & Starten'}</span> <span>
{submitting
? 'Richte Server ein…'
: 'Ersteinrichtung abschließen & Starten'}
</span>
</button> </button>
</div> </div>
</form> </form>
@@ -312,24 +317,22 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
// STANDARD LOGIN / INVITE CODE / REGISTRATION VIEW // STANDARD LOGIN / INVITE CODE / REGISTRATION VIEW
// ============================================================================ // ============================================================================
return ( return (
<div className="min-h-screen bg-canvas text-white flex flex-col items-center justify-center p-4 font-sans"> <div className="min-h-screen bg-canvas text-main flex flex-col items-center justify-center p-4 font-sans">
<div className="w-full max-w-md bg-panel border border-border shadow-2xl"> <div className="w-full max-w-md panel-surface shadow-panel overflow-hidden">
{/* Top Header */} {/* Top Header */}
<div className="p-4 bg-canvas border-b border-border flex items-center justify-between"> <div className="p-4 bg-canvas border-b border-border flex items-center justify-between">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<div className="w-7 h-7 bg-accent text-white flex items-center justify-center"> <div className="w-7 h-7 bg-accent text-on-accent rounded-btn flex items-center justify-center">
<Brain className="w-4 h-4" /> <Brain className="w-4 h-4" />
</div> </div>
<div> <div>
<h1 className="text-sm font-bold text-white tracking-tight leading-none"> <h1 className="text-sm font-bold text-main tracking-tight leading-none">
{authStatus.instanceName || 'MindSift'} {authStatus.instanceName || 'MindSift'}
</h1> </h1>
<p className="text-[11px] text-zinc-400 mt-0.5"> <p className="text-[11px] text-muted mt-0.5">Der Kopf frei in drei Sekunden</p>
Der Kopf frei in drei Sekunden
</p>
</div> </div>
</div> </div>
<Lock className="w-4 h-4 text-zinc-500" /> <Lock className="w-4 h-4 text-faint" />
</div> </div>
{/* Navigation Tabs */} {/* Navigation Tabs */}
@@ -342,8 +345,8 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
}} }}
className={`flex-1 h-9 px-3 font-medium flex items-center justify-center gap-1.5 border-r border-border transition-colors ${ className={`flex-1 h-9 px-3 font-medium flex items-center justify-center gap-1.5 border-r border-border transition-colors ${
authTab === 'login' authTab === 'login'
? 'bg-panel text-white border-b-2 border-b-accent' ? 'bg-panel text-main border-b-2 border-b-accent'
: 'text-zinc-400 hover:text-white' : 'text-muted hover:text-main'
}`} }`}
> >
<LogIn className="w-3.5 h-3.5" /> <LogIn className="w-3.5 h-3.5" />
@@ -358,8 +361,8 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
}} }}
className={`flex-1 h-9 px-3 font-medium flex items-center justify-center gap-1.5 border-r border-border transition-colors ${ className={`flex-1 h-9 px-3 font-medium flex items-center justify-center gap-1.5 border-r border-border transition-colors ${
authTab === 'invite' authTab === 'invite'
? 'bg-panel text-white border-b-2 border-b-accent' ? 'bg-panel text-main border-b-2 border-b-accent'
: 'text-zinc-400 hover:text-white' : 'text-muted hover:text-main'
}`} }`}
> >
<KeyRound className="w-3.5 h-3.5" /> <KeyRound className="w-3.5 h-3.5" />
@@ -375,8 +378,8 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
}} }}
className={`flex-1 h-9 px-3 font-medium flex items-center justify-center gap-1.5 transition-colors ${ className={`flex-1 h-9 px-3 font-medium flex items-center justify-center gap-1.5 transition-colors ${
authTab === 'register' authTab === 'register'
? 'bg-panel text-white border-b-2 border-b-accent' ? 'bg-panel text-main border-b-2 border-b-accent'
: 'text-zinc-400 hover:text-white' : 'text-muted hover:text-main'
}`} }`}
> >
<UserPlus className="w-3.5 h-3.5" /> <UserPlus className="w-3.5 h-3.5" />
@@ -388,8 +391,8 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
{/* Form Body */} {/* Form Body */}
<div className="p-5 text-xs"> <div className="p-5 text-xs">
{error && ( {error && (
<div className="mb-4 p-2.5 bg-red-950/40 border border-red-500 text-red-200 flex items-center gap-2"> <div className="mb-4 alert-err flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" /> <AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
<span>{error}</span> <span>{error}</span>
</div> </div>
)} )}
@@ -397,7 +400,7 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
{authTab === 'login' && ( {authTab === 'login' && (
<form onSubmit={handleLogin} className="space-y-3.5"> <form onSubmit={handleLogin} className="space-y-3.5">
<div> <div>
<label className="block text-zinc-400 mb-1">E-Mail-Adresse</label> <label className="block text-muted mb-1">E-Mail-Adresse</label>
<input <input
type="email" type="email"
value={email} value={email}
@@ -405,19 +408,19 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
placeholder="name@beispiel.de" placeholder="name@beispiel.de"
required required
autoFocus autoFocus
className="w-full h-8 px-2.5 input-field text-white" className="w-full h-8 px-2.5 input-field"
/> />
</div> </div>
<div> <div>
<label className="block text-zinc-400 mb-1">Passwort</label> <label className="block text-muted mb-1">Passwort</label>
<input <input
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••" placeholder="••••••••"
required required
className="w-full h-8 px-2.5 input-field text-white" className="w-full h-8 px-2.5 input-field"
/> />
</div> </div>
@@ -433,7 +436,7 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
</div> </div>
{!authStatus.allowRegistration && ( {!authStatus.allowRegistration && (
<p className="text-[11px] text-zinc-500 text-center pt-2 border-t border-border"> <p className="text-[11px] text-faint text-center pt-2 border-t border-border">
Öffentliche Registrierung ist deaktiviert. Zugang nur mit Konto oder{' '} Öffentliche Registrierung ist deaktiviert. Zugang nur mit Konto oder{' '}
<button <button
type="button" type="button"
@@ -452,7 +455,7 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
<form onSubmit={handleRegisterOrInvite} className="space-y-3.5"> <form onSubmit={handleRegisterOrInvite} className="space-y-3.5">
{authTab === 'invite' && ( {authTab === 'invite' && (
<div> <div>
<label className="block text-zinc-400 mb-1">Einladungscode *</label> <label className="block text-muted mb-1">Einladungscode *</label>
<input <input
type="text" type="text"
value={inviteCode} value={inviteCode}
@@ -460,37 +463,37 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
placeholder="MS-XXXXXXXX" placeholder="MS-XXXXXXXX"
required required
autoFocus autoFocus
className="w-full h-8 px-2.5 input-field text-white font-mono uppercase" className="w-full h-8 px-2.5 input-field font-mono uppercase"
/> />
</div> </div>
)} )}
<div> <div>
<label className="block text-zinc-400 mb-1">Anzeigename *</label> <label className="block text-muted mb-1">Anzeigename *</label>
<input <input
type="text" type="text"
value={displayName} value={displayName}
onChange={(e) => setDisplayName(e.target.value)} onChange={(e) => setDisplayName(e.target.value)}
placeholder="Dein Name" placeholder="Dein Name"
required required
className="w-full h-8 px-2.5 input-field text-white" className="w-full h-8 px-2.5 input-field"
/> />
</div> </div>
<div> <div>
<label className="block text-zinc-400 mb-1">E-Mail-Adresse *</label> <label className="block text-muted mb-1">E-Mail-Adresse *</label>
<input <input
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="name@beispiel.de" placeholder="name@beispiel.de"
required required
className="w-full h-8 px-2.5 input-field text-white" className="w-full h-8 px-2.5 input-field"
/> />
</div> </div>
<div> <div>
<label className="block text-zinc-400 mb-1">Passwort *</label> <label className="block text-muted mb-1">Passwort *</label>
<input <input
type="password" type="password"
value={password} value={password}
@@ -498,7 +501,7 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
placeholder="Mindestens 4 Zeichen" placeholder="Mindestens 4 Zeichen"
minLength={4} minLength={4}
required required
className="w-full h-8 px-2.5 input-field text-white" className="w-full h-8 px-2.5 input-field"
/> />
</div> </div>
@@ -210,15 +210,15 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
return ( return (
<div className="w-full"> <div className="w-full">
<form ref={containerRef} onSubmit={handleSubmit} className="relative"> <form ref={containerRef} onSubmit={handleSubmit} className="relative">
{/* Round Quick-Capture Input Bar */} {/* Quick-Capture Input Bar (Radius controlled by --radius-pill) */}
<div <div
className={`flex items-center h-9 pl-1.5 pr-1.5 bg-panel border rounded-full transition-colors duration-150 ${ className={`flex items-center h-9 pl-1.5 pr-1.5 bg-panel border capture-pill transition-colors duration-150 ${
isListening isListening
? 'border-accent bg-panel' ? 'border-accent bg-panel'
: 'border-border focus-within:border-accent' : 'border-border focus-within:border-accent'
}`} }`}
> >
<div className="w-6 h-6 rounded-full bg-accent text-white flex items-center justify-center mr-2 flex-shrink-0"> <div className="w-6 h-6 capture-pill bg-accent text-on-accent flex items-center justify-center mr-2 flex-shrink-0">
<Sparkles className="w-3 h-3" /> <Sparkles className="w-3 h-3" />
</div> </div>
@@ -244,7 +244,7 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
? 'Neue Workspace-Notiz direkt anlegen…' ? 'Neue Workspace-Notiz direkt anlegen…'
: 'Quick Dump: Gedanke einfach reinrotzen & später swipen… (C / Enter)' : 'Quick Dump: Gedanke einfach reinrotzen & später swipen… (C / Enter)'
} }
className="w-full bg-transparent text-white placeholder-zinc-500 text-xs sm:text-sm focus:outline-none" className="w-full bg-transparent text-main placeholder-faint text-xs sm:text-sm focus:outline-none"
autoComplete="off" autoComplete="off"
/> />
@@ -254,10 +254,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
type="button" type="button"
onClick={() => setShowOptions((prev) => !prev)} onClick={() => setShowOptions((prev) => !prev)}
title="Ziel wählen (Quick Dump, direkt als Aufgabe, auf Liste oder als Notiz) & Optionen" title="Ziel wählen (Quick Dump, direkt als Aufgabe, auf Liste oder als Notiz) & Optionen"
className={`h-6 px-2.5 rounded-full text-[11px] flex items-center gap-1 transition-colors ${ className={`h-6 px-2.5 capture-pill text-[11px] flex items-center gap-1 transition-colors ${
showOptions || target !== 'inbox' || priority !== 'normal' || dueDate showOptions || target !== 'inbox' || priority !== 'normal' || dueDate
? 'bg-accent/20 text-accent border border-accent/40 font-medium' ? 'bg-accent-dim text-accent border border-accent-border font-medium'
: 'text-zinc-400 hover:text-white hover:bg-panel-elevated border border-transparent' : 'text-muted hover:text-main hover:bg-panel-elevated border border-transparent'
}`} }`}
> >
{target === 'task' ? ( {target === 'task' ? (
@@ -281,10 +281,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
type="button" type="button"
onClick={toggleVoiceDictation} onClick={toggleVoiceDictation}
title="Spracheingabe" title="Spracheingabe"
className={`w-6 h-6 rounded-full flex items-center justify-center transition-colors ${ className={`w-6 h-6 capture-pill flex items-center justify-center transition-colors ${
isListening isListening
? 'bg-accent/20 text-accent' ? 'bg-accent-dim text-accent'
: 'text-zinc-400 hover:text-white hover:bg-panel-elevated' : 'text-muted hover:text-main hover:bg-panel-elevated'
}`} }`}
> >
{isListening ? <MicOff className="w-3 h-3" /> : <Mic className="w-3 h-3" />} {isListening ? <MicOff className="w-3 h-3" /> : <Mic className="w-3 h-3" />}
@@ -294,10 +294,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
<button <button
type="submit" type="submit"
disabled={!content.trim() || isSubmitting} disabled={!content.trim() || isSubmitting}
className={`h-6 px-2.5 rounded-full font-medium text-[11px] flex items-center gap-1 transition-all ${ className={`h-6 px-2.5 capture-pill font-medium text-[11px] flex items-center gap-1 transition-all ${
content.trim() content.trim()
? 'bg-accent hover:bg-accent-hover text-white cursor-pointer shadow-sm' ? 'bg-accent hover:bg-accent-hover text-on-accent cursor-pointer shadow-sm'
: 'bg-panel-elevated text-zinc-500 cursor-not-allowed' : 'bg-panel-elevated text-faint cursor-not-allowed'
}`} }`}
> >
{justSavedLabel ? ( {justSavedLabel ? (
@@ -323,16 +323,16 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
</div> </div>
</div> </div>
{/* Expandable Target & Metadata Panel (Square Monochromatic Dropdown) */} {/* Expandable Target & Metadata Panel (Theme-controlled Dropdown) */}
{showOptions && ( {showOptions && (
<div className="absolute top-full left-0 right-0 mt-1.5 z-50 p-3 bg-panel border border-border shadow-2xl space-y-3 text-xs"> <div className="absolute top-full left-0 right-0 mt-1.5 z-50 p-3 panel-surface shadow-panel space-y-3 text-xs">
{/* Row 1: Where should this entry go? */} {/* Row 1: Where should this entry go? */}
<div> <div>
<div className="flex items-center justify-between mb-1.5"> <div className="flex items-center justify-between mb-1.5">
<span className="text-[11px] font-semibold uppercase tracking-wider text-zinc-400"> <span className="text-[11px] font-semibold uppercase tracking-wider text-muted">
Wohin damit? {content.trim() ? '(Klick speichert sofort oder wählt Ziel)' : '(Ziel für Eingabe wählen)'} Wohin damit? {content.trim() ? '(Klick speichert sofort oder wählt Ziel)' : '(Ziel für Eingabe wählen)'}
</span> </span>
<span className="text-[11px] text-zinc-500"> <span className="text-[11px] text-faint">
Standard: Quick Dump für späteres Swipen Standard: Quick Dump für späteres Swipen
</span> </span>
</div> </div>
@@ -347,10 +347,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
executeCapture('inbox'); executeCapture('inbox');
} }
}} }}
className={`h-8 px-2.5 border flex items-center gap-1.5 text-left transition-colors ${ className={`h-8 px-2.5 border rounded-btn flex items-center gap-1.5 text-left transition-colors ${
target === 'inbox' target === 'inbox'
? 'bg-accent/20 border-accent text-white font-medium' ? 'bg-accent-dim border-accent text-main font-medium'
: 'bg-canvas border-border text-zinc-300 hover:bg-panel-hover' : 'bg-canvas border-border text-muted hover:text-main hover:bg-panel-hover'
}`} }`}
> >
<Layers className="w-3.5 h-3.5 text-accent flex-shrink-0" /> <Layers className="w-3.5 h-3.5 text-accent flex-shrink-0" />
@@ -366,10 +366,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
executeCapture('task'); executeCapture('task');
} }
}} }}
className={`h-8 px-2.5 border flex items-center gap-1.5 text-left transition-colors ${ className={`h-8 px-2.5 border rounded-btn flex items-center gap-1.5 text-left transition-colors ${
target === 'task' target === 'task'
? 'bg-accent/20 border-accent text-white font-medium' ? 'bg-accent-dim border-accent text-main font-medium'
: 'bg-canvas border-border text-zinc-300 hover:bg-panel-hover' : 'bg-canvas border-border text-muted hover:text-main hover:bg-panel-hover'
}`} }`}
> >
<CheckSquare className="w-3.5 h-3.5 text-accent flex-shrink-0" /> <CheckSquare className="w-3.5 h-3.5 text-accent flex-shrink-0" />
@@ -385,10 +385,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
setTargetListId(lists[0].id); setTargetListId(lists[0].id);
} }
}} }}
className={`h-8 px-2.5 border flex items-center gap-1.5 text-left transition-colors ${ className={`h-8 px-2.5 border rounded-btn flex items-center gap-1.5 text-left transition-colors ${
target === 'list_item' target === 'list_item'
? 'bg-accent/20 border-accent text-white font-medium' ? 'bg-accent-dim border-accent text-main font-medium'
: 'bg-canvas border-border text-zinc-300 hover:bg-panel-hover' : 'bg-canvas border-border text-muted hover:text-main hover:bg-panel-hover'
}`} }`}
> >
<ShoppingCart className="w-3.5 h-3.5 text-accent flex-shrink-0" /> <ShoppingCart className="w-3.5 h-3.5 text-accent flex-shrink-0" />
@@ -404,10 +404,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
executeCapture('document'); executeCapture('document');
} }
}} }}
className={`h-8 px-2.5 border flex items-center gap-1.5 text-left transition-colors ${ className={`h-8 px-2.5 border rounded-btn flex items-center gap-1.5 text-left transition-colors ${
target === 'document' target === 'document'
? 'bg-accent/20 border-accent text-white font-medium' ? 'bg-accent-dim border-accent text-main font-medium'
: 'bg-canvas border-border text-zinc-300 hover:bg-panel-hover' : 'bg-canvas border-border text-muted hover:text-main hover:bg-panel-hover'
}`} }`}
> >
<FileText className="w-3.5 h-3.5 text-accent flex-shrink-0" /> <FileText className="w-3.5 h-3.5 text-accent flex-shrink-0" />
@@ -416,10 +416,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
</div> </div>
</div> </div>
{/* Optional Sub-Row: Shared List Picker (always shown if lists exist so 1-click list assignment is instant) */} {/* Optional Sub-Row: Shared List Picker */}
{lists.length > 0 && ( {lists.length > 0 && (
<div className="pt-2 border-t border-border flex flex-wrap items-center gap-1.5"> <div className="pt-2 border-t border-border flex flex-wrap items-center gap-1.5">
<span className="text-zinc-400 mr-1 flex items-center gap-1"> <span className="text-muted mr-1 flex items-center gap-1">
<ShoppingCart className="w-3 h-3 text-accent" /> <ShoppingCart className="w-3 h-3 text-accent" />
Liste: Liste:
</span> </span>
@@ -436,10 +436,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
executeCapture('list_item', lst.id); executeCapture('list_item', lst.id);
} }
}} }}
className={`h-7 px-2.5 border text-xs flex items-center gap-1.5 transition-colors ${ className={`h-7 px-2.5 border rounded-btn text-xs flex items-center gap-1.5 transition-colors ${
isSelected isSelected
? 'bg-accent text-white border-accent font-medium' ? 'bg-accent text-on-accent border-accent font-medium'
: 'bg-canvas border-border text-zinc-300 hover:text-white hover:bg-panel-hover' : 'bg-canvas border-border text-muted hover:text-main hover:bg-panel-hover'
}`} }`}
> >
<span>{lst.icon}</span> <span>{lst.icon}</span>
@@ -453,7 +453,7 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
{/* Row 2: Priority & Due Date */} {/* Row 2: Priority & Due Date */}
<div className="pt-2 border-t border-border flex flex-wrap items-center justify-between gap-2"> <div className="pt-2 border-t border-border flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="text-zinc-400 mr-1">Priorität:</span> <span className="text-muted mr-1">Priorität:</span>
{(['low', 'normal', 'high', 'urgent'] as PriorityLevel[]).map((p) => { {(['low', 'normal', 'high', 'urgent'] as PriorityLevel[]).map((p) => {
const labels: Record<PriorityLevel, string> = { const labels: Record<PriorityLevel, string> = {
low: 'Niedrig', low: 'Niedrig',
@@ -467,10 +467,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
key={p} key={p}
type="button" type="button"
onClick={() => setPriority(p)} onClick={() => setPriority(p)}
className={`px-2.5 py-0.5 border transition-colors text-xs ${ className={`px-2.5 py-0.5 border rounded-btn transition-colors text-xs ${
active active
? 'bg-accent text-white border-accent font-medium' ? 'bg-accent text-on-accent border-accent font-medium'
: 'bg-canvas text-zinc-400 hover:text-white border-border' : 'bg-canvas text-muted hover:text-main border-border'
}`} }`}
> >
{labels[p]} {labels[p]}
@@ -480,7 +480,7 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-zinc-400 flex items-center gap-1"> <span className="text-muted flex items-center gap-1">
<Calendar className="w-3 h-3 text-accent" /> <Calendar className="w-3 h-3 text-accent" />
Fällig am: Fällig am:
</span> </span>
+42 -57
View File
@@ -148,54 +148,39 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
const text = newItemText.trim(); const text = newItemText.trim();
const qty = newItemQty.trim(); const qty = newItemQty.trim();
const cat = newItemCategory; const cat = newItemCategory || undefined;
setNewItemText(''); setNewItemText('');
setNewItemQty(''); setNewItemQty('');
const created = await api.createListItem(activeList.id, text, cat, qty); try {
setLists((prev) => const item = await api.createListItem(activeList.id, text, qty, cat);
prev.map((l) => { setLists((prev) =>
if (l.id !== activeList.id) return l; prev.map((l) => (l.id === activeList.id ? { ...l, items: [item, ...l.items] } : l))
if (l.items.some((i) => i.id === created.id)) return l; );
return { ...l, items: [created, ...l.items] }; onRefreshGlobal();
}) } catch (err: any) {
); alert(err.message || 'Fehler beim Erstellen des Listeneintrags');
}
}; };
const handleToggleItem = async (item: ListItem) => { const handleToggleItem = async (item: ListItem) => {
if (!activeList) return; if (!activeList) return;
triggerHaptic(18); triggerHaptic(10);
const nextCompleted = !item.isCompleted; const nextCompleted = !item.isCompleted;
// Optimistic UI update (BRAIN.md Section 3)
setLists((prev) => setLists((prev) =>
prev.map((l) => { prev.map((l) => {
if (l.id !== activeList.id) return l; if (l.id !== activeList.id) return l;
return { return {
...l, ...l,
items: l.items.map((i) => items: l.items.map((i) => (i.id === item.id ? { ...i, isCompleted: nextCompleted } : i)),
i.id === item.id ? { ...i, isCompleted: nextCompleted } : i
),
}; };
}) })
); );
try { await api.toggleListItem(activeList.id, item.id);
const updated = await api.toggleListItem(activeList.id, item.id, nextCompleted); onRefreshGlobal();
setLists((prev) =>
prev.map((l) => {
if (l.id !== activeList.id) return l;
return {
...l,
items: l.items.map((i) => (i.id === item.id ? updated : i)),
};
})
);
} catch {
// Rollback on error
loadListsForSpace(activeSpace!.id);
}
}; };
const handleDeleteItem = async (itemId: string) => { const handleDeleteItem = async (itemId: string) => {
@@ -272,10 +257,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
<button <button
key={sp.id} key={sp.id}
onClick={() => setActiveSpace(sp)} onClick={() => setActiveSpace(sp)}
className={`h-8 px-3 text-xs sm:text-sm font-medium flex items-center gap-1.5 transition-colors border ${ className={`h-8 px-3 text-xs sm:text-sm font-medium flex items-center gap-1.5 transition-colors border rounded-btn ${
activeSpace?.id === sp.id activeSpace?.id === sp.id
? 'bg-accent text-white border-accent' ? 'bg-accent text-on-accent border-accent'
: 'bg-canvas text-zinc-300 hover:text-white border-border' : 'bg-canvas text-muted hover:text-main border-border'
}`} }`}
> >
<span>{sp.icon}</span> <span>{sp.icon}</span>
@@ -302,10 +287,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
{activeSpace && ( {activeSpace && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className={`h-8 px-3 text-xs flex items-center gap-1.5 ${ className={`h-8 px-3 text-xs flex items-center gap-1.5 rounded-btn ${
wsConnected wsConnected
? 'bg-[#22c55e]/15 text-[#22c55e] border border-[#22c55e]/30' ? 'badge-ok'
: 'bg-panel-elevated text-zinc-400 border border-border' : 'badge-neutral'
}`} }`}
> >
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : ''}`} /> <Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : ''}`} />
@@ -320,9 +305,9 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
<Users className="w-3.5 h-3.5 text-accent" /> <Users className="w-3.5 h-3.5 text-accent" />
<span>Code: {activeSpace.inviteCode}</span> <span>Code: {activeSpace.inviteCode}</span>
{copiedInvite ? ( {copiedInvite ? (
<Check className="w-3.5 h-3.5 text-[#22c55e]" /> <Check className="w-3.5 h-3.5 text-status-ok" />
) : ( ) : (
<Copy className="w-3 h-3 text-zinc-400" /> <Copy className="w-3 h-3 text-muted" />
)} )}
</button> </button>
</div> </div>
@@ -383,16 +368,16 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
<button <button
key={lst.id} key={lst.id}
onClick={() => setActiveListId(lst.id)} onClick={() => setActiveListId(lst.id)}
className={`h-8 px-3 text-xs sm:text-sm font-medium flex items-center gap-1.5 transition-all border ${ className={`h-8 px-3 text-xs sm:text-sm font-medium flex items-center gap-1.5 transition-all border rounded-btn ${
isSelected isSelected
? 'bg-accent text-white border-accent' ? 'bg-accent text-on-accent border-accent'
: 'bg-panel text-zinc-300 hover:text-white border-border' : 'bg-panel text-muted hover:text-main border-border'
}`} }`}
> >
<span>{lst.icon}</span> <span>{lst.icon}</span>
<span>{lst.title}</span> <span>{lst.title}</span>
<span className={`px-1.5 py-0.2 text-[10px] font-semibold ${ <span className={`px-1.5 py-0.2 text-[10px] font-semibold rounded-pill ${
isSelected ? 'bg-white/20 text-white' : 'bg-canvas text-zinc-400' isSelected ? 'bg-white/20 text-on-accent' : 'badge-neutral'
}`}> }`}>
{openCount} {openCount}
</span> </span>
@@ -420,7 +405,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
key={ic} key={ic}
type="button" type="button"
onClick={() => setNewListIcon(ic)} onClick={() => setNewListIcon(ic)}
className={`w-9 h-9 flex items-center justify-center transition-colors ${ className={`w-9 h-9 flex items-center justify-center rounded-btn transition-colors ${
newListIcon === ic newListIcon === ic
? 'bg-accent/20 border border-accent text-accent' ? 'bg-accent/20 border border-accent text-accent'
: 'bg-canvas hover:bg-panel-elevated border border-border' : 'bg-canvas hover:bg-panel-elevated border border-border'
@@ -481,16 +466,16 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
{/* Category Quick-Select Pills */} {/* Category Quick-Select Pills */}
<div className="flex flex-wrap items-center gap-1.5"> <div className="flex flex-wrap items-center gap-1.5">
<span className="text-xs text-zinc-400 mr-1">Kategorie:</span> <span className="text-xs text-muted mr-1">Kategorie:</span>
{CATEGORIES.map((cat) => ( {CATEGORIES.map((cat) => (
<button <button
key={cat} key={cat}
type="button" type="button"
onClick={() => setNewItemCategory((prev) => (prev === cat ? '' : cat))} onClick={() => setNewItemCategory((prev) => (prev === cat ? '' : cat))}
className={`px-2.5 py-0.5 text-xs transition-colors border ${ className={`px-2.5 py-0.5 text-xs transition-colors border rounded-pill ${
newItemCategory === cat newItemCategory === cat
? 'bg-accent text-white border-accent font-medium' ? 'bg-accent text-on-accent border-accent font-medium'
: 'bg-canvas text-zinc-400 hover:text-white border-border' : 'bg-canvas text-muted hover:text-main border-border'
}`} }`}
> >
{cat} {cat}
@@ -500,9 +485,9 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
</form> </form>
{/* Open Items */} {/* Open Items */}
<div className="border border-border divide-y divide-border"> <div className="border border-border divide-y divide-border rounded-card overflow-hidden">
{openItems.length === 0 ? ( {openItems.length === 0 ? (
<div className="py-6 text-center text-zinc-400 text-sm flex flex-col items-center gap-1.5"> <div className="py-6 text-center text-muted text-sm flex flex-col items-center gap-1.5">
<Sparkles className="w-4 h-4 text-accent" /> <Sparkles className="w-4 h-4 text-accent" />
<span>Alles erledigt. Keine offenen Punkte auf dieser Liste.</span> <span>Alles erledigt. Keine offenen Punkte auf dieser Liste.</span>
</div> </div>
@@ -514,8 +499,8 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
className="px-3 py-2 bg-canvas hover:bg-panel-elevated flex items-center justify-between gap-3 cursor-pointer transition-colors group" className="px-3 py-2 bg-canvas hover:bg-panel-elevated flex items-center justify-between gap-3 cursor-pointer transition-colors group"
> >
<div className="flex items-center gap-2.5 min-w-0"> <div className="flex items-center gap-2.5 min-w-0">
<Circle className="w-4 h-4 text-zinc-500 group-hover:text-accent flex-shrink-0 transition-colors" /> <Circle className="w-4 h-4 text-faint group-hover:text-accent flex-shrink-0 transition-colors" />
<span className="text-sm font-normal text-white truncate"> <span className="text-sm font-normal text-main truncate">
{item.content} {item.content}
</span> </span>
{item.quantity && ( {item.quantity && (
@@ -524,7 +509,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
</span> </span>
)} )}
{item.category && ( {item.category && (
<span className="hidden sm:inline px-2 py-0.2 bg-panel text-zinc-400 text-xs"> <span className="hidden sm:inline px-2 py-0.2 badge-neutral text-xs">
{item.category} {item.category}
</span> </span>
)} )}
@@ -549,7 +534,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
{completedItems.length > 0 && ( {completedItems.length > 0 && (
<div className="mt-3 pt-2.5 border-t border-border"> <div className="mt-3 pt-2.5 border-t border-border">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium uppercase tracking-wider text-zinc-400"> <span className="text-xs font-medium uppercase tracking-wider text-muted">
Erledigt ({completedItems.length}) Erledigt ({completedItems.length})
</span> </span>
<button <button
@@ -560,7 +545,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
</button> </button>
</div> </div>
<div className="border border-border divide-y divide-border opacity-75"> <div className="border border-border divide-y divide-border opacity-75 rounded-card overflow-hidden">
{completedItems.map((item) => ( {completedItems.map((item) => (
<div <div
key={item.id} key={item.id}
@@ -569,15 +554,15 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
> >
<div className="flex items-center gap-2.5 min-w-0"> <div className="flex items-center gap-2.5 min-w-0">
<CheckCircle2 className="w-4 h-4 text-accent flex-shrink-0" /> <CheckCircle2 className="w-4 h-4 text-accent flex-shrink-0" />
<span className="text-sm text-zinc-400 line-through truncate"> <span className="text-sm text-muted line-through truncate">
{item.content} {item.content}
</span> </span>
{item.quantity && ( {item.quantity && (
<span className="text-xs text-zinc-500">({item.quantity})</span> <span className="text-xs text-faint">({item.quantity})</span>
)} )}
</div> </div>
{item.completedByName && ( {item.completedByName && (
<span className="text-xs text-zinc-500">✓ {item.completedByName}</span> <span className="text-xs text-faint">✓ {item.completedByName}</span>
)} )}
</div> </div>
))} ))}
@@ -382,40 +382,38 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<span>Zurück</span> <span>Zurück</span>
</button> </button>
<div> <div>
<h1 className="text-base font-bold text-white tracking-tight flex items-center gap-2"> <h1 className="text-base font-bold text-main tracking-tight flex items-center gap-2">
<Shield className="w-4 h-4 text-accent" /> <Shield className="w-4 h-4 text-accent" />
<span>Administration & Team-Steuerung</span> <span>Administration & Team-Steuerung</span>
</h1> </h1>
<p className="text-xs text-zinc-400"> <p className="text-xs text-muted">
Benutzerverwaltung, Einladungscodes, Organisationen, Teams, Isolationsmodus & Mail-Server Benutzerverwaltung, Einladungscodes, Organisationen, Teams, Isolationsmodus & Mail-Server
</p> </p>
</div> </div>
</div> </div>
<span className="px-2.5 py-1 bg-accent/15 border border-accent text-accent text-xs font-semibold"> <span className="badge-accent">
{isServerAdmin ? 'Server-Administrator' : 'Team-Administrator'} {isServerAdmin ? 'Server-Administrator' : 'Team-Administrator'}
</span> </span>
</div> </div>
{statusMsg && ( {statusMsg && (
<div <div
className={`p-3 border text-xs flex items-center justify-between gap-2 ${ className={`p-3 text-xs flex items-center justify-between gap-2 rounded-card ${
statusMsg.type === 'ok' statusMsg.type === 'ok' ? 'alert-ok' : 'alert-err'
? 'bg-blue-950/30 border-accent text-blue-200'
: 'bg-red-950/30 border-red-500 text-red-200'
}`} }`}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{statusMsg.type === 'ok' ? ( {statusMsg.type === 'ok' ? (
<Check className="w-4 h-4 text-accent flex-shrink-0" /> <Check className="w-4 h-4 text-accent flex-shrink-0" />
) : ( ) : (
<AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" /> <AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
)} )}
<span>{statusMsg.text}</span> <span>{statusMsg.text}</span>
</div> </div>
<button <button
onClick={() => setStatusMsg(null)} onClick={() => setStatusMsg(null)}
className="text-zinc-400 hover:text-white text-[11px]" className="text-muted hover:text-main text-[11px]"
> >
Schließen Schließen
</button> </button>
@@ -425,19 +423,19 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
{/* Main 2-Column Layout */} {/* Main 2-Column Layout */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-start"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-start">
{/* Left Navigation Sidebar */} {/* Left Navigation Sidebar */}
<aside className="bg-panel border border-border divide-y divide-border"> <aside className="panel-surface divide-y divide-border overflow-hidden">
<button <button
onClick={() => setSection('users')} onClick={() => setSection('users')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${ className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'users' section === 'users'
? 'bg-panel-elevated text-white border-l-2 border-l-accent' ? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]' : 'text-muted hover:text-main hover:bg-canvas/50'
}`} }`}
> >
<Users className="w-4 h-4 text-accent flex-shrink-0" /> <Users className="w-4 h-4 text-accent flex-shrink-0" />
<div> <div>
<p className="font-semibold">Benutzer & Einladungen</p> <p className="font-semibold">Benutzer & Einladungen</p>
<p className="text-[10px] text-zinc-500">Konten, Rollen & Invite-Codes</p> <p className="text-[10px] text-faint">Konten, Rollen & Invite-Codes</p>
</div> </div>
</button> </button>
@@ -445,14 +443,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
onClick={() => setSection('orgs')} onClick={() => setSection('orgs')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${ className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'orgs' section === 'orgs'
? 'bg-panel-elevated text-white border-l-2 border-l-accent' ? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]' : 'text-muted hover:text-main hover:bg-canvas/50'
}`} }`}
> >
<Building2 className="w-4 h-4 text-accent flex-shrink-0" /> <Building2 className="w-4 h-4 text-accent flex-shrink-0" />
<div> <div>
<p className="font-semibold">Organisationen & Teams</p> <p className="font-semibold">Organisationen & Teams</p>
<p className="text-[10px] text-zinc-500">Teams & Mitglieder zuweisen</p> <p className="text-[10px] text-faint">Teams & Mitglieder zuweisen</p>
</div> </div>
</button> </button>
@@ -462,14 +460,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
onClick={() => setSection('server')} onClick={() => setSection('server')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${ className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'server' section === 'server'
? 'bg-panel-elevated text-white border-l-2 border-l-accent' ? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]' : 'text-muted hover:text-main hover:bg-canvas/50'
}`} }`}
> >
<Server className="w-4 h-4 text-accent flex-shrink-0" /> <Server className="w-4 h-4 text-accent flex-shrink-0" />
<div> <div>
<p className="font-semibold">Server & Isolation</p> <p className="font-semibold">Server & Isolation</p>
<p className="text-[10px] text-zinc-500">Sichtbarkeit & Registrierung</p> <p className="text-[10px] text-faint">Sichtbarkeit & Registrierung</p>
</div> </div>
</button> </button>
@@ -477,14 +475,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
onClick={() => setSection('smtp')} onClick={() => setSection('smtp')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${ className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'smtp' section === 'smtp'
? 'bg-panel-elevated text-white border-l-2 border-l-accent' ? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]' : 'text-muted hover:text-main hover:bg-canvas/50'
}`} }`}
> >
<Mail className="w-4 h-4 text-accent flex-shrink-0" /> <Mail className="w-4 h-4 text-accent flex-shrink-0" />
<div> <div>
<p className="font-semibold">E-Mail / SMTP-Server</p> <p className="font-semibold">E-Mail / SMTP-Server</p>
<p className="text-[10px] text-zinc-500">Mailversand für Alarme</p> <p className="text-[10px] text-faint">Mailversand für Alarme</p>
</div> </div>
</button> </button>
</> </>
@@ -492,23 +490,23 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
</aside> </aside>
{/* Right Content Area */} {/* Right Content Area */}
<div className="md:col-span-3 bg-panel border border-border p-5 text-xs"> <div className="md:col-span-3 panel-surface p-5 text-xs">
{/* ================================================================= */} {/* ================================================================= */}
{/* SECTION 1: BENUTZER, ROLLEN & EINLADUNGEN */} {/* SECTION 1: BENUTZER, ROLLEN & EINLADUNGEN */}
{/* ================================================================= */} {/* ================================================================= */}
{section === 'users' && ( {section === 'users' && (
<div className="space-y-6"> <div className="space-y-6">
<div className="border-b border-border pb-3"> <div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-white">Benutzersteuerung, Rollen & Einladungen</h2> <h2 className="text-sm font-bold text-main">Benutzersteuerung, Rollen & Einladungen</h2>
<p className="text-zinc-400 mt-0.5"> <p className="text-muted mt-0.5">
Lege Benutzerkonten direkt an oder erzeuge Einladungscodes, mit denen sich eingeladene Lege Benutzerkonten direkt an oder erzeuge Einladungscodes, mit denen sich eingeladene
Personen selbst registrieren können, auch wenn die öffentliche Registrierung gesperrt ist. Personen selbst registrieren können, auch wenn die öffentliche Registrierung gesperrt ist.
</p> </p>
</div> </div>
{/* Direct User Creation Form (Fixed Layout: 4 clean columns with min-w-0, no overflow!) */} {/* Direct User Creation Form */}
<div className="p-4 bg-canvas border border-border space-y-3"> <div className="p-4 bg-canvas border border-border rounded-card space-y-3">
<h3 className="font-bold text-white flex items-center gap-1.5"> <h3 className="font-bold text-main flex items-center gap-1.5">
<UserPlus className="w-3.5 h-3.5 text-accent" /> <UserPlus className="w-3.5 h-3.5 text-accent" />
<span>Neues Benutzerkonto direkt anlegen</span> <span>Neues Benutzerkonto direkt anlegen</span>
</h3> </h3>
@@ -516,31 +514,31 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<form onSubmit={handleCreateUser} className="space-y-3"> <form onSubmit={handleCreateUser} className="space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<div className="min-w-0"> <div className="min-w-0">
<label className="block text-zinc-400 mb-1">Anzeigename</label> <label className="block text-muted mb-1">Anzeigename</label>
<input <input
type="text" type="text"
value={newUserName} value={newUserName}
onChange={(e) => setNewUserName(e.target.value)} onChange={(e) => setNewUserName(e.target.value)}
placeholder="z.B. Anna Beispiel" placeholder="z.B. Anna Beispiel"
required required
className="w-full min-w-0 h-8 px-2.5 input-field text-white" className="w-full min-w-0 h-8 px-2.5 input-field"
/> />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<label className="block text-zinc-400 mb-1">E-Mail-Adresse</label> <label className="block text-muted mb-1">E-Mail-Adresse</label>
<input <input
type="email" type="email"
value={newUserEmail} value={newUserEmail}
onChange={(e) => setNewUserEmail(e.target.value)} onChange={(e) => setNewUserEmail(e.target.value)}
placeholder="anna@beispiel.de" placeholder="anna@beispiel.de"
required required
className="w-full min-w-0 h-8 px-2.5 input-field text-white" className="w-full min-w-0 h-8 px-2.5 input-field"
/> />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<label className="block text-zinc-400 mb-1">Initial-Passwort</label> <label className="block text-muted mb-1">Initial-Passwort</label>
<input <input
type="password" type="password"
value={newUserPassword} value={newUserPassword}
@@ -548,16 +546,16 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
placeholder="Mind. 4 Zeichen" placeholder="Mind. 4 Zeichen"
minLength={4} minLength={4}
required required
className="w-full min-w-0 h-8 px-2.5 input-field text-white" className="w-full min-w-0 h-8 px-2.5 input-field"
/> />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<label className="block text-zinc-400 mb-1">Benutzerrolle</label> <label className="block text-muted mb-1">Benutzerrolle</label>
<select <select
value={newUserRole} value={newUserRole}
onChange={(e) => setNewUserRole(e.target.value as UserRole)} onChange={(e) => setNewUserRole(e.target.value as UserRole)}
className="w-full min-w-0 h-8 px-2 input-field text-white bg-panel" className="w-full min-w-0 h-8 px-2 input-field bg-panel"
> >
<option value="member">Mitglied (Standard)</option> <option value="member">Mitglied (Standard)</option>
<option value="team_admin">Team-Admin (Darf einladen)</option> <option value="team_admin">Team-Admin (Darf einladen)</option>
@@ -579,8 +577,8 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
</div> </div>
{/* Invite Code Generator */} {/* Invite Code Generator */}
<div className="p-4 bg-canvas border border-border space-y-3"> <div className="p-4 bg-canvas border border-border rounded-card space-y-3">
<h3 className="font-bold text-white flex items-center gap-1.5"> <h3 className="font-bold text-main flex items-center gap-1.5">
<KeyRound className="w-3.5 h-3.5 text-accent" /> <KeyRound className="w-3.5 h-3.5 text-accent" />
<span>Einladungscode erzeugen (gültig für 7 Tage)</span> <span>Einladungscode erzeugen (gültig für 7 Tage)</span>
</h3> </h3>
@@ -588,7 +586,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<form onSubmit={handleCreateInvite} className="space-y-3"> <form onSubmit={handleCreateInvite} className="space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="min-w-0"> <div className="min-w-0">
<label className="block text-zinc-400 mb-1"> <label className="block text-muted mb-1">
Gebundene E-Mail (optional) Gebundene E-Mail (optional)
</label> </label>
<input <input
@@ -596,16 +594,16 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
value={inviteEmail} value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)} onChange={(e) => setInviteEmail(e.target.value)}
placeholder="Leer lassen für beliebige E-Mail" placeholder="Leer lassen für beliebige E-Mail"
className="w-full min-w-0 h-8 px-2.5 input-field text-white" className="w-full min-w-0 h-8 px-2.5 input-field"
/> />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<label className="block text-zinc-400 mb-1">Zugewiesene Rolle</label> <label className="block text-muted mb-1">Zugewiesene Rolle</label>
<select <select
value={inviteRole} value={inviteRole}
onChange={(e) => setInviteRole(e.target.value as UserRole)} onChange={(e) => setInviteRole(e.target.value as UserRole)}
className="w-full min-w-0 h-8 px-2 input-field text-white bg-panel" className="w-full min-w-0 h-8 px-2 input-field bg-panel"
> >
<option value="member">Mitglied</option> <option value="member">Mitglied</option>
<option value="team_admin">Team-Admin</option> <option value="team_admin">Team-Admin</option>
@@ -614,13 +612,13 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<label className="block text-zinc-400 mb-1"> <label className="block text-muted mb-1">
Automatisch Team zuweisen (optional) Automatisch Team zuweisen (optional)
</label> </label>
<select <select
value={inviteTeamId} value={inviteTeamId}
onChange={(e) => setInviteTeamId(e.target.value)} onChange={(e) => setInviteTeamId(e.target.value)}
className="w-full min-w-0 h-8 px-2 input-field text-white bg-panel" className="w-full min-w-0 h-8 px-2 input-field bg-panel"
> >
<option value="">Kein direktes Team</option> <option value="">Kein direktes Team</option>
{flattenedTeams.map((t) => ( {flattenedTeams.map((t) => (
@@ -635,7 +633,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<div className="flex justify-end"> <div className="flex justify-end">
<button <button
type="submit" type="submit"
className="btn-action h-8 px-4 text-xs font-semibold flex items-center gap-1.5 border-accent text-white" className="btn-action h-8 px-4 text-xs font-semibold flex items-center gap-1.5 border-accent text-main"
> >
<KeyRound className="w-3.5 h-3.5 text-accent" /> <KeyRound className="w-3.5 h-3.5 text-accent" />
<span>Einladungscode generieren</span> <span>Einladungscode generieren</span>
@@ -646,29 +644,29 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
{/* Active Invites List */} {/* Active Invites List */}
{invites.length > 0 && ( {invites.length > 0 && (
<div className="pt-2 border-t border-border space-y-1.5"> <div className="pt-2 border-t border-border space-y-1.5">
<p className="text-[11px] font-semibold text-zinc-400"> <p className="text-[11px] font-semibold text-muted">
Aktive & bisherige Einladungscodes: Aktive & bisherige Einladungscodes:
</p> </p>
<div className="divide-y divide-border border border-border bg-panel"> <div className="divide-y divide-border border border-border bg-panel rounded-card overflow-hidden">
{invites.map((inv) => ( {invites.map((inv) => (
<div <div
key={inv.id} key={inv.id}
className="p-2 flex flex-wrap items-center justify-between gap-2" className="p-2 flex flex-wrap items-center justify-between gap-2"
> >
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<code className="px-2 py-0.5 bg-canvas border border-border font-mono font-bold text-accent"> <code className="px-2 py-0.5 bg-canvas border border-border font-mono font-bold text-accent rounded-card">
{inv.code} {inv.code}
</code> </code>
<span className="text-zinc-300"> <span className="text-muted">
Rolle: <strong>{inv.role}</strong> Rolle: <strong className="text-main">{inv.role}</strong>
{inv.email ? ` · Für: ${inv.email}` : ''} {inv.email ? ` · Für: ${inv.email}` : ''}
</span> </span>
{inv.usedBy ? ( {inv.usedBy ? (
<span className="px-1.5 py-0.5 bg-zinc-800 text-zinc-400 text-[10px]"> <span className="badge-neutral text-[10px]">
Eingelöst Eingelöst
</span> </span>
) : ( ) : (
<span className="px-1.5 py-0.5 bg-emerald-950/60 border border-emerald-500/40 text-emerald-300 text-[10px]"> <span className="badge-ok text-[10px]">
Offen Offen
</span> </span>
)} )}
@@ -701,23 +699,23 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
{/* Users List */} {/* Users List */}
<div className="space-y-2"> <div className="space-y-2">
<h3 className="font-bold text-white"> <h3 className="font-bold text-main">
Registrierte Benutzer ({users.length}) Registrierte Benutzer ({users.length})
</h3> </h3>
<div className="border border-border divide-y divide-border bg-canvas"> <div className="border border-border divide-y divide-border bg-canvas rounded-card overflow-hidden">
{users.map((u) => ( {users.map((u) => (
<div key={u.id} className="p-3 space-y-2"> <div key={u.id} className="p-3 space-y-2">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-bold text-white">{u.displayName}</span> <span className="font-bold text-main">{u.displayName}</span>
{!u.isActive && ( {!u.isActive && (
<span className="px-1.5 py-0.5 bg-red-950 text-red-300 border border-red-500/40 text-[10px]"> <span className="badge-err text-[10px]">
Gesperrt Gesperrt
</span> </span>
)} )}
</div> </div>
<p className="text-zinc-400 text-[11px]">{u.email}</p> <p className="text-muted text-[11px]">{u.email}</p>
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
@@ -726,7 +724,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
value={u.role} value={u.role}
disabled={!isServerAdmin && u.role === 'admin'} disabled={!isServerAdmin && u.role === 'admin'}
onChange={(e) => handleChangeUserRole(u, e.target.value as UserRole)} onChange={(e) => handleChangeUserRole(u, e.target.value as UserRole)}
className="h-7 px-2 input-field text-[11px] text-white bg-panel" className="h-7 px-2 input-field text-[11px] bg-panel"
title="Benutzerrolle ändern" title="Benutzerrolle ändern"
> >
<option value="member">Mitglied</option> <option value="member">Mitglied</option>
@@ -775,7 +773,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
value={resetPwdVal} value={resetPwdVal}
onChange={(e) => setResetPwdVal(e.target.value)} onChange={(e) => setResetPwdVal(e.target.value)}
placeholder="Neues Passwort eingeben…" placeholder="Neues Passwort eingeben…"
className="flex-1 h-7 px-2 input-field text-white" className="flex-1 h-7 px-2 input-field"
/> />
<button <button
onClick={() => handleConfirmResetPassword(u.id)} onClick={() => handleConfirmResetPassword(u.id)}
@@ -804,8 +802,8 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
{section === 'orgs' && ( {section === 'orgs' && (
<div className="space-y-6"> <div className="space-y-6">
<div className="border-b border-border pb-3"> <div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-white">Organisationen & Teams verwalten</h2> <h2 className="text-sm font-bold text-main">Organisationen & Teams verwalten</h2>
<p className="text-zinc-400 mt-0.5"> <p className="text-muted mt-0.5">
Erstelle Organisationen und unterteile sie in Teams. Weise Benutzern Teams zu, damit Erstelle Organisationen und unterteile sie in Teams. Weise Benutzern Teams zu, damit
sie im Modus &quot;Teams isoliert&quot; gemeinsam an Listen und Aufgaben arbeiten können. sie im Modus &quot;Teams isoliert&quot; gemeinsam an Listen und Aufgaben arbeiten können.
</p> </p>
@@ -813,8 +811,8 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
{/* Create Organization & Team */} {/* Create Organization & Team */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="p-4 bg-canvas border border-border space-y-3"> <div className="p-4 bg-canvas border border-border rounded-card space-y-3">
<h3 className="font-bold text-white">1. Neue Organisation gründen</h3> <h3 className="font-bold text-main">1. Neue Organisation gründen</h3>
<form onSubmit={handleCreateOrg} className="flex gap-2"> <form onSubmit={handleCreateOrg} className="flex gap-2">
<input <input
type="text" type="text"
@@ -822,7 +820,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
onChange={(e) => setNewOrgName(e.target.value)} onChange={(e) => setNewOrgName(e.target.value)}
placeholder="z.B. Homelab, Familie, Firma…" placeholder="z.B. Homelab, Familie, Firma…"
required required
className="flex-1 min-w-0 h-8 px-2.5 input-field text-white" className="flex-1 min-w-0 h-8 px-2.5 input-field"
/> />
<button type="submit" className="btn-primary h-8 px-3 text-xs flex-shrink-0"> <button type="submit" className="btn-primary h-8 px-3 text-xs flex-shrink-0">
Anlegen Anlegen
@@ -830,10 +828,10 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
</form> </form>
</div> </div>
<div className="p-4 bg-canvas border border-border space-y-3"> <div className="p-4 bg-canvas border border-border rounded-card space-y-3">
<h3 className="font-bold text-white">2. Neues Team in Organisation anlegen</h3> <h3 className="font-bold text-main">2. Neues Team in Organisation anlegen</h3>
{orgs.length === 0 ? ( {orgs.length === 0 ? (
<p className="text-zinc-500"> <p className="text-faint">
Bitte lege zuerst links eine Organisation an. Bitte lege zuerst links eine Organisation an.
</p> </p>
) : ( ) : (
@@ -842,7 +840,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<select <select
value={selectedOrgId} value={selectedOrgId}
onChange={(e) => setSelectedOrgId(e.target.value)} onChange={(e) => setSelectedOrgId(e.target.value)}
className="w-full min-w-0 h-8 px-2 input-field text-white bg-panel" className="w-full min-w-0 h-8 px-2 input-field bg-panel"
> >
{orgs.map((o) => ( {orgs.map((o) => (
<option key={o.id} value={o.id}> <option key={o.id} value={o.id}>
@@ -856,7 +854,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
onChange={(e) => setNewTeamName(e.target.value)} onChange={(e) => setNewTeamName(e.target.value)}
placeholder="Team-Name (z.B. Infrastruktur)" placeholder="Team-Name (z.B. Infrastruktur)"
required required
className="w-full min-w-0 h-8 px-2.5 input-field text-white" className="w-full min-w-0 h-8 px-2.5 input-field"
/> />
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -865,7 +863,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
value={newTeamDesc} value={newTeamDesc}
onChange={(e) => setNewTeamDesc(e.target.value)} onChange={(e) => setNewTeamDesc(e.target.value)}
placeholder="Kurzbeschreibung (optional)…" placeholder="Kurzbeschreibung (optional)…"
className="flex-1 min-w-0 h-8 px-2.5 input-field text-white" className="flex-1 min-w-0 h-8 px-2.5 input-field"
/> />
<button type="submit" className="btn-primary h-8 px-3 text-xs flex-shrink-0"> <button type="submit" className="btn-primary h-8 px-3 text-xs flex-shrink-0">
Team erstellen Team erstellen
@@ -879,19 +877,19 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
{/* Organizations & Nested Teams Display */} {/* Organizations & Nested Teams Display */}
<div className="space-y-4"> <div className="space-y-4">
{orgs.length === 0 ? ( {orgs.length === 0 ? (
<div className="p-6 bg-canvas border border-border text-center text-zinc-500"> <div className="p-6 bg-canvas border border-border rounded-card text-center text-faint">
Noch keine Organisationen vorhanden. Noch keine Organisationen vorhanden.
</div> </div>
) : ( ) : (
orgs.map((org) => { orgs.map((org) => {
const orgTeams = Array.isArray(org.teams) ? org.teams : []; const orgTeams = Array.isArray(org.teams) ? org.teams : [];
return ( return (
<div key={org.id} className="bg-canvas border border-border"> <div key={org.id} className="bg-canvas border border-border rounded-card overflow-hidden">
<div className="p-3 bg-panel-elevated border-b border-border flex items-center justify-between"> <div className="p-3 bg-panel-elevated border-b border-border flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-accent" /> <Building2 className="w-4 h-4 text-accent" />
<span className="font-bold text-sm text-white">{org.name}</span> <span className="font-bold text-sm text-main">{org.name}</span>
<span className="text-[11px] text-zinc-400"> <span className="text-[11px] text-muted">
({orgTeams.length} {orgTeams.length === 1 ? 'Team' : 'Teams'}) ({orgTeams.length} {orgTeams.length === 1 ? 'Team' : 'Teams'})
</span> </span>
</div> </div>
@@ -906,7 +904,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<div className="p-3 space-y-3"> <div className="p-3 space-y-3">
{orgTeams.length === 0 ? ( {orgTeams.length === 0 ? (
<p className="text-zinc-500 text-xs"> <p className="text-faint text-xs">
Noch keine Teams in dieser Organisation. Noch keine Teams in dieser Organisation.
</p> </p>
) : ( ) : (
@@ -915,16 +913,16 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
return ( return (
<div <div
key={team.id} key={team.id}
className="p-3 bg-panel border border-border space-y-3" className="p-3 bg-panel border border-border rounded-card space-y-3"
> >
<div className="flex items-center justify-between border-b border-border pb-2"> <div className="flex items-center justify-between border-b border-border pb-2">
<div> <div>
<span className="font-bold text-white flex items-center gap-1.5"> <span className="font-bold text-main flex items-center gap-1.5">
<Users className="w-3.5 h-3.5 text-accent" /> <Users className="w-3.5 h-3.5 text-accent" />
<span>{team.name}</span> <span>{team.name}</span>
</span> </span>
{team.description && ( {team.description && (
<p className="text-[11px] text-zinc-400 mt-0.5"> <p className="text-[11px] text-muted mt-0.5">
{team.description} {team.description}
</p> </p>
)} )}
@@ -940,11 +938,11 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
{/* Team Members */} {/* Team Members */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<p className="text-[11px] font-semibold text-zinc-400"> <p className="text-[11px] font-semibold text-muted">
Mitglieder ({members.length}): Mitglieder ({members.length}):
</p> </p>
{members.length === 0 ? ( {members.length === 0 ? (
<p className="text-[11px] text-zinc-500"> <p className="text-[11px] text-faint">
Noch keine Mitglieder zugewiesen. Noch keine Mitglieder zugewiesen.
</p> </p>
) : ( ) : (
@@ -952,19 +950,19 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
{members.map((m) => ( {members.map((m) => (
<div <div
key={m.userId} key={m.userId}
className="px-2 py-1 bg-canvas border border-border flex items-center gap-2" className="px-2 py-1 bg-canvas border border-border rounded-btn flex items-center gap-2"
> >
<span className="text-white font-medium"> <span className="text-main font-medium">
{m.displayName} {m.displayName}
</span> </span>
<span className="text-[10px] text-zinc-400"> <span className="text-[10px] text-muted">
({m.role}) ({m.role})
</span> </span>
<button <button
onClick={() => onClick={() =>
handleRemoveMemberFromTeam(team.id, m.userId) handleRemoveMemberFromTeam(team.id, m.userId)
} }
className="text-zinc-500 hover:text-red-400" className="text-faint hover:text-status-err"
title="Aus Team entfernen" title="Aus Team entfernen"
> >
× ×
@@ -985,7 +983,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
[team.id]: e.target.value, [team.id]: e.target.value,
})) }))
} }
className="flex-1 min-w-[160px] h-7 px-2 input-field text-[11px] text-white bg-canvas" className="flex-1 min-w-[160px] h-7 px-2 input-field text-[11px] bg-canvas"
> >
<option value="">Benutzer auswählen…</option> <option value="">Benutzer auswählen…</option>
{users.map((u) => ( {users.map((u) => (
@@ -1003,7 +1001,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
[team.id]: e.target.value, [team.id]: e.target.value,
})) }))
} }
className="h-7 px-2 input-field text-[11px] text-white bg-canvas" className="h-7 px-2 input-field text-[11px] bg-canvas"
> >
<option value="member">Mitglied</option> <option value="member">Mitglied</option>
<option value="team_admin">Team-Admin</option> <option value="team_admin">Team-Admin</option>
@@ -1037,10 +1035,10 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
{section === 'server' && isServerAdmin && ( {section === 'server' && isServerAdmin && (
<div className="space-y-6"> <div className="space-y-6">
<div className="border-b border-border pb-3"> <div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-white"> <h2 className="text-sm font-bold text-main">
Server-Grundeinstellungen & Benutzer-Sichtbarkeit Server-Grundeinstellungen & Benutzer-Sichtbarkeit
</h2> </h2>
<p className="text-zinc-400 mt-0.5"> <p className="text-muted mt-0.5">
Als Server-Admin bestimmst du zentral für alle Nutzer, ob sie komplett isoliert Als Server-Admin bestimmst du zentral für alle Nutzer, ob sie komplett isoliert
arbeiten, sich nur innerhalb ihrer Teams sehen oder ob alle Nutzer für alle sichtbar sind. arbeiten, sich nur innerhalb ihrer Teams sehen oder ob alle Nutzer für alle sichtbar sind.
</p> </p>
@@ -1048,7 +1046,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-zinc-300 font-semibold mb-1"> <label className="block text-muted font-semibold mb-1">
Name der MindSift-Instanz Name der MindSift-Instanz
</label> </label>
<div className="flex gap-2 max-w-md"> <div className="flex gap-2 max-w-md">
@@ -1058,7 +1056,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
onChange={(e) => onChange={(e) =>
setSettings((prev) => ({ ...prev, instanceName: e.target.value })) setSettings((prev) => ({ ...prev, instanceName: e.target.value }))
} }
className="flex-1 h-9 px-3 input-field text-white" className="flex-1 h-9 px-3 input-field"
/> />
<button <button
type="button" type="button"
@@ -1072,7 +1070,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
{/* Sharing Isolation Cards */} {/* Sharing Isolation Cards */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-zinc-300 font-semibold flex items-center gap-1.5"> <label className="block text-muted font-semibold flex items-center gap-1.5">
<Share2 className="w-4 h-4 text-accent" /> <Share2 className="w-4 h-4 text-accent" />
<span>Globale Sharing- & Sichtbarkeits-Isolation</span> <span>Globale Sharing- & Sichtbarkeits-Isolation</span>
</label> </label>
@@ -1081,14 +1079,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<button <button
type="button" type="button"
onClick={() => handleSaveSystemSettings({ sharingMode: 'strict_private' })} onClick={() => handleSaveSystemSettings({ sharingMode: 'strict_private' })}
className={`p-4 text-left border transition-colors ${ className={`p-4 text-left border rounded-card transition-colors ${
settings.sharingMode === 'strict_private' settings.sharingMode === 'strict_private'
? 'bg-canvas border-accent text-white' ? 'bg-canvas border-accent text-main shadow-panel'
: 'bg-canvas/50 border-border text-zinc-400 hover:text-white' : 'bg-canvas/50 border-border text-muted hover:text-main'
}`} }`}
> >
<p className="font-bold text-xs text-white">1. Strikt Privat (Keiner sieht andere)</p> <p className="font-bold text-xs text-main">1. Strikt Privat (Keiner sieht andere)</p>
<p className="text-[11px] text-zinc-400 mt-1 leading-relaxed"> <p className="text-[11px] text-muted mt-1 leading-relaxed">
Jeder Benutzer arbeitet komplett für sich allein. Keine geteilten Spaces und keine Jeder Benutzer arbeitet komplett für sich allein. Keine geteilten Spaces und keine
Benutzersuche möglich. Benutzersuche möglich.
</p> </p>
@@ -1097,14 +1095,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<button <button
type="button" type="button"
onClick={() => handleSaveSystemSettings({ sharingMode: 'teams_only' })} onClick={() => handleSaveSystemSettings({ sharingMode: 'teams_only' })}
className={`p-4 text-left border transition-colors ${ className={`p-4 text-left border rounded-card transition-colors ${
settings.sharingMode === 'teams_only' settings.sharingMode === 'teams_only'
? 'bg-canvas border-accent text-white' ? 'bg-canvas border-accent text-main shadow-panel'
: 'bg-canvas/50 border-border text-zinc-400 hover:text-white' : 'bg-canvas/50 border-border text-muted hover:text-main'
}`} }`}
> >
<p className="font-bold text-xs text-white">2. Teams isoliert (Empfohlen)</p> <p className="font-bold text-xs text-main">2. Teams isoliert (Empfohlen)</p>
<p className="text-[11px] text-zinc-400 mt-1 leading-relaxed"> <p className="text-[11px] text-muted mt-1 leading-relaxed">
Benutzer sehen ausschließlich Personen, mit denen sie mindestens ein gemeinsames Team Benutzer sehen ausschließlich Personen, mit denen sie mindestens ein gemeinsames Team
teilen. teilen.
</p> </p>
@@ -1113,14 +1111,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<button <button
type="button" type="button"
onClick={() => handleSaveSystemSettings({ sharingMode: 'open' })} onClick={() => handleSaveSystemSettings({ sharingMode: 'open' })}
className={`p-4 text-left border transition-colors ${ className={`p-4 text-left border rounded-card transition-colors ${
settings.sharingMode === 'open' settings.sharingMode === 'open'
? 'bg-canvas border-accent text-white' ? 'bg-canvas border-accent text-main shadow-panel'
: 'bg-canvas/50 border-border text-zinc-400 hover:text-white' : 'bg-canvas/50 border-border text-muted hover:text-main'
}`} }`}
> >
<p className="font-bold text-xs text-white">3. Offen (Alle sehen alle)</p> <p className="font-bold text-xs text-main">3. Offen (Alle sehen alle)</p>
<p className="text-[11px] text-zinc-400 mt-1 leading-relaxed"> <p className="text-[11px] text-muted mt-1 leading-relaxed">
Alle registrierten Benutzer dieser Instanz können sich gegenseitig sehen und Spaces Alle registrierten Benutzer dieser Instanz können sich gegenseitig sehen und Spaces
miteinander teilen. miteinander teilen.
</p> </p>
@@ -1129,10 +1127,10 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
</div> </div>
{/* Public Registration Toggle */} {/* Public Registration Toggle */}
<div className="p-4 bg-canvas border border-border flex items-center justify-between gap-4"> <div className="p-4 bg-canvas border border-border rounded-card flex items-center justify-between gap-4">
<div> <div>
<p className="font-bold text-white">Öffentliche Selbstregistrierung erlauben</p> <p className="font-bold text-main">Öffentliche Selbstregistrierung erlauben</p>
<p className="text-[11px] text-zinc-400 mt-0.5"> <p className="text-[11px] text-muted mt-0.5">
Wenn deaktiviert, können sich Fremde auf dem Login-Bildschirm nicht registrieren. Wenn deaktiviert, können sich Fremde auf dem Login-Bildschirm nicht registrieren.
Neue Benutzer benötigen zwingend einen Einladungscode oder müssen von einem Admin Neue Benutzer benötigen zwingend einen Einladungscode oder müssen von einem Admin
angelegt werden. angelegt werden.
@@ -1144,10 +1142,10 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
onClick={() => onClick={() =>
handleSaveSystemSettings({ allowRegistration: !settings.allowRegistration }) handleSaveSystemSettings({ allowRegistration: !settings.allowRegistration })
} }
className={`h-8 px-4 text-xs font-semibold border flex-shrink-0 ${ className={`h-8 px-4 text-xs font-semibold border rounded-btn flex-shrink-0 transition-colors ${
settings.allowRegistration settings.allowRegistration
? 'bg-accent/20 border-accent text-accent' ? 'bg-accent/20 border-accent text-accent'
: 'bg-panel border-border text-zinc-400' : 'bg-panel border-border text-muted hover:text-main'
}`} }`}
> >
{settings.allowRegistration ? 'Erlaubt (Offen)' : 'Gesperrt (Nur mit Einladung)'} {settings.allowRegistration ? 'Erlaubt (Offen)' : 'Gesperrt (Nur mit Einladung)'}
@@ -1164,17 +1162,17 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<div className="space-y-6"> <div className="space-y-6">
<div className="border-b border-border pb-3 flex flex-col sm:flex-row sm:items-center justify-between gap-2"> <div className="border-b border-border pb-3 flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div> <div>
<h2 className="text-sm font-bold text-white"> <h2 className="text-sm font-bold text-main">
E-Mail / SMTP-Server für Aufgaben-Alarme E-Mail / SMTP-Server für Aufgaben-Alarme
</h2> </h2>
<p className="text-zinc-400 mt-0.5"> <p className="text-muted mt-0.5">
Konfiguriere den Postausgangsserver per Docker-Compose Environment-Variablen oder Konfiguriere den Postausgangsserver per Docker-Compose Environment-Variablen oder
direkt hier über die Oberfläche (wird persistent in der SQLite-Datenbank gespeichert). direkt hier über die Oberfläche (wird persistent in der SQLite-Datenbank gespeichert).
</p> </p>
</div> </div>
{settings.smtpConfiguredViaEnv && ( {settings.smtpConfiguredViaEnv && (
<span className="px-2.5 py-1 bg-emerald-950/50 border border-emerald-500/40 text-emerald-300 text-[11px] flex-shrink-0"> <span className="badge-ok text-[11px] flex-shrink-0">
Docker Env erkannt Docker Env erkannt
</span> </span>
)} )}
@@ -1182,7 +1180,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="block text-zinc-300 font-medium mb-1"> <label className="block text-muted font-medium mb-1">
SMTP Host (`SMTP_HOST`) SMTP Host (`SMTP_HOST`)
</label> </label>
<input <input
@@ -1191,13 +1189,13 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
onChange={(e) => onChange={(e) =>
setSettings((prev) => ({ ...prev, smtpHost: e.target.value })) setSettings((prev) => ({ ...prev, smtpHost: e.target.value }))
} }
placeholder="z.B. smtp.gmail.com oder mail.deinedomain.de" placeholder="z.B. smtp.example.com oder mail.deinedomain.de"
className="w-full h-9 px-3 input-field text-white font-mono" className="w-full h-9 px-3 input-field font-mono"
/> />
</div> </div>
<div> <div>
<label className="block text-zinc-300 font-medium mb-1"> <label className="block text-muted font-medium mb-1">
SMTP Port (`SMTP_PORT`) SMTP Port (`SMTP_PORT`)
</label> </label>
<input <input
@@ -1207,12 +1205,12 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
setSettings((prev) => ({ ...prev, smtpPort: Number(e.target.value) })) setSettings((prev) => ({ ...prev, smtpPort: Number(e.target.value) }))
} }
placeholder="587" placeholder="587"
className="w-full h-9 px-3 input-field text-white font-mono" className="w-full h-9 px-3 input-field font-mono"
/> />
</div> </div>
<div> <div>
<label className="block text-zinc-300 font-medium mb-1"> <label className="block text-muted font-medium mb-1">
SMTP Benutzername (`SMTP_USER`) SMTP Benutzername (`SMTP_USER`)
</label> </label>
<input <input
@@ -1222,12 +1220,12 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
setSettings((prev) => ({ ...prev, smtpUser: e.target.value })) setSettings((prev) => ({ ...prev, smtpUser: e.target.value }))
} }
placeholder="alerts@deinedomain.de" placeholder="alerts@deinedomain.de"
className="w-full h-9 px-3 input-field text-white font-mono" className="w-full h-9 px-3 input-field font-mono"
/> />
</div> </div>
<div> <div>
<label className="block text-zinc-300 font-medium mb-1"> <label className="block text-muted font-medium mb-1">
SMTP Passwort (`SMTP_PASS`) SMTP Passwort (`SMTP_PASS`)
</label> </label>
<input <input
@@ -1237,12 +1235,12 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
setSettings((prev) => ({ ...prev, smtpPass: e.target.value })) setSettings((prev) => ({ ...prev, smtpPass: e.target.value }))
} }
placeholder="••••••••••••" placeholder="••••••••••••"
className="w-full h-9 px-3 input-field text-white font-mono" className="w-full h-9 px-3 input-field font-mono"
/> />
</div> </div>
<div> <div>
<label className="block text-zinc-300 font-medium mb-1"> <label className="block text-muted font-medium mb-1">
Absender-Adresse (`SMTP_FROM`) Absender-Adresse (`SMTP_FROM`)
</label> </label>
<input <input
@@ -1252,7 +1250,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
setSettings((prev) => ({ ...prev, smtpFrom: e.target.value })) setSettings((prev) => ({ ...prev, smtpFrom: e.target.value }))
} }
placeholder="MindSift <alerts@deinedomain.de>" placeholder="MindSift <alerts@deinedomain.de>"
className="w-full h-9 px-3 input-field text-white" className="w-full h-9 px-3 input-field"
/> />
</div> </div>
@@ -1264,9 +1262,9 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
onChange={(e) => onChange={(e) =>
setSettings((prev) => ({ ...prev, smtpTls: e.target.checked })) setSettings((prev) => ({ ...prev, smtpTls: e.target.checked }))
} }
className="w-4 h-4 accent-blue-600" className="w-4 h-4 checkbox-square cursor-pointer"
/> />
<span className="text-white font-medium"> <span className="text-main font-medium">
STARTTLS / TLS-Verschlüsselung verwenden (`SMTP_TLS=true`) STARTTLS / TLS-Verschlüsselung verwenden (`SMTP_TLS=true`)
</span> </span>
</label> </label>
@@ -1284,9 +1282,9 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
</div> </div>
{/* Test SMTP Delivery */} {/* Test SMTP Delivery */}
<div className="p-4 bg-canvas border border-border space-y-2"> <div className="p-4 bg-canvas border border-border rounded-card space-y-2">
<h3 className="font-bold text-white">SMTP-Verbindung testen</h3> <h3 className="font-bold text-main">SMTP-Verbindung testen</h3>
<p className="text-[11px] text-zinc-400"> <p className="text-[11px] text-muted">
Sendet sofort eine Test-E-Mail mit den oben eingetragenen Daten. Sendet sofort eine Test-E-Mail mit den oben eingetragenen Daten.
</p> </p>
<div className="flex flex-col sm:flex-row gap-2"> <div className="flex flex-col sm:flex-row gap-2">
@@ -1295,7 +1293,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
value={testSmtpEmail} value={testSmtpEmail}
onChange={(e) => setTestSmtpEmail(e.target.value)} onChange={(e) => setTestSmtpEmail(e.target.value)}
placeholder="empfaenger@beispiel.de" placeholder="empfaenger@beispiel.de"
className="flex-1 h-8 px-3 input-field text-white" className="flex-1 h-8 px-3 input-field"
/> />
<button <button
type="button" type="button"
@@ -14,20 +14,26 @@ import {
Globe, Globe,
Send, Send,
AlertCircle, AlertCircle,
Palette,
} from 'lucide-react'; } from 'lucide-react';
import { APIKey, User } from '../../lib/types'; import { APIKey, User } from '../../lib/types';
import { api, triggerHaptic } from '../../lib/api'; import { api, triggerHaptic } from '../../lib/api';
import { AVAILABLE_THEMES, ThemeId } from '../../lib/theme';
interface UserSettingsViewProps { interface UserSettingsViewProps {
currentUser: User; currentUser: User;
activeTheme: ThemeId;
onThemeChange: (themeId: ThemeId) => void;
onUserUpdated: (u: User) => void; onUserUpdated: (u: User) => void;
onBack: () => void; onBack: () => void;
} }
type UserSettingsSection = 'profile' | 'alarms' | 'webhooks'; type UserSettingsSection = 'profile' | 'appearance' | 'alarms' | 'webhooks';
export const UserSettingsView: React.FC<UserSettingsViewProps> = ({ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
currentUser, currentUser,
activeTheme,
onThemeChange,
onUserUpdated, onUserUpdated,
onBack, onBack,
}) => { }) => {
@@ -219,7 +225,8 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
}; };
const baseUrl = window.location.origin; const baseUrl = window.location.origin;
const activeKeyPlaceholder = newlyCreatedKey || apiKeys[0]?.keyPrefix + '••••••••' || 'DEIN_API_KEY'; const activeKeyPlaceholder =
newlyCreatedKey || (apiKeys[0]?.keyPrefix ? apiKeys[0].keyPrefix + '••••••••' : 'DEIN_API_KEY');
const curlTaskExample = `curl -X POST ${baseUrl}/api/v1/inbox \\ const curlTaskExample = `curl -X POST ${baseUrl}/api/v1/inbox \\
-H "X-API-Key: ${activeKeyPlaceholder}" \\ -H "X-API-Key: ${activeKeyPlaceholder}" \\
@@ -247,14 +254,14 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
<span>Zurück</span> <span>Zurück</span>
</button> </button>
<div> <div>
<h1 className="text-base font-bold text-white tracking-tight">Kontoeinstellungen</h1> <h1 className="text-base font-bold text-main tracking-tight">Kontoeinstellungen</h1>
<p className="text-xs text-zinc-400"> <p className="text-xs text-muted">
Persönliches Profil, Aufgaben-Alarme, Private Webhooks & Daten-Backup Persönliches Profil, Design-Themes, Aufgaben-Alarme, Private Webhooks & Daten-Backup
</p> </p>
</div> </div>
</div> </div>
<span className="px-2.5 py-1 bg-panel border border-border text-xs text-zinc-300 font-medium"> <span className="px-2.5 py-1 bg-panel border border-border text-xs text-muted font-medium">
Rolle: <strong className="text-accent">{roleBadgeLabel}</strong> Rolle: <strong className="text-accent">{roleBadgeLabel}</strong>
</span> </span>
</div> </div>
@@ -262,19 +269,34 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
{/* Main 2-Column Layout */} {/* Main 2-Column Layout */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-start"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-start">
{/* Left Navigation Sidebar */} {/* Left Navigation Sidebar */}
<aside className="bg-panel border border-border divide-y divide-border"> <aside className="panel-surface divide-y divide-border overflow-hidden">
<button <button
onClick={() => setSection('profile')} onClick={() => setSection('profile')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${ className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'profile' section === 'profile'
? 'bg-panel-elevated text-white border-l-2 border-l-accent' ? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]' : 'text-muted hover:text-main hover:bg-panel-hover'
}`} }`}
> >
<UserIcon className="w-4 h-4 text-accent flex-shrink-0" /> <UserIcon className="w-4 h-4 text-accent flex-shrink-0" />
<div> <div>
<p className="font-semibold">Profil & Sicherheit</p> <p className="font-semibold">Profil & Sicherheit</p>
<p className="text-[10px] text-zinc-500">Name, E-Mail & Passwort</p> <p className="text-[10px] text-faint">Name, E-Mail & Passwort</p>
</div>
</button>
<button
onClick={() => setSection('appearance')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'appearance'
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-muted hover:text-main hover:bg-panel-hover'
}`}
>
<Palette className="w-4 h-4 text-accent flex-shrink-0" />
<div>
<p className="font-semibold">Darstellung & Themes</p>
<p className="text-[10px] text-faint">Farben, Kontraste & Rundungen</p>
</div> </div>
</button> </button>
@@ -282,14 +304,14 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
onClick={() => setSection('alarms')} onClick={() => setSection('alarms')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${ className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'alarms' section === 'alarms'
? 'bg-panel-elevated text-white border-l-2 border-l-accent' ? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]' : 'text-muted hover:text-main hover:bg-panel-hover'
}`} }`}
> >
<Bell className="w-4 h-4 text-accent flex-shrink-0" /> <Bell className="w-4 h-4 text-accent flex-shrink-0" />
<div> <div>
<p className="font-semibold">Alarme</p> <p className="font-semibold">Alarme</p>
<p className="text-[10px] text-zinc-500">E-Mail, Discord & Webhooks</p> <p className="text-[10px] text-faint">E-Mail, Discord & Webhooks</p>
</div> </div>
</button> </button>
@@ -297,44 +319,42 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
onClick={() => setSection('webhooks')} onClick={() => setSection('webhooks')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${ className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'webhooks' section === 'webhooks'
? 'bg-panel-elevated text-white border-l-2 border-l-accent' ? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]' : 'text-muted hover:text-main hover:bg-panel-hover'
}`} }`}
> >
<Key className="w-4 h-4 text-accent flex-shrink-0" /> <Key className="w-4 h-4 text-accent flex-shrink-0" />
<div> <div>
<p className="font-semibold">Private Webhooks & Export</p> <p className="font-semibold">Private Webhooks & Export</p>
<p className="text-[10px] text-zinc-500">API-Keys, Curl & JSON-Backup</p> <p className="text-[10px] text-faint">API-Keys, Curl & JSON-Backup</p>
</div> </div>
</button> </button>
</aside> </aside>
{/* Right Content Area */} {/* Right Content Area */}
<div className="md:col-span-3 bg-panel border border-border p-5 text-xs"> <div className="md:col-span-3 panel-surface p-5 text-xs">
{/* ================================================================= */} {/* ================================================================= */}
{/* SECTION 1: PROFIL & SICHERHEIT */} {/* SECTION 1: PROFIL & SICHERHEIT */}
{/* ================================================================= */} {/* ================================================================= */}
{section === 'profile' && ( {section === 'profile' && (
<div className="space-y-5 max-w-2xl"> <div className="space-y-5 max-w-2xl">
<div className="border-b border-border pb-3"> <div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-white">Profil & Anmeldedaten</h2> <h2 className="text-sm font-bold text-main">Profil & Anmeldedaten</h2>
<p className="text-zinc-400 mt-0.5"> <p className="text-muted mt-0.5">
Verwalte deinen Anzeigenamen, deine E-Mail-Adresse und dein persönliches Passwort. Verwalte deinen Anzeigenamen, deine E-Mail-Adresse und dein persönliches Passwort.
</p> </p>
</div> </div>
{profileMsg && ( {profileMsg && (
<div <div
className={`p-3 border flex items-center gap-2 ${ className={`flex items-center gap-2 ${
profileMsg.type === 'ok' profileMsg.type === 'ok' ? 'alert-ok' : 'alert-err'
? 'bg-blue-950/30 border-accent text-blue-200'
: 'bg-red-950/30 border-red-500 text-red-200'
}`} }`}
> >
{profileMsg.type === 'ok' ? ( {profileMsg.type === 'ok' ? (
<Check className="w-4 h-4 text-accent flex-shrink-0" /> <Check className="w-4 h-4 text-status-ok flex-shrink-0" />
) : ( ) : (
<AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" /> <AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
)} )}
<span>{profileMsg.text}</span> <span>{profileMsg.text}</span>
</div> </div>
@@ -343,30 +363,32 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
<form onSubmit={handleSaveProfile} className="space-y-4"> <form onSubmit={handleSaveProfile} className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="block text-zinc-300 font-medium mb-1">Anzeigename</label> <label className="block text-muted font-medium mb-1">Anzeigename</label>
<input <input
type="text" type="text"
value={displayName} value={displayName}
onChange={(e) => setDisplayName(e.target.value)} onChange={(e) => setDisplayName(e.target.value)}
required required
className="w-full h-9 px-3 input-field text-white" className="w-full h-9 px-3 input-field"
/> />
</div> </div>
<div> <div>
<label className="block text-zinc-300 font-medium mb-1">E-Mail-Adresse (Login)</label> <label className="block text-muted font-medium mb-1">
E-Mail-Adresse (Login)
</label>
<input <input
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
className="w-full h-9 px-3 input-field text-white" className="w-full h-9 px-3 input-field"
/> />
</div> </div>
</div> </div>
<div> <div>
<label className="block text-zinc-300 font-medium mb-1"> <label className="block text-muted font-medium mb-1">
Neues Passwort (optional — leer lassen für keine Änderung) Neues Passwort (optional — leer lassen für keine Änderung)
</label> </label>
<input <input
@@ -374,7 +396,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
value={newPassword} value={newPassword}
onChange={(e) => setNewPassword(e.target.value)} onChange={(e) => setNewPassword(e.target.value)}
placeholder="Mindestens 4 Zeichen" placeholder="Mindestens 4 Zeichen"
className="w-full h-9 px-3 input-field text-white" className="w-full h-9 px-3 input-field"
/> />
</div> </div>
@@ -392,30 +414,129 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
)} )}
{/* ================================================================= */} {/* ================================================================= */}
{/* SECTION 2: ALARME (MULTI-CHANNEL: EMAIL, DISCORD, CUSTOM WEBHOOK) */} {/* SECTION 2: DARSTELLUNG & THEMES (SINGLE-FILE CSS THEMING) */}
{/* ================================================================= */}
{section === 'appearance' && (
<div className="space-y-5">
<div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-main">
Darstellung & Single-File CSS Themes
</h2>
<p className="text-muted mt-0.5">
Jedes Theme ist eine einzige, eigenständige CSS-Datei, in der alle Farben,
Oberflächen, Kontraste und Ecken-Rundungen in 6 festen Gruppen gebündelt sind.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{AVAILABLE_THEMES.map((t) => {
const isSelected = activeTheme === t.id;
return (
<button
key={t.id}
type="button"
onClick={() => onThemeChange(t.id)}
className={`p-4 text-left border transition-all flex flex-col justify-between gap-3 ${
isSelected
? 'bg-canvas border-accent shadow-sm'
: 'bg-canvas border-border hover:border-strong'
}`}
>
<div className="flex items-start justify-between gap-2">
<div>
<p className="font-bold text-sm text-main flex items-center gap-2">
<span>{t.name}</span>
{isSelected && <span className="badge-accent">Aktiv</span>}
</p>
<p className="text-xs text-muted mt-0.5">{t.subtitle}</p>
</div>
</div>
{/* Mini Color & Geometry Swatch */}
<div className="flex items-center justify-between pt-2 border-t border-border">
<div className="flex items-center gap-1.5">
<span
className="w-5 h-5 border border-strong inline-block"
style={{
backgroundColor: t.previewBg,
borderRadius: t.isRounded ? '6px' : '0px',
}}
title="Canvas-Hintergrund"
/>
<span
className="w-5 h-5 border border-strong inline-block"
style={{
backgroundColor: t.previewPanel,
borderRadius: t.isRounded ? '6px' : '0px',
}}
title="Panel-Oberfläche"
/>
<span
className="w-5 h-5 border border-strong inline-block"
style={{
backgroundColor: t.previewAccent,
borderRadius: t.isRounded ? '6px' : '0px',
}}
title="Akzentfarbe"
/>
<span
className="w-5 h-5 border border-strong inline-block"
style={{
backgroundColor: t.previewText,
borderRadius: t.isRounded ? '6px' : '0px',
}}
title="Textfarbe"
/>
</div>
<code className="text-[10px] font-mono text-faint">{t.file}</code>
</div>
</button>
);
})}
</div>
<div className="p-3.5 bg-canvas border border-border space-y-1.5">
<p className="font-semibold text-main">Eigenes Theme anpassen oder hinzufügen:</p>
<p className="text-muted text-[11px] leading-relaxed">
Alle Theme-Dateien liegen unter{' '}
<code className="font-mono text-accent">frontend/src/themes/</code> (inklusive der
Kopiervorlage{' '}
<code className="font-mono text-accent">theme-template.css</code>). Dort sind alle
Eigenschaften in 6 Blöcken gruppiert: <strong>1. Akzentfarbe</strong>,{' '}
<strong>2. Oberflächen</strong>, <strong>3. Textfarben</strong>,{' '}
<strong>4. Rahmen &amp; Schatten</strong>, <strong>5. Statusfarben</strong> und{' '}
<strong>6. Rundungen</strong>.
</p>
</div>
</div>
)}
{/* ================================================================= */}
{/* SECTION 3: ALARME (MULTI-CHANNEL: EMAIL, DISCORD, CUSTOM WEBHOOK) */}
{/* ================================================================= */} {/* ================================================================= */}
{section === 'alarms' && ( {section === 'alarms' && (
<div className="space-y-5"> <div className="space-y-5">
<div className="border-b border-border pb-3"> <div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-white">Aufgaben-Alarme & Benachrichtigungsziele</h2> <h2 className="text-sm font-bold text-main">
<p className="text-zinc-400 mt-0.5"> Aufgaben-Alarme & Benachrichtigungsziele
Wähle aus, über welche Kanäle du benachrichtigt werden möchtest, sobald eine Aufgabe </h2>
mit Fälligkeitsdatum ansteht. Mehrere Ziele können gleichzeitig aktiv sein. <p className="text-muted mt-0.5">
Wähle aus, über welche Kanäle du benachrichtigt werden möchtest, sobald eine
Aufgabe mit Fälligkeitsdatum ansteht. Mehrere Ziele können gleichzeitig aktiv sein.
</p> </p>
</div> </div>
{alarmMsg && ( {alarmMsg && (
<div <div
className={`p-3 border flex items-center gap-2 ${ className={`flex items-center gap-2 ${
alarmMsg.type === 'ok' alarmMsg.type === 'ok' ? 'alert-ok' : 'alert-err'
? 'bg-blue-950/30 border-accent text-blue-200'
: 'bg-red-950/30 border-red-500 text-red-200'
}`} }`}
> >
{alarmMsg.type === 'ok' ? ( {alarmMsg.type === 'ok' ? (
<Check className="w-4 h-4 text-accent flex-shrink-0" /> <Check className="w-4 h-4 text-status-ok flex-shrink-0" />
) : ( ) : (
<AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" /> <AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
)} )}
<span>{alarmMsg.text}</span> <span>{alarmMsg.text}</span>
</div> </div>
@@ -425,17 +546,17 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
{/* Lead Time Selector */} {/* Lead Time Selector */}
<div className="p-3.5 bg-canvas border border-border flex flex-col sm:flex-row sm:items-center justify-between gap-3"> <div className="p-3.5 bg-canvas border border-border flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div> <div>
<label className="font-semibold text-white block"> <label className="font-semibold text-main block">
Vorwarnzeit vor Fälligkeit Vorwarnzeit vor Fälligkeit
</label> </label>
<p className="text-[11px] text-zinc-400"> <p className="text-[11px] text-muted">
Wann soll MindSift den Alarm vor dem Fälligkeitszeitpunkt auslösen? Wann soll MindSift den Alarm vor dem Fälligkeitszeitpunkt auslösen?
</p> </p>
</div> </div>
<select <select
value={notifyBeforeMinutes} value={notifyBeforeMinutes}
onChange={(e) => setNotifyBeforeMinutes(Number(e.target.value))} onChange={(e) => setNotifyBeforeMinutes(Number(e.target.value))}
className="h-9 px-3 input-field text-white bg-panel min-w-[200px]" className="h-9 px-3 input-field min-w-[200px]"
> >
<option value={0}>Pünktlich bei Fälligkeit (0 Min.)</option> <option value={0}>Pünktlich bei Fälligkeit (0 Min.)</option>
<option value={15}>15 Minuten vorher</option> <option value={15}>15 Minuten vorher</option>
@@ -448,10 +569,8 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
{/* Channel 1: E-Mail */} {/* Channel 1: E-Mail */}
<div <div
className={`p-4 border transition-colors space-y-3 ${ className={`p-4 border transition-colors space-y-3 bg-canvas ${
alarmChannels.includes('email') alarmChannels.includes('email') ? 'border-accent' : 'border-border'
? 'bg-canvas border-accent'
: 'bg-canvas/60 border-border'
}`} }`}
> >
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
@@ -464,8 +583,8 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
/> />
<Mail className="w-4 h-4 text-accent" /> <Mail className="w-4 h-4 text-accent" />
<div> <div>
<span className="font-bold text-white block">Ziel: E-Mail</span> <span className="font-bold text-main block">Ziel: E-Mail</span>
<span className="text-[11px] text-zinc-400"> <span className="text-[11px] text-muted">
Sendet eine E-Mail über den konfigurierten SMTP-Server der Instanz. Sendet eine E-Mail über den konfigurierten SMTP-Server der Instanz.
</span> </span>
</div> </div>
@@ -483,7 +602,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
</div> </div>
<div className="pl-6"> <div className="pl-6">
<label className="block text-zinc-400 mb-1"> <label className="block text-muted mb-1">
Abweichende Empfänger-E-Mail (optional, Standard: {currentUser.email}) Abweichende Empfänger-E-Mail (optional, Standard: {currentUser.email})
</label> </label>
<input <input
@@ -491,17 +610,15 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
value={alarmEmail} value={alarmEmail}
onChange={(e) => setAlarmEmail(e.target.value)} onChange={(e) => setAlarmEmail(e.target.value)}
placeholder={currentUser.email} placeholder={currentUser.email}
className="w-full h-8 px-2.5 input-field text-white" className="w-full h-8 px-2.5 input-field"
/> />
</div> </div>
</div> </div>
{/* Channel 2: Discord Webhook */} {/* Channel 2: Discord Webhook */}
<div <div
className={`p-4 border transition-colors space-y-3 ${ className={`p-4 border transition-colors space-y-3 bg-canvas ${
alarmChannels.includes('discord') alarmChannels.includes('discord') ? 'border-accent' : 'border-border'
? 'bg-canvas border-accent'
: 'bg-canvas/60 border-border'
}`} }`}
> >
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
@@ -514,9 +631,10 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
/> />
<MessageSquare className="w-4 h-4 text-accent" /> <MessageSquare className="w-4 h-4 text-accent" />
<div> <div>
<span className="font-bold text-white block">Ziel: Discord Webhook</span> <span className="font-bold text-main block">Ziel: Discord Webhook</span>
<span className="text-[11px] text-zinc-400"> <span className="text-[11px] text-muted">
Sendet eine formatierte Embed-Benachrichtigung direkt in deinen Discord-Kanal. Sendet eine formatierte Embed-Benachrichtigung direkt in deinen
Discord-Kanal.
</span> </span>
</div> </div>
</label> </label>
@@ -533,23 +651,21 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
</div> </div>
<div className="pl-6"> <div className="pl-6">
<label className="block text-zinc-400 mb-1">Discord Webhook URL</label> <label className="block text-muted mb-1">Discord Webhook URL</label>
<input <input
type="url" type="url"
value={discordWebhookUrl} value={discordWebhookUrl}
onChange={(e) => setDiscordWebhookUrl(e.target.value)} onChange={(e) => setDiscordWebhookUrl(e.target.value)}
placeholder="https://discord.com/api/webhooks/123456789/token..." placeholder="https://discord.com/api/webhooks/123456789/token..."
className="w-full h-8 px-2.5 input-field text-white font-mono" className="w-full h-8 px-2.5 input-field font-mono"
/> />
</div> </div>
</div> </div>
{/* Channel 3: Custom HTTP Webhook */} {/* Channel 3: Custom HTTP Webhook */}
<div <div
className={`p-4 border transition-colors space-y-3 ${ className={`p-4 border transition-colors space-y-3 bg-canvas ${
alarmChannels.includes('webhook') alarmChannels.includes('webhook') ? 'border-accent' : 'border-border'
? 'bg-canvas border-accent'
: 'bg-canvas/60 border-border'
}`} }`}
> >
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
@@ -562,11 +678,12 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
/> />
<Globe className="w-4 h-4 text-accent" /> <Globe className="w-4 h-4 text-accent" />
<div> <div>
<span className="font-bold text-white block"> <span className="font-bold text-main block">
Ziel: Eigener Webhook (HTTP POST JSON) Ziel: Eigener Webhook (HTTP POST JSON)
</span> </span>
<span className="text-[11px] text-zinc-400"> <span className="text-[11px] text-muted">
Für eigene Empfänger-Skripte, Ntfy, Gotify, Slack, Mattermost oder Automatisierungen. Für eigene Empfänger-Skripte, Ntfy, Gotify, Slack, Mattermost oder
Automatisierungen.
</span> </span>
</div> </div>
</label> </label>
@@ -583,13 +700,15 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
</div> </div>
<div className="pl-6"> <div className="pl-6">
<label className="block text-zinc-400 mb-1">Ziel-URL (empfängt JSON-Payload per POST)</label> <label className="block text-muted mb-1">
Ziel-URL (empfängt JSON-Payload per POST)
</label>
<input <input
type="url" type="url"
value={customWebhookUrl} value={customWebhookUrl}
onChange={(e) => setCustomWebhookUrl(e.target.value)} onChange={(e) => setCustomWebhookUrl(e.target.value)}
placeholder="https://dein-server.local/webhook/mindsift-alarm" placeholder="https://dein-server.local/webhook/mindsift-alarm"
className="w-full h-8 px-2.5 input-field text-white font-mono" className="w-full h-8 px-2.5 input-field font-mono"
/> />
</div> </div>
</div> </div>
@@ -608,15 +727,17 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
)} )}
{/* ================================================================= */} {/* ================================================================= */}
{/* SECTION 3: PRIVATE WEBHOOKS & EXPORT */} {/* SECTION 4: PRIVATE WEBHOOKS & EXPORT */}
{/* ================================================================= */} {/* ================================================================= */}
{section === 'webhooks' && ( {section === 'webhooks' && (
<div className="space-y-5"> <div className="space-y-5">
<div className="border-b border-border pb-3"> <div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-white">Private Webhooks (Eingehend) & Daten-Export</h2> <h2 className="text-sm font-bold text-main">
<p className="text-zinc-400 mt-0.5"> Private Webhooks (Eingehend) & Daten-Export
Erstelle persönliche API-Schlüssel, um aus externen Skripten, Server-Monitoring oder </h2>
Discord-Bots automatisch Aufgaben und Gedanken in dein MindSift einzuspeisen. <p className="text-muted mt-0.5">
Erstelle persönliche API-Schlüssel, um aus externen Skripten, Server-Monitoring
oder Discord-Bots automatisch Aufgaben und Gedanken in dein MindSift einzuspeisen.
</p> </p>
</div> </div>
@@ -627,7 +748,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
value={newKeyName} value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)} onChange={(e) => setNewKeyName(e.target.value)}
placeholder="Bezeichnung des Schlüssels (z.B. Proxmox Monitoring, Discord-Forwarder)…" placeholder="Bezeichnung des Schlüssels (z.B. Proxmox Monitoring, Discord-Forwarder)…"
className="flex-1 h-9 px-3 input-field text-white" className="flex-1 h-9 px-3 input-field"
/> />
<button <button
type="submit" type="submit"
@@ -639,12 +760,13 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
</form> </form>
{newlyCreatedKey && ( {newlyCreatedKey && (
<div className="p-3 bg-accent/15 border border-accent flex flex-col sm:flex-row sm:items-center justify-between gap-2"> <div className="p-3 bg-accent-dim border border-accent flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-[11px] text-accent font-bold"> <p className="text-[11px] text-accent font-bold">
Neuer API-Schlüssel erstellt (bitte jetzt kopieren, wird nur einmal im Klartext angezeigt): Neuer API-Schlüssel erstellt (bitte jetzt kopieren, wird nur einmal im
Klartext angezeigt):
</p> </p>
<code className="text-xs font-mono text-white break-all">{newlyCreatedKey}</code> <code className="text-xs font-mono text-main break-all">{newlyCreatedKey}</code>
</div> </div>
<button <button
onClick={() => handleCopy(newlyCreatedKey, 'key')} onClick={() => handleCopy(newlyCreatedKey, 'key')}
@@ -658,16 +780,17 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
{/* Existing Keys List */} {/* Existing Keys List */}
<div className="border border-border divide-y divide-border bg-canvas"> <div className="border border-border divide-y divide-border bg-canvas">
{apiKeys.length === 0 ? ( {apiKeys.length === 0 ? (
<div className="p-4 text-center text-zinc-500"> <div className="p-4 text-center text-faint">
Noch keine persönlichen API-Schlüssel vorhanden. Noch keine persönlichen API-Schlüssel vorhanden.
</div> </div>
) : ( ) : (
apiKeys.map((k) => ( apiKeys.map((k) => (
<div key={k.id} className="p-3 flex items-center justify-between gap-3"> <div key={k.id} className="p-3 flex items-center justify-between gap-3">
<div> <div>
<p className="font-semibold text-white">{k.name}</p> <p className="font-semibold text-main">{k.name}</p>
<p className="text-[11px] text-zinc-500 font-mono"> <p className="text-[11px] text-faint font-mono">
Präfix: {k.keyPrefix}•••••••• · Erstellt: {new Date(k.createdAt).toLocaleDateString()} Präfix: {k.keyPrefix}•••••••• · Erstellt:{' '}
{new Date(k.createdAt).toLocaleDateString()}
</p> </p>
</div> </div>
<button <button
@@ -685,7 +808,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
{/* Curl Example */} {/* Curl Example */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="font-semibold text-zinc-300 flex items-center gap-1.5"> <span className="font-semibold text-muted flex items-center gap-1.5">
<Terminal className="w-3.5 h-3.5 text-accent" /> <Terminal className="w-3.5 h-3.5 text-accent" />
<span>Curl-Beispiel: Direkt als Aufgabe einspeisen</span> <span>Curl-Beispiel: Direkt als Aufgabe einspeisen</span>
</span> </span>
@@ -696,7 +819,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
{copiedLabel === 'curl' ? 'Kopiert!' : 'Befehl kopieren'} {copiedLabel === 'curl' ? 'Kopiert!' : 'Befehl kopieren'}
</button> </button>
</div> </div>
<pre className="p-3 bg-canvas border border-border text-[11px] font-mono text-zinc-300 overflow-x-auto whitespace-pre"> <pre className="p-3 code-block text-[11px] font-mono overflow-x-auto whitespace-pre">
{curlTaskExample} {curlTaskExample}
</pre> </pre>
</div> </div>
@@ -704,9 +827,10 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
{/* Full Data Backup */} {/* Full Data Backup */}
<div className="pt-4 border-t border-border flex flex-col sm:flex-row sm:items-center justify-between gap-3"> <div className="pt-4 border-t border-border flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div> <div>
<h3 className="font-bold text-white">Vollständiger Daten-Export (JSON Backup)</h3> <h3 className="font-bold text-main">Vollständiger Daten-Export (JSON Backup)</h3>
<p className="text-zinc-400 text-[11px]"> <p className="text-muted text-[11px]">
Lädt alle deine Gedanken, Aufgaben, Markdown-Dokumente und Listen als portable JSON-Datei herunter. Lädt alle deine Gedanken, Aufgaben, Markdown-Dokumente und Listen als portable
JSON-Datei herunter.
</p> </p>
</div> </div>
<button <button
+110 -102
View File
@@ -6,16 +6,12 @@ import {
Trash2, Trash2,
RotateCcw, RotateCcw,
FileText, FileText,
Tag,
LayoutList, LayoutList,
Columns, Columns,
ArrowRight, ArrowRight,
ArrowLeft, ArrowLeft,
ChevronRight,
ChevronDown,
Edit3, Edit3,
X, X,
Clock,
Layers, Layers,
Share2, Share2,
} from 'lucide-react'; } from 'lucide-react';
@@ -85,7 +81,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
const nextState = !task.isCompleted; const nextState = !task.isCompleted;
const nextKanban = nextState ? 'done' : 'todo'; const nextKanban = nextState ? 'done' : 'todo';
setTasks((prev) => setTasks((prev) =>
prev.map((t) => (t.id === task.id ? { ...t, isCompleted: nextState, kanbanStatus: nextKanban } : t)) prev.map((t) =>
t.id === task.id ? { ...t, isCompleted: nextState, kanbanStatus: nextKanban } : t
)
); );
await api.updateThought(task.id, { isCompleted: nextState, kanbanStatus: nextKanban }); await api.updateThought(task.id, { isCompleted: nextState, kanbanStatus: nextKanban });
onRefreshStats(); onRefreshStats();
@@ -182,7 +180,12 @@ export const TasksView: React.FC<TasksViewProps> = ({
}; };
const handleDeleteTaskList = async (listId: string) => { const handleDeleteTaskList = async (listId: string) => {
if (!window.confirm('Möchten Sie diese Aufgabenliste wirklich löschen? Die enthaltenen Aufgaben bleiben erhalten.')) return; if (
!window.confirm(
'Möchten Sie diese Aufgabenliste wirklich löschen? Die enthaltenen Aufgaben bleiben erhalten.'
)
)
return;
try { try {
await api.deleteTaskList(listId); await api.deleteTaskList(listId);
setTaskLists((prev) => prev.filter((l) => l.id !== listId)); setTaskLists((prev) => prev.filter((l) => l.id !== listId));
@@ -194,12 +197,18 @@ export const TasksView: React.FC<TasksViewProps> = ({
const safeTasks = Array.isArray(tasks) ? tasks : []; const safeTasks = Array.isArray(tasks) ? tasks : [];
const safeTaskLists = Array.isArray(taskLists) ? taskLists : []; const safeTaskLists = Array.isArray(taskLists) ? taskLists : [];
const safeSpaces = Array.isArray(spaces) ? spaces : [];
const safeDiscarded = Array.isArray(discarded) ? discarded : [];
const openTasks = safeTasks.filter((t) => !t.isCompleted); const openTasks = safeTasks.filter((t) => !t.isCompleted);
const doneTasks = safeTasks.filter((t) => t.isCompleted); const doneTasks = safeTasks.filter((t) => t.isCompleted);
const todoKanban = safeTasks.filter((t) => (!t.kanbanStatus || t.kanbanStatus === 'todo') && !t.isCompleted); const todoKanban = safeTasks.filter(
const inProgressKanban = safeTasks.filter((t) => t.kanbanStatus === 'in_progress' && !t.isCompleted); (t) => (!t.kanbanStatus || t.kanbanStatus === 'todo') && !t.isCompleted
);
const inProgressKanban = safeTasks.filter(
(t) => t.kanbanStatus === 'in_progress' && !t.isCompleted
);
const doneKanban = safeTasks.filter((t) => t.kanbanStatus === 'done' || t.isCompleted); const doneKanban = safeTasks.filter((t) => t.kanbanStatus === 'done' || t.isCompleted);
const currentSelectedList = safeTaskLists.find((l) => l.id === selectedListId); const currentSelectedList = safeTaskLists.find((l) => l.id === selectedListId);
@@ -212,10 +221,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
<div className="flex flex-wrap items-center gap-1"> <div className="flex flex-wrap items-center gap-1">
<button <button
onClick={() => setSelectedListId('all')} onClick={() => setSelectedListId('all')}
className={`h-8 px-3 text-xs font-medium border transition-colors ${ className={`h-8 px-3 text-xs font-medium border rounded-btn transition-colors ${
selectedListId === 'all' selectedListId === 'all'
? 'bg-panel border-accent text-white' ? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-zinc-400 hover:text-white' : 'bg-canvas border-border text-muted hover:text-main'
}`} }`}
> >
Alle Aufgaben Alle Aufgaben
@@ -225,18 +234,16 @@ export const TasksView: React.FC<TasksViewProps> = ({
<div key={tl.id} className="relative group flex items-center"> <div key={tl.id} className="relative group flex items-center">
<button <button
onClick={() => setSelectedListId(tl.id)} onClick={() => setSelectedListId(tl.id)}
className={`h-8 px-3 text-xs font-medium border flex items-center gap-1.5 transition-colors ${ className={`h-8 px-3 text-xs font-medium border rounded-btn flex items-center gap-1.5 transition-colors ${
selectedListId === tl.id selectedListId === tl.id
? 'bg-panel border-accent text-white' ? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-zinc-400 hover:text-white' : 'bg-canvas border-border text-muted hover:text-main'
}`} }`}
> >
<span>{tl.icon}</span> <span>{tl.icon}</span>
<span>{tl.title}</span> <span>{tl.title}</span>
{tl.tasksCount !== undefined && tl.tasksCount > 0 && ( {tl.tasksCount !== undefined && tl.tasksCount > 0 && (
<span className="ml-1 px-1.5 py-0.2 bg-zinc-800 text-[10px] text-zinc-300"> <span className="ml-1 badge-neutral">{tl.tasksCount}</span>
{tl.tasksCount}
</span>
)} )}
{tl.spaceId && ( {tl.spaceId && (
<span title="Geteilt mit Team/Space"> <span title="Geteilt mit Team/Space">
@@ -248,7 +255,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
<button <button
onClick={() => handleDeleteTaskList(tl.id)} onClick={() => handleDeleteTaskList(tl.id)}
title="Liste löschen" title="Liste löschen"
className="h-8 px-1.5 bg-panel border-y border-r border-accent text-zinc-500 hover:text-red-400" className="h-8 px-1.5 bg-panel border-y border-r border-accent text-faint hover:text-status-err"
> >
<X className="w-3 h-3" /> <X className="w-3 h-3" />
</button> </button>
@@ -258,7 +265,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
<button <button
onClick={() => setShowNewListModal(true)} onClick={() => setShowNewListModal(true)}
className="h-8 px-2.5 bg-canvas hover:bg-panel border border-border border-dashed text-xs text-zinc-400 hover:text-white flex items-center gap-1 transition-colors" className="h-8 px-2.5 bg-canvas hover:bg-panel border border-border border-dashed rounded-btn text-xs text-muted hover:text-main flex items-center gap-1 transition-colors"
title="Neue Aufgabenliste anlegen" title="Neue Aufgabenliste anlegen"
> >
<Plus className="w-3.5 h-3.5" /> <Plus className="w-3.5 h-3.5" />
@@ -268,11 +275,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
{/* View Switcher: List vs Kanban */} {/* View Switcher: List vs Kanban */}
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="flex border border-border bg-canvas"> <div className="flex border border-border rounded-btn bg-canvas overflow-hidden">
<button <button
onClick={() => setViewMode('list')} onClick={() => setViewMode('list')}
className={`h-8 px-2.5 flex items-center gap-1.5 text-xs transition-colors border-r border-border ${ className={`h-8 px-2.5 flex items-center gap-1.5 text-xs transition-colors border-r border-border ${
viewMode === 'list' ? 'bg-panel text-white' : 'text-zinc-400 hover:text-white' viewMode === 'list' ? 'bg-panel text-main font-medium' : 'text-muted hover:text-main'
}`} }`}
title="Listenansicht" title="Listenansicht"
> >
@@ -282,7 +289,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
<button <button
onClick={() => setViewMode('kanban')} onClick={() => setViewMode('kanban')}
className={`h-8 px-2.5 flex items-center gap-1.5 text-xs transition-colors ${ className={`h-8 px-2.5 flex items-center gap-1.5 text-xs transition-colors ${
viewMode === 'kanban' ? 'bg-panel text-white' : 'text-zinc-400 hover:text-white' viewMode === 'kanban'
? 'bg-panel text-main font-medium'
: 'text-muted hover:text-main'
}`} }`}
title="Kanban-Board" title="Kanban-Board"
> >
@@ -294,7 +303,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div> </div>
{/* Quick Add Task Input in active list */} {/* Quick Add Task Input in active list */}
<form onSubmit={handleQuickAddTask} className="flex items-center gap-1.5 bg-panel border border-border p-1.5"> <form
onSubmit={handleQuickAddTask}
className="flex items-center gap-1.5 panel-surface p-1.5"
>
<Plus className="w-4 h-4 text-accent ml-1 flex-shrink-0" /> <Plus className="w-4 h-4 text-accent ml-1 flex-shrink-0" />
<input <input
type="text" type="text"
@@ -305,14 +317,14 @@ export const TasksView: React.FC<TasksViewProps> = ({
? `Aufgabe zu "${currentSelectedList.title}" hinzufügen… (Enter)` ? `Aufgabe zu "${currentSelectedList.title}" hinzufügen… (Enter)`
: 'Neue Aufgabe anlegen… (Enter)' : 'Neue Aufgabe anlegen… (Enter)'
} }
className="flex-1 bg-transparent px-2 py-1 text-sm text-white placeholder-zinc-500 focus:outline-none" className="flex-1 bg-transparent px-2 py-1 text-sm text-main placeholder-faint focus:outline-none"
/> />
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<input <input
type="date" type="date"
value={quickDueDate} value={quickDueDate}
onChange={(e) => setQuickDueDate(e.target.value)} onChange={(e) => setQuickDueDate(e.target.value)}
className="h-7 px-2 bg-canvas border border-border text-xs text-zinc-300 focus:outline-none" className="h-7 px-2 input-field text-xs"
title="Fälligkeitsdatum" title="Fälligkeitsdatum"
/> />
<button <button
@@ -327,21 +339,24 @@ export const TasksView: React.FC<TasksViewProps> = ({
{/* NEW LIST MODAL */} {/* NEW LIST MODAL */}
{showNewListModal && ( {showNewListModal && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4"> <div className="fixed inset-0 modal-overlay z-50 flex items-center justify-center p-4">
<div className="w-full max-w-sm bg-panel border border-border p-4 space-y-3"> <div className="w-full max-w-sm panel-surface shadow-panel p-4 space-y-3">
<div className="flex items-center justify-between border-b border-border pb-2"> <div className="flex items-center justify-between border-b border-border pb-2">
<h3 className="text-sm font-semibold text-white flex items-center gap-1.5"> <h3 className="text-sm font-semibold text-main flex items-center gap-1.5">
<Layers className="w-4 h-4 text-accent" /> <Layers className="w-4 h-4 text-accent" />
<span>Neue Aufgabenliste erstellen</span> <span>Neue Aufgabenliste erstellen</span>
</h3> </h3>
<button onClick={() => setShowNewListModal(false)} className="text-zinc-400 hover:text-white"> <button
onClick={() => setShowNewListModal(false)}
className="text-muted hover:text-main"
>
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</button> </button>
</div> </div>
<form onSubmit={handleCreateTaskList} className="space-y-3"> <form onSubmit={handleCreateTaskList} className="space-y-3">
<div> <div>
<label className="block text-xs text-zinc-400 mb-1">Titel der Liste</label> <label className="block text-xs text-muted mb-1">Titel der Liste</label>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<input <input
type="text" type="text"
@@ -358,21 +373,23 @@ export const TasksView: React.FC<TasksViewProps> = ({
placeholder="z.B. Homelab, Einkäufe, Steuern" placeholder="z.B. Homelab, Einkäufe, Steuern"
autoFocus autoFocus
required required
className="flex-1 h-8 input-field px-2.5 text-xs text-white" className="flex-1 h-8 input-field px-2.5 text-xs"
/> />
</div> </div>
</div> </div>
{spaces.length > 0 && ( {safeSpaces.length > 0 && (
<div> <div>
<label className="block text-xs text-zinc-400 mb-1">Geteilt mit Space (optional)</label> <label className="block text-xs text-muted mb-1">
Geteilt mit Space (optional)
</label>
<select <select
value={newListSpaceId} value={newListSpaceId}
onChange={(e) => setNewListSpaceId(e.target.value)} onChange={(e) => setNewListSpaceId(e.target.value)}
className="w-full h-8 input-field px-2 text-xs text-white bg-canvas" className="w-full h-8 input-field px-2 text-xs"
> >
<option value="">Privat (nur ich)</option> <option value="">Privat (nur ich)</option>
{spaces.map((sp) => ( {safeSpaces.map((sp) => (
<option key={sp.id} value={sp.id}> <option key={sp.id} value={sp.id}>
{sp.icon} {sp.name} {sp.icon} {sp.name}
</option> </option>
@@ -405,29 +422,27 @@ export const TasksView: React.FC<TasksViewProps> = ({
{/* COLUMN 1: TODO */} {/* COLUMN 1: TODO */}
<div className="bg-canvas border border-border flex flex-col"> <div className="bg-canvas border border-border flex flex-col">
<div className="px-3 py-2 bg-panel border-b border-border flex items-center justify-between"> <div className="px-3 py-2 bg-panel border-b border-border flex items-center justify-between">
<span className="text-xs font-semibold text-zinc-300 flex items-center gap-1.5"> <span className="text-xs font-semibold text-main flex items-center gap-1.5">
<span className="w-2 h-2 bg-zinc-500 inline-block" /> <span className="w-2 h-2 bg-faint inline-block" />
<span>Zu erledigen</span> <span>Zu erledigen</span>
</span> </span>
<span className="px-1.5 py-0.2 bg-canvas border border-border text-[10px] text-zinc-400 font-mono"> <span className="badge-neutral font-mono">{todoKanban.length}</span>
{todoKanban.length}
</span>
</div> </div>
<div className="p-2 space-y-2 min-h-[360px] flex-1 overflow-y-auto"> <div className="p-2 space-y-2 min-h-[360px] flex-1 overflow-y-auto">
{todoKanban.map((task) => ( {todoKanban.map((task) => (
<div <div
key={task.id} key={task.id}
className="bg-panel border border-border p-2.5 space-y-2 hover:border-zinc-500 transition-colors" className="panel-surface p-2.5 space-y-2 hover:border-strong transition-colors"
> >
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<p className="text-xs font-medium text-white break-words flex-1 leading-snug"> <p className="text-xs font-medium text-main break-words flex-1 leading-snug">
{task.content} {task.content}
</p> </p>
<button <button
onClick={() => handleConvertToDoc(task)} onClick={() => handleConvertToDoc(task)}
title="Als Notiz vertiefen" title="Als Notiz vertiefen"
className="text-zinc-500 hover:text-accent flex-shrink-0" className="text-faint hover:text-accent flex-shrink-0"
> >
<FileText className="w-3.5 h-3.5" /> <FileText className="w-3.5 h-3.5" />
</button> </button>
@@ -441,7 +456,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
onChange={(e) => setNotesDraft(e.target.value)} onChange={(e) => setNotesDraft(e.target.value)}
placeholder="Detailnotizen eingeben…" placeholder="Detailnotizen eingeben…"
rows={2} rows={2}
className="w-full input-field p-1.5 text-xs text-white resize-none" className="w-full input-field p-1.5 text-xs resize-none"
/> />
<div className="flex justify-end gap-1"> <div className="flex justify-end gap-1">
<button <button
@@ -464,7 +479,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
setActiveNotesTaskId(task.id); setActiveNotesTaskId(task.id);
setNotesDraft(task.notes || ''); setNotesDraft(task.notes || '');
}} }}
className="p-1.5 bg-canvas border border-border text-[11px] text-zinc-400 break-words cursor-pointer hover:border-zinc-500" className="p-1.5 bg-canvas border border-border text-[11px] text-muted break-words cursor-pointer hover:border-strong"
title="Klicken zum Bearbeiten der Notizen" title="Klicken zum Bearbeiten der Notizen"
> >
{task.notes} {task.notes}
@@ -472,10 +487,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
) : null} ) : null}
{/* Footer & Movement Controls */} {/* Footer & Movement Controls */}
<div className="flex items-center justify-between gap-1 pt-1 border-t border-white/[0.05] text-[11px]"> <div className="flex items-center justify-between gap-1 pt-1 border-t border-subtle text-[11px]">
<div className="flex items-center gap-1.5 text-zinc-400"> <div className="flex items-center gap-1.5 text-muted">
{task.dueDate && ( {task.dueDate && (
<span className="flex items-center gap-1 text-[10px] text-zinc-300"> <span className="flex items-center gap-1 text-[10px] text-muted">
<Calendar className="w-2.5 h-2.5" /> {task.dueDate} <Calendar className="w-2.5 h-2.5" /> {task.dueDate}
</span> </span>
)} )}
@@ -485,7 +500,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
setActiveNotesTaskId(task.id); setActiveNotesTaskId(task.id);
setNotesDraft(''); setNotesDraft('');
}} }}
className="text-[10px] text-zinc-500 hover:text-zinc-300" className="text-[10px] text-faint hover:text-main"
> >
+ Notiz + Notiz
</button> </button>
@@ -506,7 +521,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div> </div>
))} ))}
{todoKanban.length === 0 && ( {todoKanban.length === 0 && (
<div className="p-4 text-center text-zinc-600 text-xs">Keine Aufgaben offen</div> <div className="p-4 text-center text-faint text-xs">Keine Aufgaben offen</div>
)} )}
</div> </div>
</div> </div>
@@ -518,25 +533,23 @@ export const TasksView: React.FC<TasksViewProps> = ({
<span className="w-2 h-2 bg-accent inline-block" /> <span className="w-2 h-2 bg-accent inline-block" />
<span>In Bearbeitung</span> <span>In Bearbeitung</span>
</span> </span>
<span className="px-1.5 py-0.2 bg-canvas border border-border text-[10px] text-zinc-400 font-mono"> <span className="badge-neutral font-mono">{inProgressKanban.length}</span>
{inProgressKanban.length}
</span>
</div> </div>
<div className="p-2 space-y-2 min-h-[360px] flex-1 overflow-y-auto"> <div className="p-2 space-y-2 min-h-[360px] flex-1 overflow-y-auto">
{inProgressKanban.map((task) => ( {inProgressKanban.map((task) => (
<div <div
key={task.id} key={task.id}
className="bg-panel border border-accent/40 p-2.5 space-y-2 hover:border-accent transition-colors" className="bg-panel border border-accent-border p-2.5 space-y-2 hover:border-accent transition-colors"
> >
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<p className="text-xs font-medium text-white break-words flex-1 leading-snug"> <p className="text-xs font-medium text-main break-words flex-1 leading-snug">
{task.content} {task.content}
</p> </p>
<button <button
onClick={() => handleConvertToDoc(task)} onClick={() => handleConvertToDoc(task)}
title="Als Notiz vertiefen" title="Als Notiz vertiefen"
className="text-zinc-500 hover:text-accent flex-shrink-0" className="text-faint hover:text-accent flex-shrink-0"
> >
<FileText className="w-3.5 h-3.5" /> <FileText className="w-3.5 h-3.5" />
</button> </button>
@@ -550,7 +563,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
onChange={(e) => setNotesDraft(e.target.value)} onChange={(e) => setNotesDraft(e.target.value)}
placeholder="Detailnotizen eingeben…" placeholder="Detailnotizen eingeben…"
rows={2} rows={2}
className="w-full input-field p-1.5 text-xs text-white resize-none" className="w-full input-field p-1.5 text-xs resize-none"
/> />
<div className="flex justify-end gap-1"> <div className="flex justify-end gap-1">
<button <button
@@ -573,20 +586,20 @@ export const TasksView: React.FC<TasksViewProps> = ({
setActiveNotesTaskId(task.id); setActiveNotesTaskId(task.id);
setNotesDraft(task.notes || ''); setNotesDraft(task.notes || '');
}} }}
className="p-1.5 bg-canvas border border-border text-[11px] text-zinc-400 break-words cursor-pointer hover:border-zinc-500" className="p-1.5 bg-canvas border border-border text-[11px] text-muted break-words cursor-pointer hover:border-strong"
> >
{task.notes} {task.notes}
</div> </div>
) : null} ) : null}
{/* Movement Controls */} {/* Movement Controls */}
<div className="flex items-center justify-between gap-1 pt-1 border-t border-white/[0.05] text-[11px]"> <div className="flex items-center justify-between gap-1 pt-1 border-t border-subtle text-[11px]">
<button <button
onClick={() => handleKanbanMove(task, 'todo')} onClick={() => handleKanbanMove(task, 'todo')}
className="btn-action h-6 px-1.5 text-[10px] flex items-center gap-1" className="btn-action h-6 px-1.5 text-[10px] flex items-center gap-1"
title="Zurück zu 'Zu erledigen'" title="Zurück zu 'Zu erledigen'"
> >
<ArrowLeft className="w-3 h-3 text-zinc-400" /> <ArrowLeft className="w-3 h-3 text-muted" />
<span>Zurück</span> <span>Zurück</span>
</button> </button>
@@ -602,7 +615,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div> </div>
))} ))}
{inProgressKanban.length === 0 && ( {inProgressKanban.length === 0 && (
<div className="p-4 text-center text-zinc-600 text-xs">Keine aktiven Aufgaben</div> <div className="p-4 text-center text-faint text-xs">Keine aktiven Aufgaben</div>
)} )}
</div> </div>
</div> </div>
@@ -610,26 +623,24 @@ export const TasksView: React.FC<TasksViewProps> = ({
{/* COLUMN 3: DONE */} {/* COLUMN 3: DONE */}
<div className="bg-canvas border border-border flex flex-col"> <div className="bg-canvas border border-border flex flex-col">
<div className="px-3 py-2 bg-panel border-b border-border flex items-center justify-between"> <div className="px-3 py-2 bg-panel border-b border-border flex items-center justify-between">
<span className="text-xs font-semibold text-emerald-400 flex items-center gap-1.5"> <span className="text-xs font-semibold text-status-ok flex items-center gap-1.5">
<span className="w-2 h-2 bg-emerald-500 inline-block" /> <span className="w-2 h-2 bg-status-ok inline-block" />
<span>Erledigt</span> <span>Erledigt</span>
</span> </span>
<span className="px-1.5 py-0.2 bg-canvas border border-border text-[10px] text-zinc-400 font-mono"> <span className="badge-neutral font-mono">{doneKanban.length}</span>
{doneKanban.length}
</span>
</div> </div>
<div className="p-2 space-y-2 min-h-[360px] flex-1 overflow-y-auto"> <div className="p-2 space-y-2 min-h-[360px] flex-1 overflow-y-auto">
{doneKanban.map((task) => ( {doneKanban.map((task) => (
<div <div
key={task.id} key={task.id}
className="bg-panel border border-border p-2.5 space-y-2 opacity-70 hover:opacity-100 transition-opacity" className="panel-surface p-2.5 space-y-2 opacity-70 hover:opacity-100 transition-opacity"
> >
<p className="text-xs font-medium text-zinc-400 line-through break-words leading-snug"> <p className="text-xs font-medium text-muted line-through break-words leading-snug">
{task.content} {task.content}
</p> </p>
<div className="flex items-center justify-between gap-1 pt-1 border-t border-white/[0.05] text-[11px]"> <div className="flex items-center justify-between gap-1 pt-1 border-t border-subtle text-[11px]">
<button <button
onClick={() => handleKanbanMove(task, 'todo')} onClick={() => handleKanbanMove(task, 'todo')}
className="btn-action h-6 px-1.5 text-[10px] flex items-center gap-1" className="btn-action h-6 px-1.5 text-[10px] flex items-center gap-1"
@@ -640,7 +651,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
</button> </button>
<button <button
onClick={() => handleDeletePermanent(task.id)} onClick={() => handleDeletePermanent(task.id)}
className="text-zinc-500 hover:text-red-400 p-1" className="text-faint hover:text-status-err p-1"
title="Löschen" title="Löschen"
> >
<Trash2 className="w-3 h-3" /> <Trash2 className="w-3 h-3" />
@@ -649,7 +660,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div> </div>
))} ))}
{doneKanban.length === 0 && ( {doneKanban.length === 0 && (
<div className="p-4 text-center text-zinc-600 text-xs">Noch keine erledigten Aufgaben</div> <div className="p-4 text-center text-faint text-xs">
Noch keine erledigten Aufgaben
</div>
)} )}
</div> </div>
</div> </div>
@@ -659,11 +672,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
<div className="space-y-2"> <div className="space-y-2">
{/* Subfilter tabs for List View */} {/* Subfilter tabs for List View */}
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center border border-border bg-canvas"> <div className="flex items-center border border-border rounded-btn bg-canvas overflow-hidden">
<button <button
onClick={() => setFilter('open')} onClick={() => setFilter('open')}
className={`h-8 px-3.5 text-xs font-medium border-r border-border transition-colors ${ className={`h-8 px-3.5 text-xs font-medium border-r border-border transition-colors ${
filter === 'open' ? 'bg-panel text-white' : 'text-zinc-400 hover:text-white' filter === 'open' ? 'bg-panel text-main' : 'text-muted hover:text-main'
}`} }`}
> >
Offen ({openTasks.length}) Offen ({openTasks.length})
@@ -672,7 +685,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
<button <button
onClick={() => setFilter('done')} onClick={() => setFilter('done')}
className={`h-8 px-3.5 text-xs font-medium border-r border-border transition-colors ${ className={`h-8 px-3.5 text-xs font-medium border-r border-border transition-colors ${
filter === 'done' ? 'bg-panel text-white' : 'text-zinc-400 hover:text-white' filter === 'done' ? 'bg-panel text-main' : 'text-muted hover:text-main'
}`} }`}
> >
Erledigt ({doneTasks.length}) Erledigt ({doneTasks.length})
@@ -681,14 +694,14 @@ export const TasksView: React.FC<TasksViewProps> = ({
<button <button
onClick={() => setFilter('discarded')} onClick={() => setFilter('discarded')}
className={`h-8 px-3.5 text-xs font-medium transition-colors ${ className={`h-8 px-3.5 text-xs font-medium transition-colors ${
filter === 'discarded' ? 'bg-panel text-white' : 'text-zinc-400 hover:text-white' filter === 'discarded' ? 'bg-panel text-main' : 'text-muted hover:text-main'
}`} }`}
> >
Papierkorb ({discarded.length}) Papierkorb ({safeDiscarded.length})
</button> </button>
</div> </div>
{filter === 'discarded' && discarded.length > 0 && ( {filter === 'discarded' && safeDiscarded.length > 0 && (
<button onClick={handlePurgeAllDiscarded} className="btn-action"> <button onClick={handlePurgeAllDiscarded} className="btn-action">
<Trash2 className="w-3.5 h-3.5" /> <Trash2 className="w-3.5 h-3.5" />
Papierkorb leeren Papierkorb leeren
@@ -697,22 +710,22 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div> </div>
{loading ? ( {loading ? (
<div className="p-6 text-center text-zinc-500 text-sm">Lade Aufgaben…</div> <div className="p-6 text-center text-faint text-sm">Lade Aufgaben…</div>
) : filter === 'discarded' ? ( ) : filter === 'discarded' ? (
<div className="panel-surface divide-y divide-border"> <div className="panel-surface divide-y divide-border">
{discarded.length === 0 ? ( {safeDiscarded.length === 0 ? (
<div className="px-4 py-6 text-center text-zinc-400 text-sm"> <div className="px-4 py-6 text-center text-muted text-sm">
Keine verworfenen Gedanken im Papierkorb. Keine verworfenen Gedanken im Papierkorb.
</div> </div>
) : ( ) : (
discarded.map((item) => ( safeDiscarded.map((item) => (
<div <div
key={item.id} key={item.id}
className="px-3 py-2 flex items-center justify-between gap-3 hover:bg-white/[0.02]" className="px-3 py-2 flex items-center justify-between gap-3 hover:bg-panel-hover"
> >
<div className="flex items-center gap-2.5 min-w-0 flex-1"> <div className="flex items-center gap-2.5 min-w-0 flex-1">
<Trash2 className="w-4 h-4 text-zinc-500 flex-shrink-0" /> <Trash2 className="w-4 h-4 text-faint flex-shrink-0" />
<p className="text-sm text-zinc-400 line-through truncate">{item.content}</p> <p className="text-sm text-muted line-through truncate">{item.content}</p>
</div> </div>
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
<button onClick={() => handleRestore(item)} className="btn-action"> <button onClick={() => handleRestore(item)} className="btn-action">
@@ -733,7 +746,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
) : ( ) : (
<div className="panel-surface divide-y divide-border"> <div className="panel-surface divide-y divide-border">
{(filter === 'open' ? openTasks : doneTasks).length === 0 ? ( {(filter === 'open' ? openTasks : doneTasks).length === 0 ? (
<div className="px-4 py-6 text-center text-zinc-400 text-sm"> <div className="px-4 py-6 text-center text-muted text-sm">
{filter === 'open' {filter === 'open'
? 'Keine offenen Aufgaben vorhanden.' ? 'Keine offenen Aufgaben vorhanden.'
: 'Noch keine erledigten Aufgaben.'} : 'Noch keine erledigten Aufgaben.'}
@@ -742,18 +755,18 @@ export const TasksView: React.FC<TasksViewProps> = ({
(filter === 'open' ? openTasks : doneTasks).map((task) => ( (filter === 'open' ? openTasks : doneTasks).map((task) => (
<div <div
key={task.id} key={task.id}
className={`px-3 py-2.5 flex flex-col sm:flex-row sm:items-center justify-between gap-2.5 transition-colors hover:bg-white/[0.02] ${ className={`px-3 py-2.5 flex flex-col sm:flex-row sm:items-center justify-between gap-2.5 transition-colors hover:bg-panel-hover ${
task.isCompleted ? 'opacity-65' : '' task.isCompleted ? 'opacity-65' : ''
}`} }`}
> >
<div className="flex items-start gap-3 min-w-0 flex-1"> <div className="flex items-start gap-3 min-w-0 flex-1">
{/* Square minimalist checkbox */} {/* Theme-controlled checkbox (--radius-checkbox) */}
<button <button
onClick={() => handleToggleDone(task)} onClick={() => handleToggleDone(task)}
className={`w-4 h-4 mt-0.5 flex items-center justify-center flex-shrink-0 transition-colors border ${ className={`w-4 h-4 mt-0.5 checkbox-square flex items-center justify-center flex-shrink-0 transition-colors border ${
task.isCompleted task.isCompleted
? 'bg-accent border-accent text-white' ? 'bg-accent border-accent text-on-accent'
: 'border-zinc-500 hover:border-accent text-transparent' : 'border-strong hover:border-accent text-transparent'
}`} }`}
title={task.isCompleted ? 'Als offen markieren' : 'Als erledigt abhaken'} title={task.isCompleted ? 'Als offen markieren' : 'Als erledigt abhaken'}
> >
@@ -763,7 +776,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
<div className="min-w-0 flex-1 space-y-1"> <div className="min-w-0 flex-1 space-y-1">
<p <p
className={`text-sm break-words leading-tight ${ className={`text-sm break-words leading-tight ${
task.isCompleted ? 'line-through text-zinc-500' : 'text-white' task.isCompleted ? 'line-through text-faint' : 'text-main'
}`} }`}
> >
{task.content} {task.content}
@@ -777,7 +790,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
onChange={(e) => setNotesDraft(e.target.value)} onChange={(e) => setNotesDraft(e.target.value)}
placeholder="Detailnotizen eingeben…" placeholder="Detailnotizen eingeben…"
rows={2} rows={2}
className="w-full input-field p-2 text-xs text-white resize-none" className="w-full input-field p-2 text-xs resize-none"
/> />
<div className="flex justify-end gap-1.5"> <div className="flex justify-end gap-1.5">
<button <button
@@ -800,7 +813,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
setActiveNotesTaskId(task.id); setActiveNotesTaskId(task.id);
setNotesDraft(task.notes || ''); setNotesDraft(task.notes || '');
}} }}
className="text-xs text-zinc-400 bg-canvas p-1.5 border border-border cursor-pointer hover:border-zinc-500" className="text-xs text-muted bg-canvas p-1.5 border border-border cursor-pointer hover:border-strong"
title="Klicken zum Bearbeiten" title="Klicken zum Bearbeiten"
> >
{task.notes} {task.notes}
@@ -812,20 +825,15 @@ export const TasksView: React.FC<TasksViewProps> = ({
<span className="badge-accent">Dringend</span> <span className="badge-accent">Dringend</span>
)} )}
{task.priority === 'high' && ( {task.priority === 'high' && (
<span className="px-1.5 py-0.2 border border-white/20 text-zinc-300 text-[10px] font-medium"> <span className="px-1.5 py-0.5 border border-strong rounded-btn text-main text-[10px] font-medium">
Wichtig Wichtig
</span> </span>
)} )}
{task.kanbanStatus === 'in_progress' && ( {task.kanbanStatus === 'in_progress' && (
<span className="px-1.5 py-0.2 bg-blue-900/40 border border-blue-500/40 text-blue-300 text-[10px]"> <span className="badge-accent">⚡ In Bearbeitung</span>
⚡ In Bearbeitung
</span>
)} )}
{task.tags?.map((tag) => ( {task.tags?.map((tag) => (
<span <span key={tag} className="badge-neutral">
key={tag}
className="px-1.5 py-0.2 bg-canvas text-zinc-400 text-[10px]"
>
#{tag} #{tag}
</span> </span>
))} ))}
@@ -850,7 +858,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
)} )}
<div className="relative flex items-center"> <div className="relative flex items-center">
<Calendar className="w-3 h-3 text-zinc-400 absolute left-2 pointer-events-none" /> <Calendar className="w-3 h-3 text-muted absolute left-2 pointer-events-none" />
<input <input
type="date" type="date"
value={task.dueDate || ''} value={task.dueDate || ''}
+43 -41
View File
@@ -193,19 +193,19 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
switch (priority) { switch (priority) {
case 'urgent': case 'urgent':
return ( return (
<span className="px-2 py-0.5 text-[11px] font-semibold border border-accent text-accent"> <span className="px-2 py-0.5 text-[11px] font-semibold border border-accent rounded-btn text-accent">
Dringend Dringend
</span> </span>
); );
case 'high': case 'high':
return ( return (
<span className="px-2 py-0.5 text-[11px] font-medium border border-white/20 text-zinc-200"> <span className="px-2 py-0.5 text-[11px] font-medium border border-strong rounded-btn text-main">
Wichtig Wichtig
</span> </span>
); );
default: default:
return ( return (
<span className="px-2 py-0.5 text-[11px] font-medium bg-panel-elevated text-zinc-400 border border-border"> <span className="px-2 py-0.5 text-[11px] font-medium bg-panel-elevated text-muted border border-border rounded-btn">
Quick Dump Quick Dump
</span> </span>
); );
@@ -215,14 +215,14 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
const sourceBadge = (source: string) => { const sourceBadge = (source: string) => {
if (source === 'voice') { if (source === 'voice') {
return ( return (
<span className="px-2 py-0.5 text-[11px] bg-canvas text-accent border border-border flex items-center gap-1"> <span className="px-2 py-0.5 text-[11px] bg-canvas text-accent border border-border rounded-btn flex items-center gap-1">
<Mic className="w-3 h-3" /> Diktat <Mic className="w-3 h-3" /> Diktat
</span> </span>
); );
} }
if (source === 'webhook') { if (source === 'webhook') {
return ( return (
<span className="px-2 py-0.5 text-[11px] bg-canvas text-zinc-300 border border-border flex items-center gap-1"> <span className="px-2 py-0.5 text-[11px] bg-canvas text-muted border border-border rounded-btn flex items-center gap-1">
<Webhook className="w-3 h-3 text-accent" /> Webhook <Webhook className="w-3 h-3 text-accent" /> Webhook
</span> </span>
); );
@@ -232,13 +232,15 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
if (!activeCard) { if (!activeCard) {
return ( return (
<div className="max-w-xl mx-auto mt-2 p-6 bg-panel border border-border text-center"> <div className="max-w-xl mx-auto mt-2 p-6 panel-surface text-center">
<div className="w-11 h-11 bg-accent text-white flex items-center justify-center mx-auto mb-3 shadow-sm"> <div className="w-11 h-11 bg-accent text-on-accent rounded-btn flex items-center justify-center mx-auto mb-3 shadow-sm">
<Sparkles className="w-5 h-5" /> <Sparkles className="w-5 h-5" />
</div> </div>
<h2 className="text-lg font-bold text-white mb-1">Triage-Deck ist leer</h2> <h2 className="text-lg font-bold text-main mb-1">Triage-Deck ist leer</h2>
<p className="text-zinc-400 text-xs sm:text-sm max-w-md mx-auto mb-4"> <p className="text-muted text-xs sm:text-sm max-w-md mx-auto mb-4">
Alle Quick Dump Notizen sind einsortiert oder verworfen. Tippe oben in die Leiste (Taste <kbd className="px-1.5 py-0.5 bg-canvas border border-border text-zinc-300">C</kbd>), um neue Gedanken reinzurotzen. Alle Quick Dump Notizen sind einsortiert oder verworfen. Tippe oben in die Leiste (Taste{' '}
<kbd className="px-1.5 py-0.5 bg-canvas border border-border rounded-btn text-main">C</kbd>
), um neue Gedanken reinzurotzen.
</p> </p>
<div className="flex flex-wrap items-center justify-center gap-2"> <div className="flex flex-wrap items-center justify-center gap-2">
@@ -271,8 +273,8 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
<span className="badge-accent"> <span className="badge-accent">
{cards.length} {cards.length === 1 ? 'Quick Dump' : 'Quick Dumps'} offen {cards.length} {cards.length === 1 ? 'Quick Dump' : 'Quick Dumps'} offen
</span> </span>
<span className="hidden sm:inline text-xs text-zinc-600">•</span> <span className="hidden sm:inline text-xs text-faint">•</span>
<span className="hidden sm:flex items-center gap-1 text-xs text-zinc-400"> <span className="hidden sm:flex items-center gap-1 text-xs text-muted">
<Keyboard className="w-3 h-3" /> ← Wegwerfen • → Aufgabe • D Notiz • Leertaste Später <Keyboard className="w-3 h-3" /> ← Wegwerfen • → Aufgabe • D Notiz • Leertaste Später
</span> </span>
</div> </div>
@@ -294,11 +296,11 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
{nextCard && ( {nextCard && (
<div <div
key={nextCard.id + '-preview'} key={nextCard.id + '-preview'}
className="absolute inset-x-3 top-3 bottom-0 bg-panel/60 border border-border p-4 opacity-40 scale-[0.97] pointer-events-none flex flex-col justify-between" className="absolute inset-x-3 top-3 bottom-0 panel-surface p-4 opacity-40 scale-[0.97] pointer-events-none flex flex-col justify-between"
> >
<div className="flex items-center gap-2">{priorityBadge(nextCard.priority)}</div> <div className="flex items-center gap-2">{priorityBadge(nextCard.priority)}</div>
<p className="text-sm text-zinc-400 line-clamp-2">{nextCard.content}</p> <p className="text-sm text-muted line-clamp-2">{nextCard.content}</p>
<div className="text-[11px] text-zinc-500">Nächster Quick Dump…</div> <div className="text-[11px] text-faint">Nächster Quick Dump…</div>
</div> </div>
)} )}
@@ -321,19 +323,19 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
initial={{ scale: 0.97, opacity: 0, y: 10 }} initial={{ scale: 0.97, opacity: 0, y: 10 }}
animate={{ scale: 1, opacity: 1, y: 0 }} animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.93, opacity: 0 }} exit={{ scale: 0.93, opacity: 0 }}
className="relative w-full h-full bg-panel border border-border p-4 sm:p-5 cursor-grab active:cursor-grabbing flex flex-col justify-between overflow-hidden" className="relative w-full h-full panel-surface p-4 sm:p-5 cursor-grab active:cursor-grabbing flex flex-col justify-between overflow-hidden"
> >
{/* Dynamic Swipe Overlays */} {/* Dynamic Swipe Overlays */}
<motion.div <motion.div
style={{ opacity: keepOpacity }} style={{ opacity: keepOpacity }}
className="pointer-events-none absolute top-3.5 right-3.5 px-2.5 py-1 bg-accent/20 border border-accent text-accent font-semibold text-xs tracking-wide uppercase" className="pointer-events-none absolute top-3.5 right-3.5 px-2.5 py-1 bg-accent-dim border border-accent rounded-btn text-accent font-semibold text-xs tracking-wide uppercase"
> >
Als Aufgabe 👉 Als Aufgabe 👉
</motion.div> </motion.div>
<motion.div <motion.div
style={{ opacity: discardOpacity }} style={{ opacity: discardOpacity }}
className="pointer-events-none absolute top-3.5 left-3.5 px-2.5 py-1 bg-white/10 border border-white/30 text-zinc-200 font-semibold text-xs tracking-wide uppercase" className="pointer-events-none absolute top-3.5 left-3.5 px-2.5 py-1 bg-panel-elevated border border-strong rounded-btn text-main font-semibold text-xs tracking-wide uppercase"
> >
👈 Wegwerfen 👈 Wegwerfen
</motion.div> </motion.div>
@@ -352,12 +354,12 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
setIsEditingCard((prev) => !prev); setIsEditingCard((prev) => !prev);
}} }}
title="Quick Dump Text vor dem Zuordnen anpassen" title="Quick Dump Text vor dem Zuordnen anpassen"
className="h-6 px-2 bg-canvas hover:bg-panel-hover border border-border text-[11px] text-zinc-400 hover:text-white flex items-center gap-1" className="h-6 px-2 bg-canvas hover:bg-panel-hover border border-border rounded-btn text-[11px] text-muted hover:text-main flex items-center gap-1"
> >
<Edit3 className="w-3 h-3" /> <Edit3 className="w-3 h-3" />
<span>Bearbeiten</span> <span>Bearbeiten</span>
</button> </button>
<span className="text-xs text-zinc-400 flex items-center gap-1"> <span className="text-xs text-muted flex items-center gap-1">
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
{new Date(activeCard.createdAt).toLocaleString('de-DE', { {new Date(activeCard.createdAt).toLocaleString('de-DE', {
day: '2-digit', day: '2-digit',
@@ -378,7 +380,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
onChange={(e) => setEditDraft(e.target.value)} onChange={(e) => setEditDraft(e.target.value)}
rows={3} rows={3}
autoFocus autoFocus
className="w-full input-field p-2 text-sm text-white resize-none" className="w-full input-field p-2 text-sm resize-none"
/> />
<div className="flex items-center justify-end gap-1.5"> <div className="flex items-center justify-end gap-1.5">
<button <button
@@ -398,7 +400,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
</div> </div>
</div> </div>
) : ( ) : (
<p className="text-base sm:text-xl font-medium text-white leading-relaxed break-words"> <p className="text-base sm:text-xl font-medium text-main leading-relaxed break-words">
{activeCard.content} {activeCard.content}
</p> </p>
)} )}
@@ -414,14 +416,14 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
</span> </span>
)) ))
) : ( ) : (
<span className="text-xs text-zinc-500"> <span className="text-xs text-faint">
Links wischen = Wegwerfen • Rechts wischen = Als Aufgabe sichern Links wischen = Wegwerfen • Rechts wischen = Als Aufgabe sichern
</span> </span>
)} )}
</div> </div>
{activeCard.dueDate && ( {activeCard.dueDate && (
<span className="px-2 py-0.5 bg-canvas text-zinc-300 border border-border text-xs"> <span className="px-2 py-0.5 bg-canvas text-muted border border-border rounded-btn text-xs">
📅 {activeCard.dueDate} 📅 {activeCard.dueDate}
</span> </span>
)} )}
@@ -432,8 +434,8 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
{/* Convert to Task with Due Date Popover */} {/* Convert to Task with Due Date Popover */}
{showDateModal && ( {showDateModal && (
<div className="p-3 bg-panel border border-border flex flex-wrap items-center justify-between gap-3"> <div className="p-3 panel-surface flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2 text-xs text-zinc-300"> <div className="flex items-center gap-2 text-xs text-muted">
<Calendar className="w-4 h-4 text-accent" /> <Calendar className="w-4 h-4 text-accent" />
<span>Aufgabe mit Fälligkeitsdatum anlegen:</span> <span>Aufgabe mit Fälligkeitsdatum anlegen:</span>
</div> </div>
@@ -456,8 +458,8 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
{/* Convert to Shared List Item Popover */} {/* Convert to Shared List Item Popover */}
{showListSelector && ( {showListSelector && (
<div className="p-3 bg-panel border border-border"> <div className="p-3 panel-surface">
<p className="text-xs text-zinc-400 mb-2"> <p className="text-xs text-muted mb-2">
Quick Dump direkt einer Liste zuordnen: Quick Dump direkt einer Liste zuordnen:
</p> </p>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
@@ -475,7 +477,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
</button> </button>
)) ))
) : ( ) : (
<span className="text-xs text-zinc-400">Keine Listen vorhanden.</span> <span className="text-xs text-muted">Keine Listen vorhanden.</span>
)} )}
</div> </div>
</div> </div>
@@ -489,7 +491,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
title="Wegwerfen (Taste H oder Pfeil links)" title="Wegwerfen (Taste H oder Pfeil links)"
className="btn-action h-8 px-2.5 justify-center" className="btn-action h-8 px-2.5 justify-center"
> >
<X className="w-3.5 h-3.5 text-zinc-400" /> <X className="w-3.5 h-3.5 text-muted" />
<span>Wegwerfen</span> <span>Wegwerfen</span>
</button> </button>
@@ -502,7 +504,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
cards.length <= 1 ? 'opacity-40 cursor-not-allowed' : '' cards.length <= 1 ? 'opacity-40 cursor-not-allowed' : ''
}`} }`}
> >
<SkipForward className="w-3.5 h-3.5 text-zinc-400" /> <SkipForward className="w-3.5 h-3.5 text-muted" />
<span>Später</span> <span>Später</span>
</button> </button>
@@ -514,7 +516,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
}} }}
title="Auf eine geteilte Liste setzen" title="Auf eine geteilte Liste setzen"
className={`btn-action h-8 px-2.5 justify-center ${ className={`btn-action h-8 px-2.5 justify-center ${
showListSelector ? 'border-accent text-accent bg-accent/20' : '' showListSelector ? 'border-accent text-accent bg-accent-dim' : ''
}`} }`}
> >
<ShoppingCart className="w-3.5 h-3.5 text-accent" /> <ShoppingCart className="w-3.5 h-3.5 text-accent" />
@@ -539,7 +541,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
}} }}
title="Als Aufgabe mit Fälligkeitsdatum (Taste T)" title="Als Aufgabe mit Fälligkeitsdatum (Taste T)"
className={`btn-action h-8 px-2.5 justify-center ${ className={`btn-action h-8 px-2.5 justify-center ${
showDateModal ? 'border-accent text-accent bg-accent/20' : '' showDateModal ? 'border-accent text-accent bg-accent-dim' : ''
}`} }`}
> >
<Calendar className="w-3.5 h-3.5 text-accent" /> <Calendar className="w-3.5 h-3.5 text-accent" />
@@ -561,16 +563,16 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
{cards.length > 1 && ( {cards.length > 1 && (
<div className="pt-2"> <div className="pt-2">
<div className="flex items-center justify-between mb-1.5 px-0.5"> <div className="flex items-center justify-between mb-1.5 px-0.5">
<span className="text-xs font-semibold uppercase tracking-wider text-zinc-400 flex items-center gap-1.5"> <span className="text-xs font-semibold uppercase tracking-wider text-muted flex items-center gap-1.5">
<Layers className="w-3.5 h-3.5 text-accent" /> <Layers className="w-3.5 h-3.5 text-accent" />
Warteschlange ({cards.length - 1} weitere Quick Dumps) Warteschlange ({cards.length - 1} weitere Quick Dumps)
</span> </span>
<span className="text-[11px] text-zinc-500"> <span className="text-[11px] text-faint">
Klicken zum Vorholen oder direkt zuordnen Klicken zum Vorholen oder direkt zuordnen
</span> </span>
</div> </div>
<div className="bg-panel border border-border divide-y divide-border max-h-60 overflow-y-auto"> <div className="panel-surface divide-y divide-border max-h-60 overflow-y-auto">
{cards.slice(1).map((item) => ( {cards.slice(1).map((item) => (
<div <div
key={item.id} key={item.id}
@@ -581,7 +583,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
className="flex-1 min-w-0 cursor-pointer flex items-center gap-2" className="flex-1 min-w-0 cursor-pointer flex items-center gap-2"
> >
<span className="w-1.5 h-1.5 bg-accent flex-shrink-0" /> <span className="w-1.5 h-1.5 bg-accent flex-shrink-0" />
<span className="text-xs text-zinc-200 truncate group-hover:text-white"> <span className="text-xs text-muted truncate group-hover:text-main">
{item.content} {item.content}
</span> </span>
</div> </div>
@@ -593,7 +595,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
handleAction('keep', { convertTo: 'task', thoughtOverride: item }) handleAction('keep', { convertTo: 'task', thoughtOverride: item })
} }
title="Direkt zur Aufgabe machen" title="Direkt zur Aufgabe machen"
className="h-6 px-2 bg-canvas hover:bg-accent hover:text-white border border-border text-[11px] text-zinc-300 flex items-center gap-1 transition-colors" className="h-6 px-2 bg-canvas hover:bg-accent hover:text-on-accent border border-border rounded-btn text-[11px] text-muted flex items-center gap-1 transition-colors"
> >
<Check className="w-3 h-3" /> <Check className="w-3 h-3" />
<span className="hidden sm:inline">Aufgabe</span> <span className="hidden sm:inline">Aufgabe</span>
@@ -605,7 +607,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
handleAction('keep', { convertTo: 'document', thoughtOverride: item }) handleAction('keep', { convertTo: 'document', thoughtOverride: item })
} }
title="Direkt als Workspace-Notiz anlegen" title="Direkt als Workspace-Notiz anlegen"
className="h-6 px-2 bg-canvas hover:bg-panel-elevated border border-border text-[11px] text-zinc-300 flex items-center gap-1 transition-colors" className="h-6 px-2 bg-canvas hover:bg-panel-elevated border border-border rounded-btn text-[11px] text-muted hover:text-main flex items-center gap-1 transition-colors"
> >
<FileText className="w-3 h-3 text-accent" /> <FileText className="w-3 h-3 text-accent" />
<span className="hidden sm:inline">Notiz</span> <span className="hidden sm:inline">Notiz</span>
@@ -622,7 +624,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
}) })
} }
title={`Direkt auf Liste "${lists[0].title}" setzen`} title={`Direkt auf Liste "${lists[0].title}" setzen`}
className="h-6 px-2 bg-canvas hover:bg-panel-elevated border border-border text-[11px] text-zinc-300 flex items-center gap-1 transition-colors" className="h-6 px-2 bg-canvas hover:bg-panel-elevated border border-border rounded-btn text-[11px] text-muted hover:text-main flex items-center gap-1 transition-colors"
> >
<ShoppingCart className="w-3 h-3 text-accent" /> <ShoppingCart className="w-3 h-3 text-accent" />
<span className="hidden md:inline">{lists[0].title}</span> <span className="hidden md:inline">{lists[0].title}</span>
@@ -633,7 +635,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
type="button" type="button"
onClick={() => handleAction('discard', { thoughtOverride: item })} onClick={() => handleAction('discard', { thoughtOverride: item })}
title="Wegwerfen" title="Wegwerfen"
className="h-6 w-6 bg-canvas hover:bg-panel-elevated border border-border text-zinc-400 hover:text-white flex items-center justify-center transition-colors" className="h-6 w-6 bg-canvas hover:bg-panel-elevated border border-border rounded-btn text-muted hover:text-main flex items-center justify-center transition-colors"
> >
<X className="w-3 h-3" /> <X className="w-3 h-3" />
</button> </button>
@@ -6,15 +6,12 @@ import {
FolderOpen, FolderOpen,
ChevronRight, ChevronRight,
ChevronDown, ChevronDown,
FileText,
Search, Search,
Pin, Pin,
Trash2, Trash2,
Download, Download,
CheckSquare, CheckSquare,
Heading1,
Heading2, Heading2,
Heading3,
List, List,
Code, Code,
Sparkles, Sparkles,
@@ -389,10 +386,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
e.stopPropagation(); e.stopPropagation();
if (target) setSelectedDoc(target); if (target) setSelectedDoc(target);
}} }}
className={`inline-flex items-center gap-1 px-1.5 py-0.2 mx-0.5 border text-xs font-medium cursor-pointer transition-colors ${ className={`inline-flex items-center gap-1 px-1.5 py-0.2 mx-0.5 border text-xs font-medium cursor-pointer rounded-btn transition-colors ${
target target
? 'bg-accent/20 border-accent/40 text-accent hover:bg-accent/30' ? 'bg-accent/20 border-accent/40 text-accent hover:bg-accent/30'
: 'bg-zinc-800 border-zinc-700 text-zinc-400 hover:text-white' : 'badge-neutral hover:text-main'
}`} }`}
title={target ? `Öffne: ${target.title}` : `Dokument "${linkTitle}" existiert noch nicht`} title={target ? `Öffne: ${target.title}` : `Dokument "${linkTitle}" existiert noch nicht`}
> >
@@ -467,8 +464,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
style={{ paddingLeft: `${depth * 14 + 16}px` }} style={{ paddingLeft: `${depth * 14 + 16}px` }}
className={`group flex items-center justify-between py-1.5 pr-2 border-l-2 transition-colors cursor-pointer text-xs ${ className={`group flex items-center justify-between py-1.5 pr-2 border-l-2 transition-colors cursor-pointer text-xs ${
isSelected isSelected
? 'bg-panel-elevated border-accent text-white font-medium' ? 'bg-panel-elevated border-accent text-main font-medium'
: 'border-transparent text-zinc-400 hover:text-white hover:bg-white/[0.02]' : 'border-transparent text-muted hover:text-main hover:bg-canvas/50'
}`} }`}
> >
<div className="flex items-center gap-2 min-w-0 flex-1"> <div className="flex items-center gap-2 min-w-0 flex-1">
@@ -490,10 +487,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<div <div
onClick={() => toggleFolder(node.path)} onClick={() => toggleFolder(node.path)}
style={{ paddingLeft: `${node.depth * 14 + 8}px` }} style={{ paddingLeft: `${node.depth * 14 + 8}px` }}
className="group flex items-center justify-between py-1.5 pr-2 hover:bg-panel-elevated cursor-pointer transition-colors text-xs text-zinc-300" className="group flex items-center justify-between py-1.5 pr-2 hover:bg-panel-elevated cursor-pointer transition-colors text-xs text-muted"
> >
<div className="flex items-center gap-1.5 min-w-0"> <div className="flex items-center gap-1.5 min-w-0">
<span className="text-zinc-500 hover:text-zinc-300"> <span className="text-faint hover:text-muted">
{isExpanded ? ( {isExpanded ? (
<ChevronDown className="w-3 h-3 flex-shrink-0" /> <ChevronDown className="w-3 h-3 flex-shrink-0" />
) : ( ) : (
@@ -503,10 +500,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{isExpanded ? ( {isExpanded ? (
<FolderOpen className="w-3.5 h-3.5 text-accent flex-shrink-0" /> <FolderOpen className="w-3.5 h-3.5 text-accent flex-shrink-0" />
) : ( ) : (
<Folder className="w-3.5 h-3.5 text-zinc-400 flex-shrink-0" /> <Folder className="w-3.5 h-3.5 text-muted flex-shrink-0" />
)} )}
<span className="font-medium text-xs text-zinc-200 truncate">{node.name}</span> <span className="font-medium text-xs text-main truncate">{node.name}</span>
<span className="text-[10px] text-zinc-500 font-mono">({totalItems})</span> <span className="text-[10px] text-faint font-mono">({totalItems})</span>
</div> </div>
<div className="opacity-0 group-hover:opacity-100 flex items-center gap-1 transition-opacity"> <div className="opacity-0 group-hover:opacity-100 flex items-center gap-1 transition-opacity">
@@ -516,7 +513,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
e.stopPropagation(); e.stopPropagation();
handleCreateDoc(node.path); handleCreateDoc(node.path);
}} }}
className="p-1 hover:bg-panel-hover text-zinc-400 hover:text-white" className="p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn"
title="Neue Datei in diesem Ordner" title="Neue Datei in diesem Ordner"
> >
<FilePlus className="w-3 h-3" /> <FilePlus className="w-3 h-3" />
@@ -527,7 +524,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
e.stopPropagation(); e.stopPropagation();
handleStartCreateFolder(node.path); handleStartCreateFolder(node.path);
}} }}
className="p-1 hover:bg-panel-hover text-zinc-400 hover:text-white" className="p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn"
title="Neuer Unterordner" title="Neuer Unterordner"
> >
<FolderPlus className="w-3 h-3" /> <FolderPlus className="w-3 h-3" />
@@ -540,7 +537,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
style={{ paddingLeft: `${(node.depth + 1) * 14 + 8}px` }} style={{ paddingLeft: `${(node.depth + 1) * 14 + 8}px` }}
className="py-1 pr-2 flex items-center gap-1 bg-canvas" className="py-1 pr-2 flex items-center gap-1 bg-canvas"
> >
<Folder className="w-3 h-3 text-zinc-400 flex-shrink-0" /> <Folder className="w-3 h-3 text-muted flex-shrink-0" />
<input <input
type="text" type="text"
autoFocus autoFocus
@@ -556,7 +553,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={handleConfirmCreateFolder} onClick={handleConfirmCreateFolder}
className="p-1 text-accent hover:text-white" className="p-1 text-accent hover:text-main rounded-btn"
title="Bestätigen" title="Bestätigen"
> >
<Check className="w-3.5 h-3.5" /> <Check className="w-3.5 h-3.5" />
@@ -564,7 +561,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => setIsCreatingFolder(false)} onClick={() => setIsCreatingFolder(false)}
className="p-1 text-zinc-400 hover:text-white" className="p-1 text-muted hover:text-main rounded-btn"
title="Abbrechen" title="Abbrechen"
> >
<X className="w-3.5 h-3.5" /> <X className="w-3.5 h-3.5" />
@@ -579,7 +576,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{node.subfolders.length === 0 && node.docs.length === 0 && !isCreatingSubfolder && ( {node.subfolders.length === 0 && node.docs.length === 0 && !isCreatingSubfolder && (
<div <div
style={{ paddingLeft: `${(node.depth + 1) * 14 + 14}px` }} style={{ paddingLeft: `${(node.depth + 1) * 14 + 14}px` }}
className="py-1 text-[11px] text-zinc-600 italic" className="py-1 text-[11px] text-faint italic"
> >
(leer) (leer)
</div> </div>
@@ -598,13 +595,13 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<div className="lg:col-span-4 panel-surface flex flex-col self-start"> <div className="lg:col-span-4 panel-surface flex flex-col self-start">
<div className="p-2 panel-header space-y-2"> <div className="p-2 panel-header space-y-2">
<div className="relative"> <div className="relative">
<Search className="w-3.5 h-3.5 text-zinc-400 absolute left-2.5 top-1/2 -translate-y-1/2" /> <Search className="w-3.5 h-3.5 text-muted absolute left-2.5 top-1/2 -translate-y-1/2" />
<input <input
type="text" type="text"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Dateien durchsuchen…" placeholder="Dateien durchsuchen…"
className="w-full h-7 pl-7 pr-2 input-field" className="w-full h-7 pl-7 pr-2 input-field text-xs"
/> />
</div> </div>
@@ -615,7 +612,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
className="flex-1 btn-action" className="flex-1 btn-action"
title="Neue Datei erstellen" title="Neue Datei erstellen"
> >
<FilePlus className="w-3.5 h-3.5 text-zinc-400" /> <FilePlus className="w-3.5 h-3.5 text-muted" />
<span>Neue Datei</span> <span>Neue Datei</span>
</button> </button>
@@ -625,7 +622,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
className="flex-1 btn-action" className="flex-1 btn-action"
title="Neuen Ordner erstellen" title="Neuen Ordner erstellen"
> >
<FolderPlus className="w-3.5 h-3.5 text-zinc-400" /> <FolderPlus className="w-3.5 h-3.5 text-muted" />
<span>Neuer Ordner</span> <span>Neuer Ordner</span>
</button> </button>
</div> </div>
@@ -633,7 +630,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{isCreatingFolder && newFolderParent === null && ( {isCreatingFolder && newFolderParent === null && (
<div className="p-2 panel-header bg-canvas flex items-center gap-1.5"> <div className="p-2 panel-header bg-canvas flex items-center gap-1.5">
<Folder className="w-3.5 h-3.5 text-zinc-400 flex-shrink-0" /> <Folder className="w-3.5 h-3.5 text-muted flex-shrink-0" />
<input <input
type="text" type="text"
autoFocus autoFocus
@@ -644,12 +641,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
if (e.key === 'Escape') setIsCreatingFolder(false); if (e.key === 'Escape') setIsCreatingFolder(false);
}} }}
placeholder="Neuer Ordnername…" placeholder="Neuer Ordnername…"
className="flex-1 h-6 px-1.5 input-field" className="flex-1 h-6 px-1.5 input-field text-xs"
/> />
<button <button
type="button" type="button"
onClick={handleConfirmCreateFolder} onClick={handleConfirmCreateFolder}
className="p-1 text-accent hover:text-white" className="p-1 text-accent hover:text-main rounded-btn"
title="Bestätigen" title="Bestätigen"
> >
<Check className="w-3.5 h-3.5" /> <Check className="w-3.5 h-3.5" />
@@ -657,7 +654,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => setIsCreatingFolder(false)} onClick={() => setIsCreatingFolder(false)}
className="p-1 text-zinc-400 hover:text-white" className="p-1 text-muted hover:text-main rounded-btn"
title="Abbrechen" title="Abbrechen"
> >
<X className="w-3.5 h-3.5" /> <X className="w-3.5 h-3.5" />
@@ -666,10 +663,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
)} )}
{isEmpty ? ( {isEmpty ? (
<div className="p-6 my-3 mx-2 border border-dashed border-border bg-canvas/50 text-center"> <div className="p-6 my-3 mx-2 border border-dashed border-border bg-canvas/50 text-center rounded-card">
<Folder className="w-7 h-7 text-zinc-600 mx-auto mb-2" /> <Folder className="w-7 h-7 text-faint mx-auto mb-2" />
<h5 className="text-xs font-semibold text-zinc-300 mb-1">Keine Dateien vorhanden</h5> <h5 className="text-xs font-semibold text-muted mb-1">Keine Dateien vorhanden</h5>
<p className="text-[11px] text-zinc-500 mb-3 max-w-xs mx-auto leading-relaxed"> <p className="text-[11px] text-faint mb-3 max-w-xs mx-auto leading-relaxed">
Erstelle eine neue Datei oder lege Ordner an, um deine Dokumente übersichtlich zu strukturieren. Erstelle eine neue Datei oder lege Ordner an, um deine Dokumente übersichtlich zu strukturieren.
</p> </p>
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
@@ -678,7 +675,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
onClick={() => handleCreateDoc(null)} onClick={() => handleCreateDoc(null)}
className="btn-action" className="btn-action"
> >
<FilePlus className="w-3.5 h-3.5 text-zinc-400" /> <FilePlus className="w-3.5 h-3.5 text-muted" />
<span>Neue Datei</span> <span>Neue Datei</span>
</button> </button>
<button <button
@@ -686,7 +683,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
onClick={() => handleStartCreateFolder(null)} onClick={() => handleStartCreateFolder(null)}
className="btn-action" className="btn-action"
> >
<FolderPlus className="w-3.5 h-3.5 text-zinc-400" /> <FolderPlus className="w-3.5 h-3.5 text-muted" />
<span>Neuer Ordner</span> <span>Neuer Ordner</span>
</button> </button>
</div> </div>
@@ -696,7 +693,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{rootTree.map(renderFolder)} {rootTree.map(renderFolder)}
{rootDocs.map((doc) => renderDocItem(doc, 0))} {rootDocs.map((doc) => renderDocItem(doc, 0))}
{filteredDocs.length === 0 && searchQuery && ( {filteredDocs.length === 0 && searchQuery && (
<div className="p-4 text-center text-xs text-zinc-500"> <div className="p-4 text-center text-xs text-faint">
Keine Dateien gefunden für &quot;{searchQuery}&quot; Keine Dateien gefunden für &quot;{searchQuery}&quot;
</div> </div>
)} )}
@@ -710,14 +707,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<div className="p-4 panel-surface space-y-3"> <div className="p-4 panel-surface space-y-3">
{/* Document Header & Metadata */} {/* Document Header & Metadata */}
<div className="flex flex-wrap items-center justify-between gap-3 pb-3 panel-header"> <div className="flex flex-wrap items-center justify-between gap-3 pb-3 panel-header">
<div className="flex flex-wrap items-center gap-2 text-xs text-zinc-400"> <div className="flex flex-wrap items-center gap-2 text-xs text-muted">
<span className="badge-accent"> <span className="badge-accent">
<Check className="w-3 h-3" /> <Check className="w-3 h-3" />
{saveState === 'saving' ? 'Speichert…' : 'Gespeichert'} {saveState === 'saving' ? 'Speichert…' : 'Gespeichert'}
</span> </span>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Folder className="w-3.5 h-3.5 text-zinc-500" /> <Folder className="w-3.5 h-3.5 text-faint" />
<select <select
value={getDocFolder(selectedDoc)} value={getDocFolder(selectedDoc)}
onChange={(e) => handleChangeDocFolder(e.target.value)} onChange={(e) => handleChangeDocFolder(e.target.value)}
@@ -773,14 +770,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => setShowIconPicker((prev) => !prev)} onClick={() => setShowIconPicker((prev) => !prev)}
className="w-10 h-10 panel-surface hover:bg-panel-elevated text-xl flex items-center justify-center flex-shrink-0 transition-colors" className="w-10 h-10 panel-surface hover:bg-panel-elevated text-xl flex items-center justify-center flex-shrink-0 transition-colors rounded-card"
title="Icon ändern" title="Icon ändern"
> >
{selectedDoc.icon} {selectedDoc.icon}
</button> </button>
{showIconPicker && ( {showIconPicker && (
<div className="absolute top-12 left-0 z-20 p-2.5 panel-surface flex flex-wrap gap-1 w-64 shadow-xl border border-border"> <div className="absolute top-12 left-0 z-20 p-2.5 panel-surface flex flex-wrap gap-1 w-64 shadow-panel border border-border rounded-card">
{ICONS.map((ic) => ( {ICONS.map((ic) => (
<button <button
key={ic} key={ic}
@@ -789,7 +786,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
persistDocument({ ...selectedDoc, icon: ic }); persistDocument({ ...selectedDoc, icon: ic });
setShowIconPicker(false); setShowIconPicker(false);
}} }}
className="w-8 h-8 hover:bg-panel-elevated text-base flex items-center justify-center transition-colors" className="w-8 h-8 hover:bg-panel-elevated text-base flex items-center justify-center rounded-btn transition-colors"
> >
{ic} {ic}
</button> </button>
@@ -802,17 +799,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
value={selectedDoc.title} value={selectedDoc.title}
onChange={(e) => persistDocument({ ...selectedDoc, title: e.target.value })} onChange={(e) => persistDocument({ ...selectedDoc, title: e.target.value })}
placeholder="Dokument-Titel…" placeholder="Dokument-Titel…"
className="w-full bg-transparent text-xl sm:text-2xl font-bold text-white focus:outline-none" className="w-full bg-transparent text-xl sm:text-2xl font-bold text-main focus:outline-none placeholder:text-faint"
/> />
</div> </div>
{/* NOTION / WORD RICH FORMATTING TOOLBAR */} {/* NOTION / WORD RICH FORMATTING TOOLBAR */}
<div className="p-1.5 bg-canvas border border-border flex flex-wrap items-center gap-1 text-xs"> <div className="p-1.5 bg-canvas border border-border rounded-card flex flex-wrap items-center gap-1 text-xs">
{/* Text styling buttons */} {/* Text styling buttons */}
<button <button
type="button" type="button"
onClick={() => applyInlineFormat('**')} onClick={() => applyInlineFormat('**')}
className="p-1 hover:bg-panel text-zinc-300 hover:text-white" className="p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn"
title="Fett (**text**)" title="Fett (**text**)"
> >
<Bold className="w-3.5 h-3.5" /> <Bold className="w-3.5 h-3.5" />
@@ -820,7 +817,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => applyInlineFormat('*')} onClick={() => applyInlineFormat('*')}
className="p-1 hover:bg-panel text-zinc-300 hover:text-white" className="p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn"
title="Kursiv (*text*)" title="Kursiv (*text*)"
> >
<Italic className="w-3.5 h-3.5" /> <Italic className="w-3.5 h-3.5" />
@@ -828,7 +825,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => applyInlineFormat('~~')} onClick={() => applyInlineFormat('~~')}
className="p-1 hover:bg-panel text-zinc-300 hover:text-white" className="p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn"
title="Durchgestrichen (~~text~~)" title="Durchgestrichen (~~text~~)"
> >
<Strikethrough className="w-3.5 h-3.5" /> <Strikethrough className="w-3.5 h-3.5" />
@@ -836,7 +833,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => applyInlineFormat('`')} onClick={() => applyInlineFormat('`')}
className="p-1 hover:bg-panel text-zinc-300 hover:text-white" className="p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn"
title="Inline Code (`code`)" title="Inline Code (`code`)"
> >
<Code className="w-3.5 h-3.5" /> <Code className="w-3.5 h-3.5" />
@@ -844,7 +841,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => applyInlineFormat('[Link](', ')')} onClick={() => applyInlineFormat('[Link](', ')')}
className="p-1 hover:bg-panel text-zinc-300 hover:text-white" className="p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn"
title="Link einfügen" title="Link einfügen"
> >
<Link className="w-3.5 h-3.5" /> <Link className="w-3.5 h-3.5" />
@@ -857,14 +854,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => setShowWikiLinkMenu((prev) => !prev)} onClick={() => setShowWikiLinkMenu((prev) => !prev)}
className="px-2 py-0.5 bg-panel border border-border text-[11px] text-accent flex items-center gap-1 hover:bg-panel-hover" className="px-2 py-0.5 bg-panel border border-border text-[11px] text-accent flex items-center gap-1 hover:bg-panel-elevated rounded-btn"
title="Auf anderes Dokument verlinken ([[Dokumentname]])" title="Auf anderes Dokument verlinken ([[Dokumentname]])"
> >
<span>[[ Wiki-Link ]]</span> <span>[[ Wiki-Link ]]</span>
</button> </button>
{showWikiLinkMenu && ( {showWikiLinkMenu && (
<div className="absolute top-7 left-0 z-30 w-56 bg-panel border border-border p-1.5 shadow-2xl space-y-1"> <div className="absolute top-7 left-0 z-30 w-56 bg-panel border border-border p-1.5 shadow-panel rounded-card space-y-1">
<p className="text-[10px] text-zinc-400 px-1.5 pb-1 border-b border-border"> <p className="text-[10px] text-muted px-1.5 pb-1 border-b border-border">
Dokument zum Verlinken wählen: Dokument zum Verlinken wählen:
</p> </p>
<div className="max-h-48 overflow-y-auto"> <div className="max-h-48 overflow-y-auto">
@@ -875,14 +872,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
key={d.id} key={d.id}
type="button" type="button"
onClick={() => insertWikiLink(d.title)} onClick={() => insertWikiLink(d.title)}
className="w-full text-left px-2 py-1 text-xs text-zinc-300 hover:text-white hover:bg-panel-hover truncate flex items-center gap-1.5" className="w-full text-left px-2 py-1 text-xs text-muted hover:text-main hover:bg-panel-elevated rounded-btn truncate flex items-center gap-1.5"
> >
<span>{d.icon}</span> <span>{d.icon}</span>
<span className="truncate">{d.title}</span> <span className="truncate">{d.title}</span>
</button> </button>
))} ))}
{docs.length <= 1 && ( {docs.length <= 1 && (
<div className="p-2 text-center text-[11px] text-zinc-500"> <div className="p-2 text-center text-[11px] text-faint">
Keine weiteren Dokumente vorhanden Keine weiteren Dokumente vorhanden
</div> </div>
)} )}
@@ -903,7 +900,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
'| Spalte 1 | Spalte 2 | Spalte 3 |\n|---|---|---|\n| Inhalt A | Inhalt B | Inhalt C |\n| Wert 1 | Wert 2 | Wert 3 |' '| Spalte 1 | Spalte 2 | Spalte 3 |\n|---|---|---|\n| Inhalt A | Inhalt B | Inhalt C |\n| Wert 1 | Wert 2 | Wert 3 |'
) )
} }
className="px-2 py-0.5 bg-panel border border-border text-[11px] text-zinc-300 hover:text-white flex items-center gap-1" className="px-2 py-0.5 bg-panel border border-border text-[11px] text-muted hover:text-main rounded-btn flex items-center gap-1"
title="Tabelle einfügen" title="Tabelle einfügen"
> >
<TableIcon className="w-3 h-3 text-accent" /> <TableIcon className="w-3 h-3 text-accent" />
@@ -913,7 +910,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => addBlock('progress', undefined, '50')} onClick={() => addBlock('progress', undefined, '50')}
className="px-2 py-0.5 bg-panel border border-border text-[11px] text-zinc-300 hover:text-white flex items-center gap-1" className="px-2 py-0.5 bg-panel border border-border text-[11px] text-muted hover:text-main rounded-btn flex items-center gap-1"
title="Fortschrittsbalken einfügen" title="Fortschrittsbalken einfügen"
> >
<Percent className="w-3 h-3 text-accent" /> <Percent className="w-3 h-3 text-accent" />
@@ -923,7 +920,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => addBlock('divider')} onClick={() => addBlock('divider')}
className="p-1 hover:bg-panel text-zinc-400 hover:text-white" className="p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn"
title="Trennlinie" title="Trennlinie"
> >
<Minus className="w-3.5 h-3.5" /> <Minus className="w-3.5 h-3.5" />
@@ -946,7 +943,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
value={block.content} value={block.content}
onChange={(e) => updateBlock(block.id, { content: e.target.value })} onChange={(e) => updateBlock(block.id, { content: e.target.value })}
placeholder="Überschrift 1…" placeholder="Überschrift 1…"
className="w-full bg-transparent text-xl font-bold text-white focus:outline-none py-1" className="w-full bg-transparent text-xl font-bold text-main focus:outline-none py-1 placeholder:text-faint"
/> />
)} )}
@@ -957,7 +954,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
value={block.content} value={block.content}
onChange={(e) => updateBlock(block.id, { content: e.target.value })} onChange={(e) => updateBlock(block.id, { content: e.target.value })}
placeholder="Abschnitts-Überschrift…" placeholder="Abschnitts-Überschrift…"
className="w-full bg-transparent text-base font-semibold text-accent focus:outline-none pt-2 pb-1" className="w-full bg-transparent text-base font-semibold text-accent focus:outline-none pt-2 pb-1 placeholder:text-faint"
/> />
)} )}
@@ -968,7 +965,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
value={block.content} value={block.content}
onChange={(e) => updateBlock(block.id, { content: e.target.value })} onChange={(e) => updateBlock(block.id, { content: e.target.value })}
placeholder="Unterabschnitt…" placeholder="Unterabschnitt…"
className="w-full bg-transparent text-sm font-semibold text-zinc-200 focus:outline-none pt-1" className="w-full bg-transparent text-sm font-semibold text-main focus:outline-none pt-1 placeholder:text-faint"
/> />
)} )}
@@ -980,10 +977,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
value={block.content} value={block.content}
onChange={(e) => updateBlock(block.id, { content: e.target.value })} onChange={(e) => updateBlock(block.id, { content: e.target.value })}
placeholder="Text schreiben… (nutze [[Dokument]] für Wiki-Links)" placeholder="Text schreiben… (nutze [[Dokument]] für Wiki-Links)"
className="w-full bg-transparent text-xs sm:text-sm text-zinc-200 leading-relaxed focus:outline-none resize-none font-normal" className="w-full bg-transparent text-xs sm:text-sm text-main leading-relaxed focus:outline-none resize-none font-normal placeholder:text-faint"
/> />
{block.content.includes('[[') && ( {block.content.includes('[[') && (
<div className="text-xs text-zinc-400 pt-0.5"> <div className="text-xs text-muted pt-0.5">
{renderTextWithWikiLinks(block.content)} {renderTextWithWikiLinks(block.content)}
</div> </div>
)} )}
@@ -997,7 +994,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
type="checkbox" type="checkbox"
checked={!!block.checked} checked={!!block.checked}
onChange={(e) => updateBlock(block.id, { checked: e.target.checked })} onChange={(e) => updateBlock(block.id, { checked: e.target.checked })}
className="w-3.5 h-3.5 accent-accent cursor-pointer" className="w-4 h-4 checkbox-square cursor-pointer"
/> />
<div className="flex-1"> <div className="flex-1">
<input <input
@@ -1005,12 +1002,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
value={block.content} value={block.content}
onChange={(e) => updateBlock(block.id, { content: e.target.value })} onChange={(e) => updateBlock(block.id, { content: e.target.value })}
placeholder="To-Do Punkt…" placeholder="To-Do Punkt…"
className={`w-full bg-transparent text-xs sm:text-sm focus:outline-none ${ className={`w-full bg-transparent text-xs sm:text-sm focus:outline-none placeholder:text-faint ${
block.checked ? 'line-through text-zinc-500' : 'text-white' block.checked ? 'line-through text-faint' : 'text-main'
}`} }`}
/> />
{block.content.includes('[[') && ( {block.content.includes('[[') && (
<div className="text-xs text-zinc-400"> <div className="text-xs text-muted">
{renderTextWithWikiLinks(block.content)} {renderTextWithWikiLinks(block.content)}
</div> </div>
)} )}
@@ -1021,17 +1018,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* BULLET */} {/* BULLET */}
{block.type === 'bullet' && ( {block.type === 'bullet' && (
<div className="flex items-center gap-2.5 py-0.5"> <div className="flex items-center gap-2.5 py-0.5">
<span className="w-1.5 h-1.5 bg-accent flex-shrink-0" /> <span className="w-1.5 h-1.5 bg-accent rounded-full flex-shrink-0" />
<div className="flex-1"> <div className="flex-1">
<input <input
type="text" type="text"
value={block.content} value={block.content}
onChange={(e) => updateBlock(block.id, { content: e.target.value })} onChange={(e) => updateBlock(block.id, { content: e.target.value })}
placeholder="Listenpunkt…" placeholder="Listenpunkt…"
className="w-full bg-transparent text-xs sm:text-sm text-zinc-200 focus:outline-none" className="w-full bg-transparent text-xs sm:text-sm text-main focus:outline-none placeholder:text-faint"
/> />
{block.content.includes('[[') && ( {block.content.includes('[[') && (
<div className="text-xs text-zinc-400"> <div className="text-xs text-muted">
{renderTextWithWikiLinks(block.content)} {renderTextWithWikiLinks(block.content)}
</div> </div>
)} )}
@@ -1041,7 +1038,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* CALLOUT */} {/* CALLOUT */}
{block.type === 'callout' && ( {block.type === 'callout' && (
<div className="p-2.5 bg-canvas border border-accent/40 flex items-start gap-2"> <div className="p-2.5 bg-canvas border border-accent/40 rounded-card flex items-start gap-2">
<Sparkles className="w-3.5 h-3.5 text-accent flex-shrink-0 mt-0.5" /> <Sparkles className="w-3.5 h-3.5 text-accent flex-shrink-0 mt-0.5" />
<div className="flex-1 space-y-1"> <div className="flex-1 space-y-1">
<textarea <textarea
@@ -1049,10 +1046,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
value={block.content} value={block.content}
onChange={(e) => updateBlock(block.id, { content: e.target.value })} onChange={(e) => updateBlock(block.id, { content: e.target.value })}
placeholder="Wichtiger Hinweis oder Zusammenfassung…" placeholder="Wichtiger Hinweis oder Zusammenfassung…"
className="w-full bg-transparent text-xs sm:text-sm text-zinc-200 focus:outline-none resize-none" className="w-full bg-transparent text-xs sm:text-sm text-main focus:outline-none resize-none placeholder:text-faint"
/> />
{block.content.includes('[[') && ( {block.content.includes('[[') && (
<div className="text-xs text-zinc-400"> <div className="text-xs text-muted">
{renderTextWithWikiLinks(block.content)} {renderTextWithWikiLinks(block.content)}
</div> </div>
)} )}
@@ -1062,22 +1059,22 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* CODE */} {/* CODE */}
{block.type === 'code' && ( {block.type === 'code' && (
<div className="p-2.5 bg-canvas border border-border font-mono text-xs text-zinc-200"> <div className="p-2.5 code-block">
<textarea <textarea
rows={Math.max(2, (block.content || '').split('\n').length)} rows={Math.max(2, (block.content || '').split('\n').length)}
value={block.content} value={block.content}
onChange={(e) => updateBlock(block.id, { content: e.target.value })} onChange={(e) => updateBlock(block.id, { content: e.target.value })}
placeholder="// Code oder Shell-Kommando…" placeholder="// Code oder Shell-Kommando…"
className="w-full bg-transparent focus:outline-none resize-none font-mono" className="w-full bg-transparent focus:outline-none resize-none font-mono text-xs text-main placeholder:text-faint"
/> />
</div> </div>
)} )}
{/* TABLE */} {/* TABLE */}
{block.type === 'table' && ( {block.type === 'table' && (
<div className="p-2.5 bg-canvas border border-border space-y-2"> <div className="p-2.5 bg-canvas border border-border rounded-card space-y-2">
<div className="flex items-center justify-between text-[11px] text-zinc-400 border-b border-border pb-1"> <div className="flex items-center justify-between text-[11px] text-muted border-b border-border pb-1">
<span className="flex items-center gap-1 font-semibold text-zinc-300"> <span className="flex items-center gap-1 font-semibold text-main">
<TableIcon className="w-3 h-3 text-accent" /> Markdown Tabelle <TableIcon className="w-3 h-3 text-accent" /> Markdown Tabelle
</span> </span>
</div> </div>
@@ -1086,25 +1083,25 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
value={block.content} value={block.content}
onChange={(e) => updateBlock(block.id, { content: e.target.value })} onChange={(e) => updateBlock(block.id, { content: e.target.value })}
placeholder="| Spalte 1 | Spalte 2 |\n|---|---|\n| Inhalt | Wert |" placeholder="| Spalte 1 | Spalte 2 |\n|---|---|\n| Inhalt | Wert |"
className="w-full bg-transparent font-mono text-xs text-zinc-200 focus:outline-none resize-none" className="w-full bg-transparent font-mono text-xs text-main focus:outline-none resize-none placeholder:text-faint"
/> />
</div> </div>
)} )}
{/* PROGRESS BAR */} {/* PROGRESS BAR */}
{block.type === 'progress' && ( {block.type === 'progress' && (
<div className="p-2.5 bg-canvas border border-border space-y-2"> <div className="p-2.5 bg-canvas border border-border rounded-card space-y-2">
<div className="flex items-center justify-between text-xs"> <div className="flex items-center justify-between text-xs">
<span className="text-zinc-300 font-medium flex items-center gap-1"> <span className="text-main font-medium flex items-center gap-1">
<Percent className="w-3 h-3 text-accent" /> Fortschritt <Percent className="w-3 h-3 text-accent" /> Fortschritt
</span> </span>
<span className="font-mono text-accent font-semibold"> <span className="font-mono text-accent font-semibold">
{parseInt(block.content) || 0}% {parseInt(block.content) || 0}%
</span> </span>
</div> </div>
<div className="w-full bg-panel h-2.5 overflow-hidden border border-border"> <div className="w-full bg-panel h-2.5 overflow-hidden border border-border rounded-pill">
<div <div
className="bg-accent h-full transition-all" className="bg-accent h-full transition-all rounded-pill"
style={{ width: `${Math.min(100, Math.max(0, parseInt(block.content) || 0))}%` }} style={{ width: `${Math.min(100, Math.max(0, parseInt(block.content) || 0))}%` }}
/> />
</div> </div>
@@ -1132,7 +1129,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => moveBlock(idx, -1)} onClick={() => moveBlock(idx, -1)}
className="p-1 hover:bg-panel-elevated text-zinc-400 hover:text-white transition-colors" className="p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn transition-colors"
title="Nach oben" title="Nach oben"
> >
<ArrowUp className="w-3 h-3" /> <ArrowUp className="w-3 h-3" />
@@ -1140,7 +1137,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => moveBlock(idx, 1)} onClick={() => moveBlock(idx, 1)}
className="p-1 hover:bg-panel-elevated text-zinc-400 hover:text-white transition-colors" className="p-1 hover:bg-panel-elevated text-muted hover:text-main rounded-btn transition-colors"
title="Nach unten" title="Nach unten"
> >
<ArrowDown className="w-3 h-3" /> <ArrowDown className="w-3 h-3" />
@@ -1148,7 +1145,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => removeBlock(block.id)} onClick={() => removeBlock(block.id)}
className="p-1 hover:bg-red-500/20 text-zinc-400 hover:text-red-400 transition-colors" className="p-1 hover:bg-status-err/20 text-muted hover:text-status-err rounded-btn transition-colors"
title="Block entfernen" title="Block entfernen"
> >
<Trash2 className="w-3 h-3" /> <Trash2 className="w-3 h-3" />
@@ -1160,7 +1157,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* Bottom Add Block Toolbar */} {/* Bottom Add Block Toolbar */}
<div className="mt-4 pt-3 border-t border-border flex flex-wrap items-center gap-1.5"> <div className="mt-4 pt-3 border-t border-border flex flex-wrap items-center gap-1.5">
<span className="text-xs text-zinc-400 mr-1">+ Neuer Block:</span> <span className="text-xs text-muted mr-1">+ Neuer Block:</span>
<button <button
type="button" type="button"
@@ -1242,7 +1239,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</div> </div>
</div> </div>
) : ( ) : (
<div className="p-8 panel-surface text-center text-zinc-400 text-xs"> <div className="p-8 panel-surface text-center text-muted text-xs">
Wähle links eine Datei im Datei-Explorer aus oder erstelle eine neue Datei. Wähle links eine Datei im Datei-Explorer aus oder erstelle eine neue Datei.
</div> </div>
)} )}
+123 -50
View File
@@ -1,47 +1,23 @@
/* ==========================================================================
MINDSIFT THEME ENGINE — ZENTRALE EINBINDUNG
Jedes Theme ist eine eigenständige CSS-Datei unter src/themes/ mit 6 Gruppen:
1. Akzentfarbe
2. Oberflächen & Hintergründe
3. Text & Kontraste
4. Rahmen, Linien & Schatten
5. Status- & Signalfarben
6. Rundungen (Geometrie)
========================================================================== */
@import './themes/theme-dark.css';
@import './themes/theme-light.css';
@import './themes/theme-high-contrast.css';
@import './themes/theme-soft.css';
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* ==========================================================================
MINDFSIFT DESIGN SYSTEM - ZENTRALE STEUERUNG
Hier können Farben, Blauton, Kontraste und Rundungen zentral angepasst werden!
========================================================================== */
:root {
/* --- 1. AKZENTFARBE (Blauton hier regeln) --- */
--accent-primary: #1d8cf8; /* Haupt-Akzentblau */
--accent-hover: #3b9cfc; /* Hellere Nuance beim Hover */
--accent-dim: rgba(29, 140, 248, 0.14); /* Dezenter Hintergrund für Badges */
--accent-border: rgba(29, 140, 248, 0.35);/* Rand für aktive Badges */
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE --- */
--bg-app: #16171a; /* Gesamte Canvas-Hintergrundfarbe */
--bg-panel: #222428; /* Karten, Sidebar, Header, Modals */
--bg-panel-elevated: #282a2f; /* Ausgewählte Menüpunkte, Toolbars, Hover */
--bg-panel-hover: #32353b; /* Stärkerer Hover-Effekt */
--bg-input: #16171a; /* Eingabefelder und Quick-Inputs */
/* --- 3. RAHMEN & TRENNLINIEN --- */
--border-color: rgba(255, 255, 255, 0.08); /* Subtile Linien & Kanten */
--border-subtle: rgba(255, 255, 255, 0.05);
/* --- 4. TEXTFARBEN --- */
--text-main: #ffffff; /* Haupttext */
--text-muted: #a1a1aa; /* Sekundärtext (Grau) */
--text-faint: #71717a; /* Platzhalter & Hilfetexte */
/* --- 5. STATUS- UND SIGNALFARBEN --- */
--color-success: #22c55e;
--color-danger: #ef4444;
--color-warning: #f97316;
/* --- 6. RUNDUNGEN (Zentral konfigurierbar) --- */
--radius-card: 0px; /* Karten & Panels (0px = eckig) */
--radius-btn: 0px; /* Standard-Buttons & Tabs */
--radius-input: 0px; /* Textfelder für Optionen & Eigenschaften */
--radius-pill: 9999px; /* Runde Elemente (z.B. Quick-Capture) */
}
* { * {
box-sizing: border-box; box-sizing: border-box;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
@@ -52,9 +28,10 @@ body {
color: var(--text-main); color: var(--text-main);
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow-x: hidden; overflow-x: hidden;
transition: background-color 140ms ease, color 140ms ease;
} }
/* Subtiler monochromer Scrollbar */ /* Scrollbar über Theme-Variablen */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 5px; width: 5px;
height: 5px; height: 5px;
@@ -65,16 +42,16 @@ body {
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.12); background: var(--scrollbar-thumb);
border-radius: var(--radius-btn); border-radius: var(--radius-btn);
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.22); background: var(--scrollbar-thumb-hover);
} }
/* ========================================================================== /* ==========================================================================
WIEDERVERWENDBARE SEMANTISCHE KLASSEN SEMANTISCHE KOMPONENTEN-KLASSEN (100% Theme-gesteuert)
========================================================================== */ ========================================================================== */
@layer components { @layer components {
@@ -85,11 +62,33 @@ body {
border-radius: var(--radius-card); border-radius: var(--radius-card);
} }
.panel-elevated {
background-color: var(--bg-panel-elevated);
border: 1px solid var(--border-color);
border-radius: var(--radius-card);
}
.panel-header { .panel-header {
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
/* Monochromer, eckiger Standard-Aktionsbutton */ /* Automatische Übernahme von --radius-card auf gerahmten Panels & Boxen */
.bg-panel.border,
.bg-canvas.border {
border-radius: var(--radius-card);
}
/* Quick-Capture-Blase & Profil-Pill im Header */
.capture-pill {
border-radius: var(--radius-pill);
}
/* Aufgaben-Checkboxen */
.checkbox-square {
border-radius: var(--radius-checkbox);
}
/* Standard-Aktionsbutton */
.btn-action { .btn-action {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -122,8 +121,9 @@ body {
padding-left: 0.75rem; padding-left: 0.75rem;
padding-right: 0.75rem; padding-right: 0.75rem;
background-color: var(--accent-primary); background-color: var(--accent-primary);
border: 1px solid var(--accent-border);
border-radius: var(--radius-btn); border-radius: var(--radius-btn);
color: #ffffff; color: var(--text-on-accent);
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 500;
transition: all 120ms ease; transition: all 120ms ease;
@@ -133,7 +133,7 @@ body {
background-color: var(--accent-hover); background-color: var(--accent-hover);
} }
/* Kleiner eckiger Icon-Button */ /* Kleiner Icon-Button */
.btn-icon { .btn-icon {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -166,9 +166,9 @@ body {
cursor: pointer; cursor: pointer;
} }
.btn-icon-danger:hover { .btn-icon-danger:hover {
background-color: rgba(239, 68, 68, 0.15); background-color: var(--bg-danger);
color: var(--color-danger); color: var(--color-danger);
border-color: rgba(239, 68, 68, 0.3); border-color: var(--border-danger);
} }
/* Zentrales Texteingabefeld */ /* Zentrales Texteingabefeld */
@@ -211,7 +211,7 @@ body {
background-color: var(--bg-panel-elevated); background-color: var(--bg-panel-elevated);
} }
/* Status Badge */ /* Status Badges */
.badge-accent { .badge-accent {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -219,11 +219,84 @@ body {
padding: 0.125rem 0.5rem; padding: 0.125rem 0.5rem;
background-color: var(--accent-dim); background-color: var(--accent-dim);
border: 1px solid var(--accent-border); border: 1px solid var(--accent-border);
border-radius: var(--radius-btn);
color: var(--accent-primary); color: var(--accent-primary);
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 500;
} }
.badge-neutral {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.375rem;
background-color: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--radius-btn);
color: var(--text-muted);
font-size: 0.65rem;
font-weight: 500;
}
.badge-ok {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.375rem;
background-color: var(--bg-success);
border: 1px solid var(--border-success);
border-radius: var(--radius-btn);
color: var(--color-success);
font-size: 0.65rem;
font-weight: 500;
}
.badge-err {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.375rem;
background-color: var(--bg-danger);
border: 1px solid var(--border-danger);
border-radius: var(--radius-btn);
color: var(--color-danger);
font-size: 0.65rem;
font-weight: 500;
}
/* Status Alert Boxen */
.alert-ok {
padding: 0.75rem;
background-color: var(--bg-success);
border: 1px solid var(--border-success);
border-radius: var(--radius-card);
color: var(--text-main);
font-size: 0.75rem;
}
.alert-err {
padding: 0.75rem;
background-color: var(--bg-danger);
border: 1px solid var(--border-danger);
border-radius: var(--radius-card);
color: var(--color-danger);
font-size: 0.75rem;
}
/* Code- & Terminal-Blöcke */
.code-block {
background-color: var(--bg-code);
border: 1px solid var(--border-color);
border-radius: var(--radius-card);
color: var(--text-main);
}
/* Modal-Hintergrund-Overlay */
.modal-overlay {
background-color: var(--bg-overlay);
backdrop-filter: blur(4px);
}
/* File Tree Item */ /* File Tree Item */
.tree-node-active { .tree-node-active {
background-color: var(--bg-panel-elevated); background-color: var(--bg-panel-elevated);
+93
View File
@@ -0,0 +1,93 @@
export type ThemeId = 'dark' | 'light' | 'high-contrast' | 'soft';
export interface ThemeOption {
id: ThemeId;
name: string;
subtitle: string;
file: string;
previewBg: string;
previewPanel: string;
previewAccent: string;
previewText: string;
isRounded: boolean;
}
export const THEME_STORAGE_KEY = 'mindsift_active_theme';
export const AVAILABLE_THEMES: ThemeOption[] = [
{
id: 'dark',
name: 'MindSift Dark (Standard)',
subtitle: 'Monochrom, dunkel & eckig (0px)',
file: 'src/themes/theme-dark.css',
previewBg: '#121316',
previewPanel: '#1a1c20',
previewAccent: '#2563eb',
previewText: '#f4f4f5',
isRounded: false,
},
{
id: 'light',
name: 'MindSift Light (Tagmodus)',
subtitle: 'Helles Papier-Design & klare Kontraste',
file: 'src/themes/theme-light.css',
previewBg: '#f3f4f6',
previewPanel: '#ffffff',
previewAccent: '#2563eb',
previewText: '#111827',
isRounded: false,
},
{
id: 'high-contrast',
name: 'High Contrast (Scharf)',
subtitle: 'Tiefschwarz (#000) & weiße Konturen',
file: 'src/themes/theme-high-contrast.css',
previewBg: '#000000',
previewPanel: '#09090b',
previewAccent: '#38bdf8',
previewText: '#ffffff',
isRounded: false,
},
{
id: 'soft',
name: 'Soft Slate (Abgerundet)',
subtitle: 'Schiefer-Blau mit weichen Radien (10px)',
file: 'src/themes/theme-soft.css',
previewBg: '#0f172a',
previewPanel: '#1e293b',
previewAccent: '#3b82f6',
previewText: '#f8fafc',
isRounded: true,
},
];
export function getActiveTheme(): ThemeId {
try {
const saved = localStorage.getItem(THEME_STORAGE_KEY) as ThemeId | null;
if (saved && AVAILABLE_THEMES.some((t) => t.id === saved)) {
return saved;
}
} catch {
// ignore
}
return 'dark';
}
export function setActiveTheme(themeId: ThemeId): void {
try {
localStorage.setItem(THEME_STORAGE_KEY, themeId);
} catch {
// ignore
}
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-theme', themeId);
}
}
export function initThemeOnBoot(): ThemeId {
const active = getActiveTheme();
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-theme', active);
}
return active;
}
+56
View File
@@ -0,0 +1,56 @@
/* ==========================================================================
MINDSIFT THEME: DARK MONOCHROME (Standard)
Alle Farben, Kontraste und Rundungen zentral in einer einzigen Datei.
========================================================================== */
:root,
:root[data-theme="dark"] {
/* --- 1. AKZENTFARBE --- */
--accent-primary: #2563eb;
--accent-hover: #3b82f6;
--accent-dim: rgba(37, 99, 235, 0.14);
--accent-border: rgba(59, 130, 246, 0.45);
--text-on-accent: #ffffff;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE --- */
--bg-app: #121316;
--bg-panel: #1a1c20;
--bg-panel-elevated: #22252a;
--bg-panel-hover: #2a2e35;
--bg-input: #121316;
--bg-overlay: rgba(0, 0, 0, 0.72);
--bg-code: #0d0e11;
/* --- 3. TEXT & KONTRASTE --- */
--text-main: #f4f4f5;
--text-muted: #a1a1aa;
--text-faint: #71717a;
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(255, 255, 255, 0.09);
--border-subtle: rgba(255, 255, 255, 0.05);
--border-strong: rgba(255, 255, 255, 0.22);
--shadow-panel: 0 10px 30px rgba(0, 0, 0, 0.45);
--scrollbar-thumb: rgba(255, 255, 255, 0.14);
--scrollbar-thumb-hover: rgba(255, 255, 255, 0.26);
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #22c55e;
--bg-success: rgba(34, 197, 94, 0.12);
--border-success: rgba(34, 197, 94, 0.38);
--color-danger: #ef4444;
--bg-danger: rgba(239, 68, 68, 0.12);
--border-danger: rgba(239, 68, 68, 0.38);
--color-warning: #f59e0b;
--bg-warning: rgba(245, 158, 11, 0.12);
--border-warning: rgba(245, 158, 11, 0.38);
/* --- 6. RUNDUNGEN (Geometrie) --- */
--radius-card: 0px;
--radius-btn: 2px;
--radius-input: 2px;
--radius-checkbox: 0px;
--radius-pill: 9999px;
}
@@ -0,0 +1,55 @@
/* ==========================================================================
MINDSIFT THEME: HIGH CONTRAST (Maximaler Kontrast & Schärfe)
Alle Farben, Kontraste und Rundungen zentral in einer einzigen Datei.
========================================================================== */
:root[data-theme="high-contrast"] {
/* --- 1. AKZENTFARBE --- */
--accent-primary: #38bdf8;
--accent-hover: #7dd3fc;
--accent-dim: rgba(56, 189, 248, 0.22);
--accent-border: #38bdf8;
--text-on-accent: #000000;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE --- */
--bg-app: #000000;
--bg-panel: #09090b;
--bg-panel-elevated: #18181b;
--bg-panel-hover: #27272a;
--bg-input: #000000;
--bg-overlay: rgba(0, 0, 0, 0.88);
--bg-code: #000000;
/* --- 3. TEXT & KONTRASTE --- */
--text-main: #ffffff;
--text-muted: #e4e4e7;
--text-faint: #a1a1aa;
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(255, 255, 255, 0.38);
--border-subtle: rgba(255, 255, 255, 0.20);
--border-strong: #ffffff;
--shadow-panel: 0 0 0 1px #ffffff;
--scrollbar-thumb: rgba(255, 255, 255, 0.45);
--scrollbar-thumb-hover: #ffffff;
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #4ade80;
--bg-success: rgba(74, 222, 128, 0.20);
--border-success: #4ade80;
--color-danger: #f87171;
--bg-danger: rgba(248, 113, 113, 0.20);
--border-danger: #f87171;
--color-warning: #fbbf24;
--bg-warning: rgba(251, 191, 36, 0.20);
--border-warning: #fbbf24;
/* --- 6. RUNDUNGEN (Geometrie: Komplett scharfkantig) --- */
--radius-card: 0px;
--radius-btn: 0px;
--radius-input: 0px;
--radius-checkbox: 0px;
--radius-pill: 0px;
}
+55
View File
@@ -0,0 +1,55 @@
/* ==========================================================================
MINDSIFT THEME: LIGHT (Tagmodus / Hell & Klar)
Alle Farben, Kontraste und Rundungen zentral in einer einzigen Datei.
========================================================================== */
:root[data-theme="light"] {
/* --- 1. AKZENTFARBE --- */
--accent-primary: #2563eb;
--accent-hover: #1d4ed8;
--accent-dim: rgba(37, 99, 235, 0.10);
--accent-border: rgba(37, 99, 235, 0.38);
--text-on-accent: #ffffff;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE --- */
--bg-app: #f3f4f6;
--bg-panel: #ffffff;
--bg-panel-elevated: #ebedf0;
--bg-panel-hover: #e2e5ea;
--bg-input: #ffffff;
--bg-overlay: rgba(15, 23, 42, 0.45);
--bg-code: #f1f5f9;
/* --- 3. TEXT & KONTRASTE --- */
--text-main: #111827;
--text-muted: #4b5563;
--text-faint: #9ca3af;
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(17, 24, 39, 0.13);
--border-subtle: rgba(17, 24, 39, 0.06);
--border-strong: rgba(17, 24, 39, 0.28);
--shadow-panel: 0 8px 24px rgba(15, 23, 42, 0.08);
--scrollbar-thumb: rgba(17, 24, 39, 0.18);
--scrollbar-thumb-hover: rgba(17, 24, 39, 0.32);
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #15803d;
--bg-success: rgba(22, 163, 74, 0.10);
--border-success: rgba(22, 163, 74, 0.35);
--color-danger: #b91c1c;
--bg-danger: rgba(220, 38, 38, 0.09);
--border-danger: rgba(220, 38, 38, 0.35);
--color-warning: #b45309;
--bg-warning: rgba(217, 119, 6, 0.10);
--border-warning: rgba(217, 119, 6, 0.35);
/* --- 6. RUNDUNGEN (Geometrie) --- */
--radius-card: 0px;
--radius-btn: 2px;
--radius-input: 2px;
--radius-checkbox: 0px;
--radius-pill: 9999px;
}
+56
View File
@@ -0,0 +1,56 @@
/* ==========================================================================
MINDSIFT THEME: SOFT SLATE (Abgerundete Geometrie & Sanftes Schiefer-Blau)
Demonstriert, wie über die Rundungs-Variablen in einer einzigen Datei
das gesamte App-Layout auf abgerundete Ecken umschaltet.
========================================================================== */
:root[data-theme="soft"] {
/* --- 1. AKZENTFARBE --- */
--accent-primary: #3b82f6;
--accent-hover: #60a5fa;
--accent-dim: rgba(59, 130, 246, 0.15);
--accent-border: rgba(96, 165, 250, 0.40);
--text-on-accent: #ffffff;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE --- */
--bg-app: #0f172a;
--bg-panel: #1e293b;
--bg-panel-elevated: #293548;
--bg-panel-hover: #334155;
--bg-input: #0f172a;
--bg-overlay: rgba(2, 6, 23, 0.72);
--bg-code: #090d16;
/* --- 3. TEXT & KONTRASTE --- */
--text-main: #f8fafc;
--text-muted: #94a3b8;
--text-faint: #64748b;
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(148, 163, 184, 0.16);
--border-subtle: rgba(148, 163, 184, 0.08);
--border-strong: rgba(148, 163, 184, 0.32);
--shadow-panel: 0 12px 32px rgba(2, 6, 23, 0.40);
--scrollbar-thumb: rgba(148, 163, 184, 0.22);
--scrollbar-thumb-hover: rgba(148, 163, 184, 0.38);
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #10b981;
--bg-success: rgba(16, 185, 129, 0.14);
--border-success: rgba(16, 185, 129, 0.38);
--color-danger: #f43f5e;
--bg-danger: rgba(244, 63, 94, 0.14);
--border-danger: rgba(244, 63, 94, 0.38);
--color-warning: #f59e0b;
--bg-warning: rgba(245, 158, 11, 0.14);
--border-warning: rgba(245, 158, 11, 0.38);
/* --- 6. RUNDUNGEN (Weich abgerundete Ecken überall) --- */
--radius-card: 10px;
--radius-btn: 6px;
--radius-input: 6px;
--radius-checkbox: 4px;
--radius-pill: 9999px;
}
+56
View File
@@ -0,0 +1,56 @@
/* ==========================================================================
MINDSIFT THEME-VORLAGE (TEMPLATE)
Kopiere diese Datei, benenne [data-theme="mein-theme"] um und passe
die 6 Gruppen nach deinen Wünschen an.
========================================================================== */
:root[data-theme="custom-template"] {
/* --- 1. AKZENTFARBE (Buttons, aktive Tabs, Fokus-Ränder, Badges) --- */
--accent-primary: #2563eb;
--accent-hover: #3b82f6;
--accent-dim: rgba(37, 99, 235, 0.14);
--accent-border: rgba(59, 130, 246, 0.45);
--text-on-accent: #ffffff;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE --- */
--bg-app: #121316; /* Seiten-Hintergrund (Canvas) */
--bg-panel: #1a1c20; /* Karten, Header, Sidebar, Dropdowns */
--bg-panel-elevated: #22252a; /* Hervorgehobene Elemente & aktive Zeilen */
--bg-panel-hover: #2a2e35; /* Hover-Effekt auf Oberflächen */
--bg-input: #121316; /* Textfelder, Selects, Textareas */
--bg-overlay: rgba(0, 0, 0, 0.72); /* Hintergrund bei Modals/Dialogen */
--bg-code: #0d0e11; /* Code-Blöcke & Terminal-Boxen */
/* --- 3. TEXT & KONTRASTE --- */
--text-main: #f4f4f5; /* Haupttext & Überschriften */
--text-muted: #a1a1aa; /* Sekundärtext & Beschreibungen */
--text-faint: #71717a; /* Platzhalter & dezente Hinweise */
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(255, 255, 255, 0.09);
--border-subtle: rgba(255, 255, 255, 0.05);
--border-strong: rgba(255, 255, 255, 0.22);
--shadow-panel: 0 10px 30px rgba(0, 0, 0, 0.45);
--scrollbar-thumb: rgba(255, 255, 255, 0.14);
--scrollbar-thumb-hover: rgba(255, 255, 255, 0.26);
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #22c55e;
--bg-success: rgba(34, 197, 94, 0.12);
--border-success: rgba(34, 197, 94, 0.38);
--color-danger: #ef4444;
--bg-danger: rgba(239, 68, 68, 0.12);
--border-danger: rgba(239, 68, 68, 0.38);
--color-warning: #f59e0b;
--bg-warning: rgba(245, 158, 11, 0.12);
--border-warning: rgba(245, 158, 11, 0.38);
/* --- 6. RUNDUNGEN (Geometrie) --- */
--radius-card: 0px; /* Karten, Panels, Container (0px = eckig) */
--radius-btn: 2px; /* Buttons & Badges */
--radius-input: 2px; /* Eingabefelder & Dropdowns */
--radius-checkbox: 0px; /* Aufgaben-Checkboxen */
--radius-pill: 9999px; /* Quick-Capture-Blase & Profil-Pill */
}
+33 -1
View File
@@ -17,6 +17,13 @@ export default {
hover: 'var(--bg-panel-hover)', hover: 'var(--bg-panel-hover)',
border: 'var(--border-color)', border: 'var(--border-color)',
}, },
input: 'var(--bg-input)',
overlay: 'var(--bg-overlay)',
code: 'var(--bg-code)',
main: 'var(--text-main)',
muted: 'var(--text-muted)',
faint: 'var(--text-faint)',
'on-accent': 'var(--text-on-accent)',
accent: { accent: {
DEFAULT: 'var(--accent-primary)', DEFAULT: 'var(--accent-primary)',
hover: 'var(--accent-hover)', hover: 'var(--accent-hover)',
@@ -28,20 +35,45 @@ export default {
hover: 'var(--accent-hover)', hover: 'var(--accent-hover)',
dark: 'var(--accent-primary)', dark: 'var(--accent-primary)',
}, },
status: {
ok: 'var(--color-success)',
'ok-bg': 'var(--bg-success)',
'ok-border': 'var(--border-success)',
err: 'var(--color-danger)',
'err-bg': 'var(--bg-danger)',
'err-border': 'var(--border-danger)',
warn: 'var(--color-warning)',
'warn-bg': 'var(--bg-warning)',
'warn-border': 'var(--border-warning)',
},
border: 'var(--border-color)', border: 'var(--border-color)',
}, },
borderColor: { borderColor: {
DEFAULT: 'var(--border-color)', DEFAULT: 'var(--border-color)',
border: 'var(--border-color)', border: 'var(--border-color)',
subtle: 'var(--border-subtle)', subtle: 'var(--border-subtle)',
strong: 'var(--border-strong)',
}, },
borderRadius: { borderRadius: {
card: 'var(--radius-card)', card: 'var(--radius-card)',
btn: 'var(--radius-btn)', btn: 'var(--radius-btn)',
input: 'var(--radius-input)', input: 'var(--radius-input)',
checkbox: 'var(--radius-checkbox)',
pill: 'var(--radius-pill)',
},
boxShadow: {
panel: 'var(--shadow-panel)',
}, },
fontFamily: { fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'], sans: [
'Inter',
'system-ui',
'-apple-system',
'BlinkMacSystemFont',
'Segoe UI',
'Roboto',
'sans-serif',
],
mono: ['JetBrains Mono', 'Fira Code', 'monospace'], mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
}, },
}, },